Skip to content
Draft
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { createPage } from '@/app/_helpers';
import { getServerTranslation } from '@/shared/i18n';
import { notFound } from 'next/navigation';
import { HeroGroup } from '@/entities/Hero';
import { initializeHeroGroups } from '@/entities/Hero/model/initializeHeroGroups';
import { HeroGroup, GroupInfo } from '@/entities/Hero';
import {
initializeHeroGroups,
initializeHeroGroupsFromDirectus,
} from '@/entities/Hero/model/initializeHeroGroups';
import { StaticImageData } from 'next/image';
import { SingleDefensePageProps } from '@/preparedPages/DefenseGalleryPages';
import { getRouteDefenseGalleryGroupPage } from '@/shared/appLinks/RoutePaths';
import { baseUrl, defaultOpenGraph } from '@/shared/seoConstants';

function getOgImageUrl(info?: ReturnType<typeof initializeHeroGroups>[HeroGroup]) {
function getOgImageUrl(info?: GroupInfo) {
const candidate =
(info?.srcImg as string | StaticImageData | undefined) ??
(info?.heroes?.[0]?.srcImg as string | StaticImageData | undefined) ??
Expand All @@ -27,7 +30,17 @@ export async function _getPage(lng: string, heroGroup: string) {
}

const group = heroGroup as HeroGroup;
const groups = initializeHeroGroups(t);
// Try to fetch from Directus first, fallback to static data
let groups: Record<HeroGroup, GroupInfo>;
try {
groups = await initializeHeroGroupsFromDirectus(lng as 'en' | 'fi' | 'ru');
// If Directus returns empty, fallback to static
if (Object.keys(groups).length === 0) {
groups = initializeHeroGroups(t);
}
} catch {
groups = initializeHeroGroups(t);
}
const info = groups[group];

const groupName = info?.name ?? group;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,22 @@ function getOgImageUrl(hero: HeroWithGroup) {
export async function _getPage(lng: string, slug: string) {
const { t } = await getServerTranslation(lng, 'heroes');
const heroManager = new HeroManager(t);
const currentHero = heroManager.getHeroBySlug(slug as HeroSlug);

// Initialize from Directus first
await heroManager.initializeFromDirectus(lng as 'en' | 'fi' | 'ru');

// Try to get hero from Directus, fallback to static data
let currentHero = await heroManager.getHeroBySlugAsync(
slug as HeroSlug,
lng as 'en' | 'fi' | 'ru',
);
if (!currentHero) {
currentHero = heroManager.getHeroBySlug(slug as HeroSlug);
}
if (!currentHero) {
notFound();
}

currentHero.stats.forEach((stat) => {
stat.color = color[stat.name];
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,48 @@ function getOgImageUrl(hero: HeroWithGroup) {
return /^https?:\/\//i.test(src) ? src : `${baseUrl}${src}`;
}

export async function _getPage(lng: string, slug: string) {
const { t } = await getServerTranslation(lng, 'heroes');
const heroManager = new HeroManager(t);

const currentHero = heroManager.getHeroBySlug(slug as HeroSlug);
async function getCurrentHero(
heroManager: HeroManager,
slug: string,
lng: string,
): Promise<HeroWithGroup> {
const locale = lng as 'en' | 'fi' | 'ru';
let currentHero = await heroManager.getHeroBySlugAsync(slug as HeroSlug, locale);
if (!currentHero) {
// eslint-disable-next-line no-console
console.log(`[heroes/_getPage] Hero "${slug}" not found in Directus, trying static data`);
currentHero = heroManager.getHeroBySlug(slug as HeroSlug);
}
if (!currentHero) {
// eslint-disable-next-line no-console
console.error(`[heroes/_getPage] Hero "${slug}" not found in Directus or static data`);
notFound();
}
return currentHero;
}

const heroes = heroManager.getAllHeroes();
function getNavigationHeroes(
heroes: HeroWithGroup[],
currentHero: HeroWithGroup,
): { prevHero: HeroWithGroup; nextHero: HeroWithGroup } {
const currentHeroIndex = heroes.findIndex((hero) => hero.id === currentHero.id);
const prevHero =
heroManager.getHeroBeforeSpecificHero(currentHero.id) || (heroes.at(-1) as HeroWithGroup);
currentHeroIndex > 0
? heroes[currentHeroIndex - 1]
: (heroes.at(-1) as HeroWithGroup | undefined);
const nextHero =
heroManager.getHeroAfterSpecificHero(currentHero.id) || (heroes.at(0) as HeroWithGroup);
currentHeroIndex < heroes.length - 1
? heroes[currentHeroIndex + 1]
: (heroes.at(0) as HeroWithGroup | undefined);

const prevHeroLink = getRouteOneHeroPage(prevHero.slug);
const nextHeroLink = getRouteOneHeroPage(nextHero.slug);
// Fallback to current hero if navigation heroes not found
return {
prevHero: prevHero || currentHero,
nextHero: nextHero || currentHero,
};
}

// Routes & SEO
function buildSeoData(currentHero: HeroWithGroup, lng: string, t: (key: string) => string) {
const relPath = getRouteOneHeroPage(encodeURIComponent(currentHero.slug));
const path = `/${lng}${relPath}`;
const title = currentHero.title;
Expand All @@ -41,6 +64,37 @@ export async function _getPage(lng: string, slug: string) {
: null;
const ogImages = ogImage ? [ogImage] : (defaultOpenGraph.images ?? []);

return {
title,
description,
keywords,
path,
ogImages,
};
}

export async function _getPage(lng: string, slug: string) {
const { t } = await getServerTranslation(lng, 'heroes');
const heroManager = new HeroManager(t);

// Initialize from Directus first (this may fail silently and use static data)
await heroManager.initializeFromDirectus(lng as 'en' | 'fi' | 'ru');

// Get current hero with fallback
const currentHero = await getCurrentHero(heroManager, slug, lng);

// Get all heroes for navigation (try Directus first, fallback to static)
let heroes = await heroManager.getAllHeroesFromDirectus(lng as 'en' | 'fi' | 'ru');
if (heroes.length === 0) {
heroes = heroManager.getAllHeroes();
}

const { prevHero, nextHero } = getNavigationHeroes(heroes, currentHero);
const prevHeroLink = getRouteOneHeroPage(prevHero.slug);
const nextHeroLink = getRouteOneHeroPage(nextHero.slug);

const seoData = buildSeoData(currentHero, lng, t);

return createPage<SingleHeroPageProps>({
buildPage: () => ({
slug: currentHero.slug,
Expand All @@ -49,18 +103,18 @@ export async function _getPage(lng: string, slug: string) {
nextHeroLink: nextHeroLink,
}),
buildSeo: () => ({
title,
description,
keywords,
title: seoData.title,
description: seoData.description,
keywords: seoData.keywords,
openGraph: {
...defaultOpenGraph,
type: 'website',
title,
description,
url: path,
images: ogImages,
title: seoData.title,
description: seoData.description,
url: seoData.path,
images: seoData.ogImages,
},
alternates: { canonical: path },
alternates: { canonical: seoData.path },
}),
});
}
73 changes: 70 additions & 3 deletions frontend-next-migration/src/entities/Hero/model/HeroManager.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { GroupInfo, HeroWithGroup, HeroGroup, HeroSlug } from '../types/hero';
// import { HeroLevel, HeroStats } from '../types/HeroStats';
import { initializeHeroGroups } from './initializeHeroGroups';
import { initializeHeroGroups, initializeHeroGroupsFromDirectus } from './initializeHeroGroups';
// import { HeroStatsManager } from './stats';
import { fetchHeroBySlug, fetchAllHeroes, type Locale } from './heroApi';

export class HeroManager {
private readonly t: (key: string) => string;
private readonly heroGroups: Record<HeroGroup, GroupInfo>;
private heroGroups: Record<HeroGroup, GroupInfo>;
private heroesCache: HeroWithGroup[] | null = null;
// private heroStatsManager: HeroStatsManager;

constructor(t: (key: string) => string) {
Expand All @@ -14,12 +16,36 @@ export class HeroManager {
// this.heroStatsManager = new HeroStatsManager();
}

/**
* Initialize hero groups from Directus (async)
*/
public async initializeFromDirectus(locale: Locale = 'en'): Promise<void> {
try {
const directusGroups = await initializeHeroGroupsFromDirectus(locale);
// Only replace static data if Directus returned non-empty groups
if (directusGroups && Object.keys(directusGroups).length > 0) {
this.heroGroups = directusGroups;
this.heroesCache = null; // Clear cache to force recalculation
} else {
console.warn('[HeroManager] Directus returned empty groups, keeping static data');
}
} catch (error) {
console.error('[HeroManager] Failed to initialize hero groups from Directus:', error);
// Keep existing static data as fallback
}
}

// public getHeroStatsBySlugAndLevel(slug: HeroSlug, statLevel: HeroLevel): HeroStats {
// return this.heroStatsManager.getStatsForHero(slug, statLevel);
// }

public getAllHeroes(): HeroWithGroup[] {
return Object.entries(this.heroGroups).flatMap(([group, groupInfo]) => {
// Use cache if available
if (this.heroesCache) {
return this.heroesCache;
}

const heroes = Object.entries(this.heroGroups).flatMap(([group, groupInfo]) => {
const {
name: groupName,
description: groupDescription,
Expand All @@ -35,6 +61,30 @@ export class HeroManager {
groupBgColour,
}));
});

this.heroesCache = heroes;
return heroes;
}

/**
* Get all heroes from Directus (async)
*/
public async getAllHeroesFromDirectus(locale: Locale = 'en'): Promise<HeroWithGroup[]> {
try {
const heroes = await fetchAllHeroes(locale);
// If Directus returns empty array, fallback to static data
if (heroes.length === 0) {
console.warn(
'[HeroManager] Directus returned empty heroes array, using static data',
);
return this.getAllHeroes();
}
return heroes;
} catch (error) {
console.error('[HeroManager] Failed to fetch all heroes from Directus:', error);
// Fallback to static data
return this.getAllHeroes();
}
}

public getGroupsWithHeroes(): Record<HeroGroup, GroupInfo> {
Expand All @@ -55,6 +105,23 @@ export class HeroManager {
return this.getAllHeroes().find((hero) => hero.slug === slug);
}

/**
* Get hero by slug from Directus (async)
* Falls back to static data if Directus fetch fails
*/
public async getHeroBySlugAsync(
slug: HeroSlug,
locale: Locale = 'en',
): Promise<HeroWithGroup | undefined> {
try {
const hero = await fetchHeroBySlug(slug, locale);
if (hero) return hero;
} catch {
// ignore error and fallback
}
return this.getHeroBySlug(slug);
}

public getHeroesBySpecificGroup(group: HeroGroup): HeroWithGroup[] | undefined {
const groupInfo = this.heroGroups[group];
if (!groupInfo) return undefined;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { Locale } from './heroApi';

/** Relation field keys (O2M translations) */
const HERO_TR_KEYS = ['translations'] as const;
const GROUP_TR_KEYS = ['translations'] as const;

/** File field keys */
const HERO_IMG_KEYS = ['srcImg'] as const;
const HERO_GIF_KEYS = ['srcGif'] as const;
const GROUP_IMG_KEYS = ['srcImg'] as const;
const GROUP_LABEL_KEYS = ['label'] as const;

/** Stats relation key */
const STATS_KEYS = ['stats'] as const;

/** Normalized languages_code in Directus */
const languageCode = (locale: Locale): string => locale;

/** Fields requested from Directus */
export const FIELDS = [
'id',
'slug',
'order',
...HERO_IMG_KEYS.flatMap((key) => [`${key}.id`, `${key}.width`, `${key}.height`]),
...HERO_GIF_KEYS.flatMap((key) => [`${key}.id`, `${key}.width`, `${key}.height`]),
...HERO_TR_KEYS.flatMap((key) => [
`${key}.languages_code`,
`${key}.title`,
`${key}.description`,
`${key}.alt`,
`${key}.altGif`,
]),
'rarityClass',
'group.id',
'group.key',
'group.bgColour',
...GROUP_IMG_KEYS.flatMap((key) => [
`group.${key}.id`,
`group.${key}.width`,
`group.${key}.height`,
]),
...GROUP_LABEL_KEYS.flatMap((key) => [
`group.${key}.id`,
`group.${key}.width`,
`group.${key}.height`,
]),
...GROUP_TR_KEYS.flatMap((key) => [
`group.${key}.languages_code`,
`group.${key}.name`,
`group.${key}.description`,
]),
...STATS_KEYS.flatMap((key) => [
`${key}.name`,
`${key}.rarityClass`,
`${key}.defaultLevel`,
`${key}.developmentLevel`,
`${key}.order`,
]),
].join(',');

/**
* Build query parameters for Directus hero API calls
*/
export function buildHeroQueryParams(
locale: Locale,
options?: { slug?: string; limit?: string },
): URLSearchParams {
const params = new URLSearchParams();
if (options?.slug) params.set('filter[slug][_eq]', options.slug);
if (options?.limit) params.set('limit', options.limit);
params.set('fields', FIELDS);
params.set('sort', 'order');
for (const key of HERO_TR_KEYS) {
params.set(`deep[${key}][filter][languages_code][_eq]`, languageCode(locale));
}
for (const key of GROUP_TR_KEYS) {
params.set(`deep[group][${key}][filter][languages_code][_eq]`, languageCode(locale));
}
return params;
}
Loading