diff --git a/frontend-next-migration/src/entities/Prg/api/mappers.ts b/frontend-next-migration/src/entities/Prg/api/mappers.ts new file mode 100644 index 000000000..74eadb042 --- /dev/null +++ b/frontend-next-migration/src/entities/Prg/api/mappers.ts @@ -0,0 +1,55 @@ +import type { PrgPage, PrgPageData, PrgBoardMemberDirectus, PrgDocument } from '../types/types'; +import { envHelper } from '@/shared/const/envHelper'; + +/** + * Maps the PRG page data to a more structured format. + * @param prgPage The PRG page data. + * @param prgDocuments The PRG documents. + * @param prgBoardMembers The PRG board members. + * @param lng The language. + * @returns The mapped PRG page data. + */ +export const mapPrgPageData = ( + prgPage?: PrgPage, + prgDocuments?: PrgDocument[], + prgBoardMembers?: PrgBoardMemberDirectus[], + lng: 'fi' | 'en' = 'en', +): PrgPageData => { + const documentsMap: Record = { + action_plan: null, + activity_report: null, + bylaws: null, + }; + + prgDocuments?.forEach((doc) => { + if (doc.key && documentsMap.hasOwnProperty(doc.key)) { + documentsMap[doc.key] = doc.url; + } + }); + + const boardMembers = prgBoardMembers?.map((member) => ({ + name: member.name, + jobTitle: member[`job_title_${lng}`] || member.job_title_en || '', + profession: member[`profession_${lng}`] || member.profession_en || '', + imageUrl: member.image ? normalizedDirectusHost() + '/assets/' + member.image : undefined, + })); + + return { + heroImageUrl: prgPage?.hero_image + ? normalizedDirectusHost() + '/assets/' + prgPage.hero_image + : undefined, + documents: { + actionPlanUrl: documentsMap.action_plan || undefined, + activityReportUrl: documentsMap.activity_report || undefined, + bylawsUrl: documentsMap.bylaws || undefined, + }, + registryInfo: prgPage?.[`registry_info_${lng}`] || prgPage?.registry_info_en || '', + boardMembers: boardMembers && boardMembers.length > 0 ? boardMembers : undefined, + }; +}; + +// remove trailing slash from directusHost if present +const normalizedDirectusHost = () => { + const directusHost = envHelper.directusHost || ''; + return directusHost.endsWith('/') ? directusHost.slice(0, -1) : directusHost; +}; diff --git a/frontend-next-migration/src/entities/Prg/api/prgPageApi.ts b/frontend-next-migration/src/entities/Prg/api/prgPageApi.ts new file mode 100644 index 000000000..05b078348 --- /dev/null +++ b/frontend-next-migration/src/entities/Prg/api/prgPageApi.ts @@ -0,0 +1,81 @@ +import { directusApi } from '@/shared/api'; +import { envHelper } from '@/shared/const/envHelper'; +import { createDirectus, rest, readItems, readSingleton } from '@directus/sdk'; +import { PrgPage, PrgDocument, PrgBoardMemberDirectus, PrgDirectusSchema } from '../types/types'; +import { FetchBaseQueryError } from '@reduxjs/toolkit/dist/query/react'; + +const directusBaseUrl = envHelper.directusHost || ''; +const client = createDirectus(directusBaseUrl).with(rest()); + +const errorHandler = (error: unknown): FetchBaseQueryError => { + const status = + typeof error === 'object' && + error !== null && + 'status' in error && + typeof error.status === 'number' + ? error.status + : 500; + const message = error instanceof Error ? error.message : 'Data fetch failed'; + return { + status, + data: { message: message }, + }; +}; + +/** + * Fetches the PRG page data from Directus, including the PRG page hero image and registroy info, document links, and board members. + * + * @returns An object containing the PRG page data, loading state, and any error that occurred during the fetch. + */ +const prgPageApi = directusApi.injectEndpoints({ + endpoints: (builder) => ({ + getPrgPage: builder.query({ + queryFn: async ( + _arg: void, + ): Promise<{ data: PrgPage } | { error: FetchBaseQueryError }> => { + try { + const prgPage = await client.request( + readSingleton('prg_page', { fields: ['*'] }), + ); + return { data: prgPage }; + } catch (error: unknown) { + return { error: errorHandler(error) }; + } + }, + }), + getPrgDocuments: builder.query({ + queryFn: async (): Promise< + { data: PrgDocument[] } | { error: FetchBaseQueryError } + > => { + try { + const prgDocuments = await client.request( + readItems('prg_documents', { fields: ['*'] }), + ); + return { data: prgDocuments }; + } catch (error) { + return { error: errorHandler(error) }; + } + }, + }), + getPrgBoardMembers: builder.query({ + queryFn: async (): Promise< + { data: PrgBoardMemberDirectus[] } | { error: FetchBaseQueryError } + > => { + try { + const prgBoardMembers = await client.request( + readItems('prg_board_members', { + fields: ['*'], + sort: ['sort', 'id'], + }), + ); + return { data: prgBoardMembers }; + } catch (error) { + return { error: errorHandler(error) }; + } + }, + }), + }), +}); + +export const { useGetPrgPageQuery, useGetPrgDocumentsQuery, useGetPrgBoardMembersQuery } = + prgPageApi; diff --git a/frontend-next-migration/src/entities/Prg/api/useGetPrgPageData.ts b/frontend-next-migration/src/entities/Prg/api/useGetPrgPageData.ts new file mode 100644 index 000000000..264826e77 --- /dev/null +++ b/frontend-next-migration/src/entities/Prg/api/useGetPrgPageData.ts @@ -0,0 +1,41 @@ +import { + useGetPrgPageQuery, + useGetPrgDocumentsQuery, + useGetPrgBoardMembersQuery, +} from './prgPageApi'; +import { mapPrgPageData } from './mappers'; +import { useMemo } from 'react'; + +/** + * Custom hook to fetch and combine PRG page data, documents, and board members. + * + * @param lng - The language code ('fi' or 'en') for localization. Defaults to 'en'. + * @returns An object containing the combined PRG page data, loading state, and any error that occurred during the fetch. + */ +export const useGetPrgPageData = (lng: 'fi' | 'en' = 'en') => { + const { + data: prgPageData, + error: prgPageError, + isLoading: prgPageIsLoading, + } = useGetPrgPageQuery(); + const { + data: prgDocumentsData, + error: prgDocumentsError, + isLoading: prgDocumentsIsLoading, + } = useGetPrgDocumentsQuery(); + const { + data: prgBoardMembersData, + error: prgBoardMembersError, + isLoading: prgBoardMembersIsLoading, + } = useGetPrgBoardMembersQuery(); + + const fullPrgPageData = useMemo(() => { + return mapPrgPageData(prgPageData, prgDocumentsData, prgBoardMembersData, lng); + }, [prgPageData, prgDocumentsData, prgBoardMembersData, lng]); + + return { + fullPrgPageData, + loading: { prgPageIsLoading, prgDocumentsIsLoading, prgBoardMembersIsLoading }, + error: { prgPageError, prgDocumentsError, prgBoardMembersError }, + }; +}; diff --git a/frontend-next-migration/src/entities/Prg/index.ts b/frontend-next-migration/src/entities/Prg/index.ts new file mode 100644 index 000000000..e6c202f3a --- /dev/null +++ b/frontend-next-migration/src/entities/Prg/index.ts @@ -0,0 +1,2 @@ +export { useGetPrgPageData } from './api/useGetPrgPageData'; +export type { PrgPageData, PrgBoardMember } from './types/types'; diff --git a/frontend-next-migration/src/entities/Prg/types/types.ts b/frontend-next-migration/src/entities/Prg/types/types.ts new file mode 100644 index 000000000..88c511a40 --- /dev/null +++ b/frontend-next-migration/src/entities/Prg/types/types.ts @@ -0,0 +1,57 @@ +// Directus data +export interface PrgPage { + id: number; + date_created: string; + date_updated: string | null; + status?: 'draft' | 'published' | 'archived' | null; + hero_image: string | null; + registry_info_fi: string | null; + registry_info_en: string | null; +} + +export interface PrgDocument { + key: string | null; // e.g. action_plan, activity_report, bylaws + date_created: string; + date_updated: string | null; + status?: 'draft' | 'published' | 'archived' | null; + url: string | null; +} + +export interface PrgBoardMemberDirectus { + id: number; + date_created: string; + date_updated: string | null; + status?: 'draft' | 'published' | 'archived' | null; + name: string; + job_title_fi: string | null; + job_title_en: string | null; + profession_fi: string | null; + profession_en: string | null; + image: string | null; + sort: number | null; +} + +export interface PrgDirectusSchema { + prg_page: PrgPage; + prg_documents: PrgDocument[]; + prg_board_members: PrgBoardMemberDirectus[]; +} + +// Mapped data, localized +export interface PrgBoardMember { + name: string; + jobTitle: string; + profession: string; + imageUrl?: string; +} + +export interface PrgPageData { + heroImageUrl?: string; + documents: { + actionPlanUrl?: string; + activityReportUrl?: string; + bylawsUrl?: string; + }; + registryInfo?: string; + boardMembers?: PrgBoardMember[]; +} diff --git a/frontend-next-migration/src/preparedPages/PRGPage/ui/PRGPage.test.tsx b/frontend-next-migration/src/preparedPages/PRGPage/ui/PRGPage.test.tsx index 7e3af154a..2a4200b62 100644 --- a/frontend-next-migration/src/preparedPages/PRGPage/ui/PRGPage.test.tsx +++ b/frontend-next-migration/src/preparedPages/PRGPage/ui/PRGPage.test.tsx @@ -1,5 +1,7 @@ import { render, screen } from '@testing-library/react'; import PRGPage from './PRGPage'; +import { useParams } from 'next/navigation'; +import { useGetPrgPageData } from '@/entities/Prg'; jest.mock('@/shared/i18n', () => ({ useClientTranslation: jest.fn(), @@ -21,6 +23,14 @@ jest.mock('@/shared/ui/CustomSwitch', () => ({ CustomSwitchItems: { ToggleItem: 'ToggleItem' }, })); +jest.mock('next/navigation', () => ({ + useParams: jest.fn(), +})); + +jest.mock('@/entities/Prg', () => ({ + useGetPrgPageData: jest.fn(), +})); + jest.mock('@/shared/ui/AppLink/AppLink', () => ({ AppLink: ({ children, @@ -55,6 +65,43 @@ import useSizes from '@/shared/lib/hooks/useSizes'; describe('PRGPage', () => { beforeEach(() => { + (useParams as jest.Mock).mockReturnValue({ lng: 'en' }); + + (useGetPrgPageData as jest.Mock).mockReturnValue({ + fullPrgPageData: { + heroImageUrl: undefined, + documents: { + actionPlanUrl: 'https://example.com/action-plan.pdf', + activityReportUrl: 'https://example.com/activity-report.pdf', + bylawsUrl: 'https://example.com/bylaws.pdf', + }, + registryInfo: 'Test registry information', + boardMembers: [ + { + name: 'Helena Pavloff-Pelkonen', + jobTitle: 'Chairperson', + profession: 'Profession', + }, + { + name: 'Esa Pavloff-Pelkonen', + jobTitle: 'Member', + profession: 'Profession', + }, + { + name: 'Emmi-Irina Pavloff', + jobTitle: 'Member', + profession: 'Profession', + }, + ], + }, + loading: { + prgPageIsLoading: false, + prgDocumentsIsLoading: false, + prgBoardMembersIsLoading: false, + }, + error: { prgPageError: null, prgDocumentsError: null, prgBoardMembersError: null }, + }); + (useClientTranslation as jest.Mock).mockReturnValue({ t: (key: string) => key, }); diff --git a/frontend-next-migration/src/preparedPages/PRGPage/ui/PRGPage.tsx b/frontend-next-migration/src/preparedPages/PRGPage/ui/PRGPage.tsx index f393c68e0..73f52690f 100644 --- a/frontend-next-migration/src/preparedPages/PRGPage/ui/PRGPage.tsx +++ b/frontend-next-migration/src/preparedPages/PRGPage/ui/PRGPage.tsx @@ -3,14 +3,9 @@ import cls from './PRGPage.module.scss'; import { PageTitle } from '@/shared/ui/PageTitle'; import type { TFunction } from 'i18next'; import { useClientTranslation } from '@/shared/i18n'; -import prgDeveloper from '@/shared/assets/images/prgDeveloper.png'; import Image, { StaticImageData } from 'next/image'; import { AppLink } from '@/shared/ui/AppLink/AppLink'; import useSizes from '@/shared/lib/hooks/useSizes'; -import Helena from '@/shared/assets/images/board/helena.png'; -import Esa from '@/shared/assets/images/board/esa.png'; -import Emmi_Irina from '@/shared/assets/images/board/emmi-irina.png'; -import { AppExternalLinks } from '@/shared/appLinks/appExternalLinks'; import actionPlanImg from '@/shared/assets/images/PRGPage/actionplan.png'; import activityReportImg from '@/shared/assets/images/PRGPage/annualreport.png'; import associationRulesImg from '@/shared/assets/images/PRGPage/associationrules.png'; @@ -20,6 +15,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useState, useMemo } from 'react'; import { CustomSwitch, CustomSwitchItems } from '@/shared/ui/CustomSwitch'; import type { ToggleItem } from '@/shared/ui/CustomSwitch'; +import { useParams } from 'next/navigation'; +import { useGetPrgPageData, type PrgBoardMember } from '@/entities/Prg'; type PrgT = TFunction<'prg'>; @@ -30,84 +27,73 @@ interface CheckPdfButtonProps { t: PrgT; } -const CheckPdfButton = (checkPdfButtonProps: CheckPdfButtonProps) => ( - - {checkPdfButtonProps.t('check-pdf')} - - -); +const CheckPdfButton = (checkPdfButtonProps: CheckPdfButtonProps) => { + if (!checkPdfButtonProps.link) { + return null; + } + return ( + + {checkPdfButtonProps.t('check-pdf')} + + + ); +}; interface BoardCardProps { /** Board member portrait image. */ - picture: StaticImageData; - /** Full name of the board member. */ - name: string; - /** Translation key for the member's job title. */ - job: string; - /** Translation key for the member's profession. */ - profession: string; - /** Translation function for resolving job/profession labels. */ - t: PrgT; + boardMember: PrgBoardMember; /** When true, renders the compact mobile layout. */ isMobileSize: boolean; } const Boardcard = (props: BoardCardProps) => { - const { picture, name, job, profession, t, isMobileSize } = props; + const { boardMember, isMobileSize } = props; if (!isMobileSize) { return (
- {name} + {boardMember.imageUrl && ( + {boardMember.name} + )}
-

