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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { render, screen } from '@testing-library/react';
import PRGPage from './PRGPage';

jest.mock('@/shared/i18n', () => ({
useClientTranslation: jest.fn(),
}));

jest.mock('@/shared/lib/hooks/useSizes', () => ({
__esModule: true,
default: () => ({ isMobileSize: false, isTabletSize: false }),
}));

jest.mock('@/shared/ui/CustomSwitch', () => ({
CustomSwitch: ({ elements }: { elements: { children: React.ReactNode }[] }) => (
<div data-testid="custom-switch">
{elements.map((el, i) => (
<span key={i}>{el.children}</span>
))}
</div>
),
CustomSwitchItems: { ToggleItem: 'ToggleItem' },
}));

jest.mock('@/shared/ui/AppLink/AppLink', () => ({
AppLink: ({
children,
to,
isExternal: _isExternal,
...props
}: {
children: React.ReactNode;
to: string;
isExternal?: boolean;
[key: string]: unknown;
}) => (
<a
href={to}
{...props}
>
{children}
</a>
),
}));

jest.mock('@/shared/ui/PageTitle', () => ({
PageTitle: ({ titleText }: { titleText: string }) => <h1>{titleText}</h1>,
}));

jest.mock('@fortawesome/react-fontawesome', () => ({
FontAwesomeIcon: () => <span data-testid="fa-icon" />,
}));

import { useClientTranslation } from '@/shared/i18n';

describe('PRGPage', () => {
beforeEach(() => {
(useClientTranslation as jest.Mock).mockReturnValue({
t: (key: string) => key,
});
});

it('renders all board members', () => {
render(<PRGPage />);

expect(screen.getByText('Helena Pavloff-Pelkonen')).toBeInTheDocument();
expect(screen.getByText('Esa Pavloff-Pelkonen')).toBeInTheDocument();
expect(screen.getByText('Emmi-Irina Pavloff')).toBeInTheDocument();
});

it('renders team link pointing to /team', () => {
render(<PRGPage />);

const teamLink = screen.getByText('alt-zone-team').closest('a');
expect(teamLink).toHaveAttribute('href', '/team');
});
});
249 changes: 152 additions & 97 deletions frontend-next-migration/src/preparedPages/PRGPage/ui/PRGPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,22 @@ 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';
import { classNames } from '@/shared/lib/classNames/classNames';
import { faExternalLink } from '@fortawesome/free-solid-svg-icons';
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';

type PrgT = TFunction<'prg'>;

interface CheckPdfButtonProps {
/** External URL to the PDF document. */
link: string;
/** Translation function for button label. */
t: PrgT;
}

Expand All @@ -38,11 +46,17 @@ const CheckPdfButton = (checkPdfButtonProps: CheckPdfButtonProps) => (
);

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;
/** When true, renders the compact mobile layout. */
isMobileSize: boolean;
}

Expand Down Expand Up @@ -78,124 +92,165 @@ const Boardcard = (props: BoardCardProps) => {
);
};

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];

const tabTranslationKeys: Record<DocumentTab, string> = {
'action-plan': 'action-plan',
'activity-report': 'activity-report',
bylaws: 'bylaws',
};

const tabTextKeys: Record<DocumentTab, string> = {
'action-plan': 'action-plan-text',
'activity-report': 'activity-report-text',
bylaws: 'bylaws-text',
};

const tabLinks: Record<DocumentTab, string> = {
'action-plan': AppExternalLinks.prgActionPlan,
'activity-report': AppExternalLinks.prgActivityReport,
bylaws: AppExternalLinks.prgBylaws,
};

const tabImages: Record<DocumentTab, StaticImageData> = {
'action-plan': actionPlanImg,
'activity-report': activityReportImg,
bylaws: associationRulesImg,
};

const tabImageSide: Record<DocumentTab, 'left' | 'right'> = {
'action-plan': 'right',
'activity-report': 'left',
bylaws: 'right',
};

