Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const languageCode = (locale: Locale): string => locale;
export const FIELDS = [
'id',
'slug',
'status',
'order',
...HERO_IMG_KEYS.flatMap((key) => [`${key}.id`, `${key}.width`, `${key}.height`]),
...HERO_GIF_KEYS.flatMap((key) => [`${key}.id`, `${key}.width`, `${key}.height`]),
Expand Down Expand Up @@ -67,6 +68,7 @@ export function buildHeroQueryParams(
): URLSearchParams {
const params = new URLSearchParams();
if (options?.slug) params.set('filter[slug][_eq]', options.slug);
params.set('filter[status][_eq]', 'Published');
if (options?.limit) params.set('limit', options.limit);
params.set('fields', FIELDS);
params.set('sort', 'order');
Expand Down
82 changes: 82 additions & 0 deletions frontend-next-migration/src/entities/Hero/model/heroApi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { HeroGroup, HeroSlug } from '../types/hero';
import { groupHeroesByGroup } from './groupHeroesByGroup';
import { fetchAllHeroes, fetchHeroBySlug } from './heroApi';

jest.mock('@/shared/const/envHelper', () => ({
envHelper: { directusHost: 'https://directus.test' },
}));

const publishedHero = {
id: 1,
slug: HeroSlug.OVEREATER,
status: 'Published',
order: 1,
translations: [
{
languages_code: 'en-US',
title: 'Overeater',
description: 'Published hero',
alt: 'Overeater',
altGif: 'Overeater animated',
},
],
group: {
key: HeroGroup.RETROFLECTOR,
bgColour: '#000',
translations: [{ languages_code: 'en-US', name: 'Retroflector', description: 'Group' }],
},
heroes_stats: [],
};

const response = (data: unknown) =>
({
ok: true,
json: async () => ({ data }),
text: async () => JSON.stringify({ data }),
}) as Response;

describe('heroApi status gating', () => {
const fetchMock = jest.fn() as jest.MockedFunction<typeof fetch>;

beforeEach(() => {
fetchMock.mockReset();
global.fetch = fetchMock;
});

it.each(['Draft', 'Archived'])('does not return a %s hero by slug', async (status) => {
fetchMock.mockImplementation(async (input) => {
const url = new URL(String(input));
expect(url.searchParams.get('filter[status][_eq]')).toBe('Published');
return response([{ ...publishedHero, status }]);
});

await expect(fetchHeroBySlug('draft-hero' as HeroSlug)).resolves.toBeUndefined();
});

it('returns only the published heroes from the all-heroes request', async () => {
fetchMock.mockImplementation(async (input) => {
const url = new URL(String(input));
expect(url.searchParams.get('filter[status][_eq]')).toBe('Published');
return response([publishedHero]);
});

const heroes = await fetchAllHeroes();

expect(heroes).toHaveLength(1);
expect(heroes[0].slug).toBe(HeroSlug.OVEREATER);
});

it('groups the published heroes returned by the filtered request', async () => {
fetchMock.mockImplementation(async (input) => {
const url = new URL(String(input));
expect(url.searchParams.get('filter[status][_eq]')).toBe('Published');
return response([publishedHero]);
});

const heroes = await fetchAllHeroes();
const result = groupHeroesByGroup(heroes);

expect(result[HeroGroup.RETROFLECTOR].heroes).toHaveLength(1);
expect(result[HeroGroup.RETROFLECTOR].heroes[0].slug).toBe(HeroSlug.OVEREATER);
});
});
15 changes: 10 additions & 5 deletions frontend-next-migration/src/entities/Hero/model/heroApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const normalizeLocale = (locale: Locale): string => {
return localeMap[locale] || locale;
};
const languageCode = (locale: Locale): string => normalizeLocale(locale);
const isPublished = (item: any): boolean => item?.status === 'Published';

function pickTranslationByLocale<T extends { languages_code?: string }>(
arr: T[] | undefined,
Expand All @@ -46,6 +47,7 @@ function pickTranslationByLocale<T extends { languages_code?: string }>(
const FIELDS = [
'id',
'slug',
'status',
'order',
...HERO_IMG_KEYS.flatMap((key) => [`${key}.id`, `${key}.width`, `${key}.height`]),
...HERO_GIF_KEYS.flatMap((key) => [`${key}.id`, `${key}.width`, `${key}.height`]),
Expand Down Expand Up @@ -175,6 +177,7 @@ function mapHero(item: any, locale: Locale): HeroWithGroup {
function buildParams(locale: Locale, options?: { slug?: string; limit?: string }) {
const params = new URLSearchParams();
if (options?.slug) params.set('filter[slug][_eq]', options.slug);
params.set('filter[status][_eq]', 'Published');
if (options?.limit) params.set('limit', options.limit);
params.set('fields', FIELDS);
params.set('sort', 'order');
Expand All @@ -195,7 +198,9 @@ export const heroApi = directusApi.injectEndpoints({
}),
transformResponse: (resp: any, _meta, args) => {
const item = resp?.data?.[0];
return item ? mapHero(item, (args?.locale ?? 'en') as Locale) : undefined;
return item && isPublished(item)
? mapHero(item, (args?.locale ?? 'en') as Locale)
: undefined;
},
providesTags: (_res, _err, args) => [{ type: 'Hero' as const, id: args.slug }],
}),
Expand All @@ -216,7 +221,7 @@ export const heroApi = directusApi.injectEndpoints({
url: `/items/heroes?${buildParams(locale, { limit: '-1' }).toString()}`,
}),
transformResponse: (resp: any, _meta, args) => {
const items = resp?.data || [];
const items = (resp?.data || []).filter(isPublished);
return items.map((item: any) => mapHero(item, (args?.locale ?? 'en') as Locale));
},
providesTags: () => [{ type: 'Hero' as const, id: 'LIST' }],
Expand All @@ -226,7 +231,7 @@ export const heroApi = directusApi.injectEndpoints({
url: `/items/heroes?${buildParams(locale, { limit: '-1' }).toString()}`,
}),
transformResponse: (resp: any, _meta, args) => {
const items = resp?.data || [];
const items = (resp?.data || []).filter(isPublished);
const heroes = items.map((item: any) =>
mapHero(item, (args?.locale ?? 'en') as Locale),
);
Expand Down Expand Up @@ -273,7 +278,7 @@ export async function fetchHeroBySlug(

const json = await res.json();
const item = json?.data?.[0];
if (!item) {
if (!item || !isPublished(item)) {
// eslint-disable-next-line no-console
console.warn(
`[fetchHeroBySlug] No hero found in Directus for slug "${slug}" (response had ${json?.data?.length || 0} items)`,
Expand Down Expand Up @@ -318,7 +323,7 @@ export async function fetchAllHeroes(locale: Locale = 'en'): Promise<HeroWithGro
}

const json = await res.json();
const items = json?.data || [];
const items = (json?.data || []).filter(isPublished);
return items.map((item: any) => mapHero(item, locale));
} catch (error) {
// eslint-disable-next-line no-console
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
'use client';
import React from 'react';
import { useInView } from 'react-intersection-observer';
import { HeroManager } from '@/entities/Hero';
import { useGetHeroGroupsQuery } from '@/entities/Hero/model/heroApi';
import { Button, ButtonSize, ButtonTheme } from '@/shared/ui/Button';
import { classNames } from '@/shared/lib/classNames/classNames';
import { useClientTranslation } from '@/shared/i18n';
import { Container } from '@/shared/ui/Container';
import { AppLink } from '@/shared/ui/AppLink/AppLink';
import { useParams } from 'next/navigation';
import HeroesBlocks from './heroesBlocks/HeroesBlocks';
import cls from './main.module.scss';

Expand Down Expand Up @@ -34,8 +37,16 @@ function Main(props: Props) {
};

const { t } = useClientTranslation('heroes');
const heroManager = new HeroManager(t);
const heroesGroups2 = heroManager.getGroupsWithHeroesAsArray();
const params = useParams();
const lng = (params?.lng as string) || 'en';
const locale = (lng === 'en' ? 'en' : lng === 'fi' ? 'fi' : 'ru') as 'en' | 'fi' | 'ru';
const { data: directusGroups, isError, isLoading } = useGetHeroGroupsQuery({ locale });
const staticGroups = React.useMemo(() => new HeroManager(t).getGroupsWithHeroesAsArray(), [t]);
const heroesGroups2 = React.useMemo(() => {
if (isLoading) return [];
if (isError) return staticGroups;
return directusGroups ? Object.values(directusGroups) : [];
}, [directusGroups, isError, isLoading, staticGroups]);

const displayedGroups = maxGroupsPerPage
? heroesGroups2.slice(0, maxGroupsPerPage)
Expand Down