diff --git a/frontend-next-migration/src/entities/Hero/model/buildHeroQueryParams.ts b/frontend-next-migration/src/entities/Hero/model/buildHeroQueryParams.ts index c1504b3fc..f9434aea8 100644 --- a/frontend-next-migration/src/entities/Hero/model/buildHeroQueryParams.ts +++ b/frontend-next-migration/src/entities/Hero/model/buildHeroQueryParams.ts @@ -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`]), @@ -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'); diff --git a/frontend-next-migration/src/entities/Hero/model/heroApi.test.ts b/frontend-next-migration/src/entities/Hero/model/heroApi.test.ts new file mode 100644 index 000000000..b4bc3e3dc --- /dev/null +++ b/frontend-next-migration/src/entities/Hero/model/heroApi.test.ts @@ -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; + + 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); + }); +}); diff --git a/frontend-next-migration/src/entities/Hero/model/heroApi.ts b/frontend-next-migration/src/entities/Hero/model/heroApi.ts index c541e3cd4..68f8e9494 100644 --- a/frontend-next-migration/src/entities/Hero/model/heroApi.ts +++ b/frontend-next-migration/src/entities/Hero/model/heroApi.ts @@ -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( arr: T[] | undefined, @@ -46,6 +47,7 @@ function pickTranslationByLocale( 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`]), @@ -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'); @@ -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 }], }), @@ -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' }], @@ -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), ); @@ -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)`, @@ -318,7 +323,7 @@ export async function fetchAllHeroes(locale: Locale = 'en'): Promise mapHero(item, locale)); } catch (error) { // eslint-disable-next-line no-console diff --git a/frontend-next-migration/src/shared/assets/images/altLogoold.png b/frontend-next-migration/src/shared/assets/images/altLogoold.png index cc4540b40..388432320 100644 Binary files a/frontend-next-migration/src/shared/assets/images/altLogoold.png and b/frontend-next-migration/src/shared/assets/images/altLogoold.png differ diff --git a/frontend-next-migration/src/widgets/SectionHeroesBlocks/ui/index.tsx b/frontend-next-migration/src/widgets/SectionHeroesBlocks/ui/index.tsx index 12e81b7be..3bd97237e 100644 --- a/frontend-next-migration/src/widgets/SectionHeroesBlocks/ui/index.tsx +++ b/frontend-next-migration/src/widgets/SectionHeroesBlocks/ui/index.tsx @@ -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'; @@ -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)