const PRGPage = () => {
const { t } = useClientTranslation('prg');
const { isMobileSize, isTabletSize } = useSizes();
const [activeTab, setActiveTab] = useState<DocumentTab>('action-plan');

const tabElements: ToggleItem[] = useMemo(
() =>
DOCUMENT_TABS.map((tab) => ({
type: CustomSwitchItems.ToggleItem,
isOpen: activeTab === tab,
onOpen: () => setActiveTab(tab),
children: isMobileSize ? (
/* eslint-disable-next-line @next/next/no-img-element */
<img
src={tabImages[tab].src}
alt={t(tabTranslationKeys[tab])}
className={cls.tabIcon}
/>
) : (
<p>{t(tabTranslationKeys[tab])}</p>
),
})),
[activeTab, t, isMobileSize],
);

return (
<div className={cls.Container}>
<PageTitle
titleText={t('head-title')}
alternate={true}
searchVisible={false}
/>
<div className={cls.titleGap}>
<PageTitle
titleText={t('head-title')}
alternate={true}
searchVisible={false}
/>
</div>
<Image
src={prgDeveloper}
alt="prgDeveloper"
/>
<div className={cls.TextContainer}>
<div className={classNames(cls.TextContainer, undefined, [cls.MarginBottom])}>
<p className={cls.Subheading}>{t('prg')}</p>
<p className={cls.textColumns}>{t('prg-text')}</p>
</div>
<div className={cls.TextContainer}>
<p className={cls.Subheading}>{t('action-plan')}</p>
<p className={cls.textCenter}>{t('action-plan-text')}</p>
<div className={cls.ButtonBlock}>
<CheckPdfButton
link={AppExternalLinks.prgActionPlan}
t={t}
/>
</div>
<p className={cls.Subheading}>{t('activity-report')}</p>
<p className={cls.textCenter}>{t('activity-report-text')}</p>
<div className={cls.ButtonBlock}>
<CheckPdfButton
link={AppExternalLinks.prgActivityReport}
t={t}
/>
</div>
<p className={cls.Subheading}>{t('bylaws')}</p>
<p className={cls.textCenter}>{t('bylaws-text')}</p>
<div className={cls.ButtonBlock}>
<CheckPdfButton
link={AppExternalLinks.prgBylaws}
t={t}
/>
<p className={cls.textCenter}>{t('prg-text')}</p>
<div className={cls.headingWithLines}>
<span className={cls.headingWithLinesText}>{t('prg-board')}</span>
</div>
<div
className={cls.registryInfo}
style={{ whiteSpace: 'pre-line' }}
className={classNames(cls.BoardCardContainer, {
[cls.BoardCardMobileContainer]: isMobileSize || isTabletSize,
})}
>
{t('registry-info')}
</div>
</div>
<div className={classNames(cls.TextContainer, undefined, [cls.MarginBottom])}>
<p className={cls.Subheading}>{t('prg-board')}</p>
{!isMobileSize && !isTabletSize ? (
<div className={cls.BoardCardContainer}>
<Boardcard
picture={Helena}
name={'Helena Pavloff-Pelkonen'}
job={'helena-job'}
profession={'helena-profession'}
t={t}
isMobileSize={isMobileSize}
/>
<Boardcard
picture={Esa}
name={'Esa Pavloff-Pelkonen'}
job={'esa-job'}
profession={'esa-profession'}
t={t}
isMobileSize={isMobileSize}
/>
<Boardcard
picture={Emmi_Irina}
name={'Emmi-Irina Pavloff'}
job={'emmi-irina-job'}
profession={'emmi-irina-profession'}
t={t}
isMobileSize={isMobileSize}
/>
</div>
) : (
<div className={cls.BoardCardMobileContainer}>
{BOARD_MEMBERS.map((member) => (
<Boardcard
picture={Helena}
name={'Helena Pavloff-Pelkonen'}
job={'helena-job'}
profession={'helena-profession'}
key={member.name}
picture={member.picture}
name={member.name}
job={member.job}
profession={member.profession}
t={t}
isMobileSize={isMobileSize || isTabletSize}
/>
<Boardcard
picture={Esa}
name={'Esa Pavloff-Pelkonen'}
job={'esa-job'}
profession={'esa-profession'}
t={t}
isMobileSize={isMobileSize || isTabletSize}
/>
<Boardcard
picture={Emmi_Irina}
name={'Emmi-Irina Pavloff'}
job={'emmi-irina-job'}
profession={'emmi-irina-profession'}
t={t}
isMobileSize={isMobileSize || isTabletSize}
))}
</div>
<div className={cls.ButtonBlock}>
<AppLink
to={'/team'}
className={classNames(cls.pdfButton, undefined, [cls.teamButton])}
aria-label={t('link-to-team-page')}
isExternal={false}
>
<span className={cls.label}>{t('alt-zone-team')}</span>
</AppLink>
</div>
</div>
<CustomSwitch
elements={tabElements}
className={cls.prgTabSwitch}
/>
<div className={classNames(cls.TextContainer, undefined, [cls.tabContentContainer])}>
<div
className={classNames(cls.tabContentLayout, {
[cls.tabContentLayoutReverse]: tabImageSide[activeTab] === 'left',
})}
>
<div className={cls.tabTextArea}>
<p className={cls.Subheading}>{t(tabTranslationKeys[activeTab])}</p>
<p className={cls.textCenter}>{t(tabTextKeys[activeTab])}</p>
<div className={cls.ButtonBlock}>
<CheckPdfButton
link={tabLinks[activeTab]}
t={t}
/>
</div>
</div>
<div className={cls.tabImageArea}>
<Image
src={tabImages[activeTab]}
alt={t(tabTranslationKeys[activeTab])}
className={cls.tabImage}
/>
</div>
)}
<AppLink
to={'/team'}
className={cls.MeetBoard}
aria-label={t('link-to-team-page')}
isExternal={false}
</div>
</div>
<div className={classNames(cls.TextContainer, undefined, [cls.MarginBottom])}>
<p className={cls.Subheading}>{t('registry-title')}</p>
<div
className={cls.registryInfo}
style={{ whiteSpace: 'pre-line' }}
>
<span className={classNames(cls.label, undefined, [cls.bold, cls.Underline])}>
{t('alt-zone-team')}&nbsp;
</span>
</AppLink>
{t('registry-info')}
</div>
</div>
</div>
);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 4 additions & 3 deletions frontend-next-migration/src/shared/i18n/locales/en/prg.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"head-title": "What is PRG",
"prg": "PRG – Psyche's Royale Gaming Association",
"prg-text": "PRG – Psyche’s Royale Gaming Registered Association is a non-profit organization that provides a community platform for inclusive gaming activities and the development of new games. At present, our main goal is to develop the mobile game ALT Zone for educational use – as a non-commercial and participatory project. Our activities can be compared to community theatre: people from diverse backgrounds are involved – some are completing internships as part of their game industry studies, some participate through work trials, and many contribute as volunteer hobby members of PRG. Each participant contributes in their own way, doing exactly what excites and helps them grow. PRG offers an opportunity to develop, learn, and engage in meaningful collaboration in the field of games – no matter where you are in life or in your career.",
"prg-text": "PRG – Psyche's Royale Gaming ry is a non-profit association that provides a community platform for inclusive gaming activities and the development of new games. At present, our main goal is to develop the ALT Zone mobile game for educational use – as a non-commercial and participatory project. Our activities can be compared to community theatre: people from diverse backgrounds are involved – some are completing internships as part of their game industry studies, some participate through work trials, and many contribute as volunteer hobby members of PRG. Each participant contributes in their own way, doing exactly what inspires and develops them. PRG offers an opportunity to grow, learn, and engage in meaningful collaboration in the game industry – regardless of where you are in life or in your career. Our activities have been supported by the City of Pori's cultural unit with an operational grant in 2026.",
"action-plan": "Action plan",
"action-plan-text": "PRG’s Operational Plan outlines our next focus areas: key priorities in game development, growing the community, and collaborating with schools.",
"activity-report": "Annual report",
Expand All @@ -22,5 +22,6 @@
"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-info": "PRG\nPsyche's Royale Gaming ry\nly: 1459866-4\nDUNS: 368247973\nPavloff, Ignatiuksenkatu 12, 28120 Pori\npsykkis@hotmail.com\nproyaleg@gmail.com\np. 0442407396"
}
"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