{name}

-

{t(job)}

-

{t(profession)}

+

{boardMember.name}

+

{boardMember.jobTitle}

+

{boardMember.profession}

); } return (
- {name} + {boardMember.imageUrl && ( + {boardMember.name} + )}
-

{name}

-

{t(job)}

-

{t(profession)}

+

{boardMember.name}

+

{boardMember.jobTitle}

+

{boardMember.profession}

); }; -const BOARD_MEMBERS = [ - { - picture: Helena, - name: 'Helena Pavloff-Pelkonen', - job: 'helena-job', - profession: 'helena-profession', - }, - { picture: Esa, name: 'Esa Pavloff-Pelkonen', job: 'esa-job', profession: 'esa-profession' }, - { - picture: Emmi_Irina, - name: 'Emmi-Irina Pavloff', - job: 'emmi-irina-job', - profession: 'emmi-irina-profession', - }, -] as const; - const DOCUMENT_TABS = ['action-plan', 'activity-report', 'bylaws'] as const; type DocumentTab = (typeof DOCUMENT_TABS)[number]; @@ -124,12 +110,6 @@ const tabTextKeys: Record = { bylaws: 'bylaws-text', }; -const tabLinks: Record = { - 'action-plan': AppExternalLinks.prgActionPlan, - 'activity-report': AppExternalLinks.prgActivityReport, - bylaws: AppExternalLinks.prgBylaws, -}; - const tabImages: Record = { 'action-plan': actionPlanImg, 'activity-report': activityReportImg, @@ -142,10 +122,45 @@ const tabImageSide: Record = { bylaws: 'right', }; +const BoardMembers = (props: { + boardMembers?: PrgBoardMember[]; + isMobileSize: boolean; + loading: boolean; + error: boolean; + t: PrgT; +}) => { + const { boardMembers, isMobileSize, loading, error, t } = props; + if (loading) return

{t('loading')}

; + if (error) return

{t('load-error')}

; + return ( +
+ {boardMembers?.map((member) => ( + + ))} +
+ ); +}; + const PRGPage = () => { const { t } = useClientTranslation('prg'); const { isMobileSize, isTabletSize } = useSizes(); const [activeTab, setActiveTab] = useState('action-plan'); + const params = useParams(); + const lng = params.lng; + const { + fullPrgPageData: prgPageData, + loading, + error, + } = useGetPrgPageData(lng === 'fi' ? 'fi' : 'en'); + const isSmallScreen = isMobileSize || isTabletSize; const tabElements: ToggleItem[] = useMemo( () => @@ -157,6 +172,11 @@ const PRGPage = () => { })), [activeTab, t], ); + const tabLinks: Record = { + 'action-plan': prgPageData?.documents?.actionPlanUrl || '', + 'activity-report': prgPageData?.documents?.activityReportUrl || '', + bylaws: prgPageData?.documents?.bylawsUrl || '', + }; const renderTabSwitch = () => ( <> @@ -226,33 +246,27 @@ const PRGPage = () => { searchVisible={false} /> - {t('head-title')} + {prgPageData.heroImageUrl && ( + {t('head-title')} + )}

{t('prg')}

{t('prg-text')}

{t('prg-board')}
-
- {BOARD_MEMBERS.map((member) => ( - - ))} -
+
{
- {isMobileSize || isTabletSize ? renderMobileTabs() : renderTabSwitch()} + {isSmallScreen ? renderMobileTabs() : renderTabSwitch()}

{t('registry-title')}

-
{t('registry-info')}
+
+ {loading.prgPageIsLoading + ? t('loading') + : error.prgPageError + ? t('load-error') + : prgPageData.registryInfo} +
); diff --git a/frontend-next-migration/src/shared/appLinks/appExternalLinks.ts b/frontend-next-migration/src/shared/appLinks/appExternalLinks.ts index 8749ea0af..1e9b7d146 100644 --- a/frontend-next-migration/src/shared/appLinks/appExternalLinks.ts +++ b/frontend-next-migration/src/shared/appLinks/appExternalLinks.ts @@ -25,10 +25,4 @@ export const AppExternalLinks = { join: 'https://discord.gg/mgjQkCR2Fg', dlpackage: 'https://drive.google.com/file/d/16MJzw25RAzV0uH6m9VzReYGFCExK23Uh/view', pastversions: 'https://drive.google.com/drive/folders/1n5GVJVXFF2F1sF5-C07uYkFNd9Bsdxzu', - prgActivityReport: - 'https://drive.google.com/file/d/1UkBPyRLEcbWwbUy37tmHCMcr5AYzbZHe/view?usp=drive_link', - prgBylaws: - 'https://drive.google.com/file/d/1_ek4eGfbxbCYLliY53gLIs0Gv2pwRkhw/view?usp=drive_link', - prgActionPlan: - 'https://drive.google.com/file/d/1fu_iab151a2sNvaSp_U8-zKJKdvVhnhz/view?usp=drive_link', } as const; diff --git a/frontend-next-migration/src/shared/assets/images/altLogoOld.png b/frontend-next-migration/src/shared/assets/images/altLogoOld.png deleted file mode 100644 index 388432320..000000000 Binary files a/frontend-next-migration/src/shared/assets/images/altLogoOld.png and /dev/null differ diff --git a/frontend-next-migration/src/shared/assets/images/altLogoold.png b/frontend-next-migration/src/shared/assets/images/altLogoold.png deleted file mode 100644 index cc4540b40..000000000 Binary files a/frontend-next-migration/src/shared/assets/images/altLogoold.png and /dev/null differ diff --git a/frontend-next-migration/src/shared/assets/images/board/emmi-irina.png b/frontend-next-migration/src/shared/assets/images/board/emmi-irina.png deleted file mode 100644 index 56898e4c1..000000000 Binary files a/frontend-next-migration/src/shared/assets/images/board/emmi-irina.png and /dev/null differ diff --git a/frontend-next-migration/src/shared/assets/images/board/esa.png b/frontend-next-migration/src/shared/assets/images/board/esa.png deleted file mode 100644 index 5ad61bc01..000000000 Binary files a/frontend-next-migration/src/shared/assets/images/board/esa.png and /dev/null differ diff --git a/frontend-next-migration/src/shared/assets/images/board/helena.png b/frontend-next-migration/src/shared/assets/images/board/helena.png deleted file mode 100644 index b0e0a9a02..000000000 Binary files a/frontend-next-migration/src/shared/assets/images/board/helena.png and /dev/null differ diff --git a/frontend-next-migration/src/shared/assets/images/prgDeveloper.png b/frontend-next-migration/src/shared/assets/images/prgDeveloper.png deleted file mode 100644 index f20988870..000000000 Binary files a/frontend-next-migration/src/shared/assets/images/prgDeveloper.png and /dev/null differ diff --git a/frontend-next-migration/src/shared/i18n/locales/en/prg.json b/frontend-next-migration/src/shared/i18n/locales/en/prg.json index a40b9820e..36c6be011 100644 --- a/frontend-next-migration/src/shared/i18n/locales/en/prg.json +++ b/frontend-next-migration/src/shared/i18n/locales/en/prg.json @@ -10,15 +10,10 @@ "bylaws-text": "PRG operates as a registered association. Below you can find the official Articles of Association, which describe the organization’s purpose, structure, and operating principles.", "check-pdf": "Check PDF", "prg-board": "PRG board", - "helena-job": "Executive Director, Chairperson", - "helena-profession": "Drama Educator, Vocational Teacher, Youth Worker, Solution-Focused Work Supervisor, Poster Artist", - "esa-job": "Board Member, Vice Chairperson", - "esa-profession": "Master of Adult Education, Doctoral Researcher in Game Arts", - "emmi-irina-job": "Board Member", - "emmi-irina-profession": "Youth Worker", "alt-zone-team": "ALT Zone Team here", "open-pdf": "Open the PDF in a new tab", "link-to-team-page": "Open the ALT Zone team page in a new tab.", "registry-title": "Company info", - "registry-info": "Psyche's Royale Gaming ry, PRG\n\nPavloff, Ignatiuksenkatu 12, 28120 Pori\n\nly: 1459866-4\n\nDUNS: 368247973" + "loading": "Loading...", + "load-error": "Error loading data." } diff --git a/frontend-next-migration/src/shared/i18n/locales/fi/prg.json b/frontend-next-migration/src/shared/i18n/locales/fi/prg.json index 8b294f7c4..22acc70d8 100644 --- a/frontend-next-migration/src/shared/i18n/locales/fi/prg.json +++ b/frontend-next-migration/src/shared/i18n/locales/fi/prg.json @@ -10,14 +10,10 @@ "bylaws-text": "PRG toimii rekisteröitynä yhdistyksenä. Alta löydät viralliset yhdistyksen säännöt, joissa kuvataan yhdistyksen tarkoitus, rakenne ja toimintaperiaatteet.", "check-pdf": "Katso PDF", "prg-board": "PRG hallitus", - "helena-job": "Toiminnanjohtaja, puheenjohtaja", - "helena-profession": "Teatteri-ilmaisun ohjaaja, ammatillinen opettaja, nuoriso-ohjaaja, ratkaisukeskeinen työnohjaaja, julistetaiteilija", - "esa-job": "Hallituksen jäsen, varapuheenjohtaja", - "esa-profession": "Aikuiskasvatustieteen maisteri, pelitaiteen väitöskirjatutkija", - "emmi-irina-job": "Hallituksen jäsen", - "emmi-irina-profession": "Nuoriso-ohjaaja", "alt-zone-team": "ALT Zone -tiimi täällä", "open-pdf": "Avaa PDF:n uudessa välilehdessä", "link-to-team-page": "Avaa ALT Zone tiimi - sivun uudessa välilehdessä.", "registry-title": "Yritystiedot", - "registry-info": "Psyche's Royale Gaming ry, PRG\n\nPavloff, Ignatiuksenkatu 12, 28120 Pori\n\nly: 1459866-4\n\nDUNS: 368247973"} \ No newline at end of file + "loading": "Ladataan...", + "load-error": "Virhe ladattaessa tietoja." +} \ No newline at end of file