diff --git a/frontend-next-migration/src/preparedPages/PRGPage/ui/PRGPage.module.scss b/frontend-next-migration/src/preparedPages/PRGPage/ui/PRGPage.module.scss index aa1053ad2..54b25e5ed 100644 --- a/frontend-next-migration/src/preparedPages/PRGPage/ui/PRGPage.module.scss +++ b/frontend-next-migration/src/preparedPages/PRGPage/ui/PRGPage.module.scss @@ -1,6 +1,42 @@ +%flexColumn { + display: flex; + flex-direction: column; +} + +%headingBase { + font-family: var(--font-family-title); + font-size: clamp(1.5rem, 4vw, var(--font-size-5xl)); + line-height: var(--font-line-5xl); + color: var(--primary-color); + text-align: center; + padding-inline: 1rem; + + @media (max-width: breakpoint(sm)) { + font-size: 1.5rem; + } +} + +%boardCardBase { + @extend %flexColumn; + border: 2px solid var(--primary-color); +} + +%boardInfoBase { + @extend %flexColumn; + text-align: center; + height: 100%; +} + +@mixin jobShared { + font-size: 1.2rem; + color: var(--primary-color); + padding-top: .2rem; + padding-bottom: 1rem; +} + .Container { margin-inline: auto; - padding-inline: 16px; + padding-inline: var(--spacer300); font: var(--font-dm-m) !important; @media (min-width: breakpoint(md)) { @@ -8,9 +44,22 @@ } } -.HeaderLine { - display: flex; - justify-content: space-between; +.titleGap { + margin-bottom: 3rem; + text-align: left; + width: 100%; +} + +.titleGap h1 { + font-size: clamp(2rem, 6vw, 128px) !important; + text-align: left; +} + +.titleGap > div { + max-width: none !important; + margin: 0 !important; + padding: 0 !important; + width: 100%; } .TextContainer { @@ -23,44 +72,154 @@ } .Subheading { - font: var(--font-sw-xl); + @extend %headingBase; + padding: 1rem; +} + +.prgTabSwitch.prgTabSwitch { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 0 8px; + gap: 5px; + height: 80px; + background: var(--background); + border: 2px solid var(--black); + box-shadow: 2px 4px 0px var(--black); + border-radius: 35px; +} + +.prgTabSwitch [class*="ToggleItem"] { + flex: 1; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + height: 65px; + border-radius: 35px; + font: 500 var(--font-dm-xl); + letter-spacing: -0.04em; color: var(--primary-color); - text-align: center; - padding-top: 1rem; - padding-bottom: 1rem; + border: none; + cursor: pointer; + transition: background 0.15s, color 0.15s; + padding: 0 4px; + min-width: 0; - @media (max-width: #{breakpoint(sm)}) { - font-size: 1.5rem; + &:hover { + height: 70px; } } -.textColumns { - box-sizing: border-box; - padding-inline: 32px; - column-count: 1; - column-gap: 24px; - column-fill: balance; - hyphens: auto; - overflow-wrap: break-word; - padding-bottom: 1em; +.prgTabSwitch [class*="ToggleItem"] > * { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; } -@media (min-width: #{breakpoint(md)}) { - .textColumns { - column-count: 2; - } +.prgTabSwitch > * > *, +.prgTabSwitch > * > * > * { + padding: 0; + margin: 0; +} + +.prgTabSwitch [class*="OpenToggleItem"] { + background: var(--primary-color); + border: 2px solid var(--black); + height: 70px; + color: var(--black); +} + +.tabContentLayoutReverse { + flex-direction: row-reverse; } .textCenter { text-align: center; padding-inline: 2em; + font: var(--font-dm-l); +} + +.headingWithLines { + position: relative; + text-align: center; + padding-block: 1rem; + margin-inline: -1rem; +} + +.headingWithLines::before, +.headingWithLines::after { + content: ''; + position: absolute; + top: 50%; + height: 2px; + background: var(--primary-color); + transform: translateY(-50%); +} + +.headingWithLines::before { + left: 0; + width: 15%; +} + +.headingWithLines::after { + right: 0; + width: 15%; +} + +.headingWithLinesText { + @extend %headingBase; +} + +.tabIcon { + height: 100%; + max-width: 100%; + object-fit: contain; +} + +.tabContentContainer { + margin-top: 0.5rem; + min-height: 359px; + display: flex; + align-items: center; + justify-content: center; +} + +.tabContentLayout { + display: flex; + gap: 2rem; + align-items: center; + justify-content: center; + flex-wrap: wrap; +} + +.tabTextArea { + flex: 1; + min-width: 0; + overflow-wrap: break-word; +} + +.tabImageArea { + flex-shrink: 0; + width: 30%; +} + +.tabImage { + width: 100%; + height: auto; + max-height: 200px; + object-fit: contain; + display: block; } .pdfButton { display: inline-flex; align-items: center; + justify-content: center; gap: 10px; - + width: 304px; padding: 6px 22px !important; border: 2px solid var(--black, #000); border-radius: 2em; @@ -87,8 +246,7 @@ } .label { - font: var(--font-dm-m, inherit); - line-height: var(--font-line-m, 1.3); + font: var(--font-dm-l); white-space: nowrap; color: var(--primary-color, #000); } @@ -99,12 +257,6 @@ height: 15px; } -.icon { - display: inline-block; - width: 15px; - height: 15px; -} - .ButtonBlock { display: flex; justify-content: center; @@ -113,33 +265,29 @@ } .BoardCard { - display: flex; - flex-direction: column; - border: 2px solid var(--primary-color); + @extend %boardCardBase; border-radius: .8em; width: calc(33% - 1.333rem); } .BoardCardInfoArea { + @extend %boardInfoBase; outline: 2px solid var(--primary-color); border-radius: .8em; - text-align: center; - height: 100%; + min-width: 0; } .BoardCardInfoAreaMobile { + @extend %boardInfoBase; outline: 1px solid var(--primary-color); border-bottom-right-radius: .6em; border-bottom-left-radius: .6em; - text-align: center; - height: 100%; width: 100%; + min-width: 0; } .BoardCardMobile { - display: flex; - flex-direction: column; - border: 2px solid var(--primary-color); + @extend %boardCardBase; border-radius: .6em; width: 100%; align-items: center; @@ -153,15 +301,21 @@ .Job { font: var(--font-sw-m); - font-size: 1.2rem; - color: var(--primary-color); - padding-top: .2rem; + @include jobShared; } -.Profession { - padding-top: .2rem; +.Profession, +.ProfessionMobile { + flex: 1; + display: flex; + align-items: center; + justify-content: center; padding-bottom: .2rem; - + text-align: center; + font: var(--font-dm-l); + color: var(--neutral); + overflow-wrap: break-word; + word-break: break-word; } .NameMobile { @@ -172,15 +326,7 @@ .JobMobile { font: var(--font-sw-s); - font-size: 1.2rem; - color: var(--primary-color); - padding-top: .2rem; -} - -.ProfessionMobile { - padding-top: .2rem; - padding-bottom: .2rem; - font-size: .8rem; + @include jobShared; } .BoardCardContainer { @@ -192,57 +338,105 @@ padding-inline: 2rem; } -.BoardCardHeader { - display: flex; - justify-content: space-between; - align-items: center; -} - -.MeetBoard { - padding-top: 1rem !important; - color: var(--primary-color) !important; - text-align: center; - display: block; - width: 100%; -} - -.SeeBoard { - padding-top: 2rem; -} - .BoardCardMobileContainer { - display: flex; - flex-direction: column; + @extend %flexColumn; gap: 1rem; width: 100%; } -.Underline { - &:hover { - text-decoration: underline !important; - text-underline-offset: 2px; - text-decoration-thickness: 1.5px; - } -} - .MarginBottom { margin-bottom: 2rem; } -.bold { - font-weight: bold; +.teamButton { + background: var(--primary-color); + color: var(--black); + + .label { + color: var(--black); + } } .registryInfo { - display: flex; - flex-direction: column; - text-align: left; - margin-top: 2rem; - margin-left: auto; - margin-right: auto; - width: fit-content; - padding: 1rem; - border: 2px solid var(--primary-color); - border-radius: .8em; + text-align: center; white-space: pre-line; -} \ No newline at end of file + font: var(--font-dm-l); + font-size: 30px; + margin-top: 1rem; +} + +@media (max-width: breakpoint(sm)) { + .prgTabSwitch { + height: auto; + padding: 4px 8px; + gap: 3px; + } + + .prgTabSwitch [class*="ToggleItem"] { + height: 50px; + font: 500 var(--font-dm-m); + } + + .prgTabSwitch [class*="ToggleItem"]:hover { + height: 54px; + } + + .prgTabSwitch [class*="OpenToggleItem"] { + height: 54px; + } + + .tabContentLayout { + flex-direction: column; + gap: 1rem; + } + + .tabContentLayoutReverse { + flex-direction: column; + } + + .tabTextArea { + display: contents; + } + + .tabTextArea > *:nth-child(1) { + order: 0; + } + + .tabTextArea > *:nth-child(2) { + order: 1; + } + + .tabTextArea > *:nth-child(3) { + order: 3; + } + + .tabImageArea { + width: 100%; + order: 2; + } + + .tabContentContainer .Subheading { + padding: 0; + } + + .tabContentContainer { + min-height: auto; + } + + .textCenter { + padding-inline: 0.5rem; + font: var(--font-dm-m); + } + + .registryInfo { + font: var(--font-dm-m); + } + + .TextContainer { + padding: 0.5rem; + } + + .headingWithLines { + margin-inline: -0.5rem; + } +} diff --git a/frontend-next-migration/src/preparedPages/PRGPage/ui/PRGPage.test.tsx b/frontend-next-migration/src/preparedPages/PRGPage/ui/PRGPage.test.tsx new file mode 100644 index 000000000..92df96720 --- /dev/null +++ b/frontend-next-migration/src/preparedPages/PRGPage/ui/PRGPage.test.tsx @@ -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 }[] }) => ( +
+ {elements.map((el, i) => ( + {el.children} + ))} +
+ ), + 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; + }) => ( + + {children} + + ), +})); + +jest.mock('@/shared/ui/PageTitle', () => ({ + PageTitle: ({ titleText }: { titleText: string }) =>

{titleText}

, +})); + +jest.mock('@fortawesome/react-fontawesome', () => ({ + FontAwesomeIcon: () => , +})); + +import { useClientTranslation } from '@/shared/i18n'; + +describe('PRGPage', () => { + beforeEach(() => { + (useClientTranslation as jest.Mock).mockReturnValue({ + t: (key: string) => key, + }); + }); + + it('renders all board members', () => { + render(); + + 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(); + + const teamLink = screen.getByText('alt-zone-team').closest('a'); + expect(teamLink).toHaveAttribute('href', '/team'); + }); +}); diff --git a/frontend-next-migration/src/preparedPages/PRGPage/ui/PRGPage.tsx b/frontend-next-migration/src/preparedPages/PRGPage/ui/PRGPage.tsx index c91800ad9..62d4acc07 100644 --- a/frontend-next-migration/src/preparedPages/PRGPage/ui/PRGPage.tsx +++ b/frontend-next-migration/src/preparedPages/PRGPage/ui/PRGPage.tsx @@ -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; } @@ -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; } @@ -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 = { + 'action-plan': 'action-plan', + 'activity-report': 'activity-report', + bylaws: 'bylaws', +}; + +const tabTextKeys: Record = { + 'action-plan': 'action-plan-text', + 'activity-report': 'activity-report-text', + 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, + bylaws: associationRulesImg, +}; + +const tabImageSide: Record = { + 'action-plan': 'right', + 'activity-report': 'left', + bylaws: 'right', +}; + const PRGPage = () => { const { t } = useClientTranslation('prg'); const { isMobileSize, isTabletSize } = useSizes(); + const [activeTab, setActiveTab] = useState('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 */ + {t(tabTranslationKeys[tab])} + ) : ( +

{t(tabTranslationKeys[tab])}

+ ), + })), + [activeTab, t, isMobileSize], + ); return (
- +
+ +
prgDeveloper -
+

{t('prg')}

-

{t('prg-text')}

-
-
-

{t('action-plan')}

-

{t('action-plan-text')}

-
- -
-

{t('activity-report')}

-

{t('activity-report-text')}

-
- -
-

{t('bylaws')}

-

{t('bylaws-text')}

-
- +

{t('prg-text')}

+
+ {t('prg-board')}
- {t('registry-info')} -
-
-
-

{t('prg-board')}

- {!isMobileSize && !isTabletSize ? ( -
- - - -
- ) : ( -
+ {BOARD_MEMBERS.map((member) => ( - - +
+ + {t('alt-zone-team')} + +
+
+ +
+
+
+

{t(tabTranslationKeys[activeTab])}

+

{t(tabTextKeys[activeTab])}

+
+ +
+
+
+ {t(tabTranslationKeys[activeTab])}
- )} - +
+
+

{t('registry-title')}

+
- - {t('alt-zone-team')}  - - + {t('registry-info')} +
); diff --git a/frontend-next-migration/src/shared/assets/images/PRGPage/actionplan.png b/frontend-next-migration/src/shared/assets/images/PRGPage/actionplan.png new file mode 100644 index 000000000..5cb53d956 Binary files /dev/null and b/frontend-next-migration/src/shared/assets/images/PRGPage/actionplan.png differ diff --git a/frontend-next-migration/src/shared/assets/images/PRGPage/annualreport.png b/frontend-next-migration/src/shared/assets/images/PRGPage/annualreport.png new file mode 100644 index 000000000..dc51a2097 Binary files /dev/null and b/frontend-next-migration/src/shared/assets/images/PRGPage/annualreport.png differ diff --git a/frontend-next-migration/src/shared/assets/images/PRGPage/associationrules.png b/frontend-next-migration/src/shared/assets/images/PRGPage/associationrules.png new file mode 100644 index 000000000..4e01ec2fe Binary files /dev/null and b/frontend-next-migration/src/shared/assets/images/PRGPage/associationrules.png 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 85023980f..a5bf3c400 100644 --- a/frontend-next-migration/src/shared/i18n/locales/en/prg.json +++ b/frontend-next-migration/src/shared/i18n/locales/en/prg.json @@ -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", @@ -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" -} \ No newline at end of file + "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" +} 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 ec0cf70d3..f31a6bc40 100644 --- a/frontend-next-migration/src/shared/i18n/locales/fi/prg.json +++ b/frontend-next-migration/src/shared/i18n/locales/fi/prg.json @@ -1,7 +1,7 @@ { "head-title": "Mikä on PRG", "prg": "PRG -Psyche's Royale Gaming RY", - "prg-text": "PRG – Psyche’s Royale Gaming ry on voittoa tavoittelematon yhdistys, joka tarjoaa yhteisöllisen alustan osallistavaan pelitoimintaan ja uusien pelien kehittämiseen. Tällä hetkellä päätavoitteenamme on kehittää ALT Zone -mobiilipeliä koulukäyttöön – ei-kaupallisena ja osallistavana projektina. Toimintaamme voi verrata harrastajateatteriin: mukana on ihmisiä erilaisista taustoista – \nosa suorittaa työharjo ittelua pelialan opintojen yhteydessä, osa osallistuu työkokeilun kautta, ja monet toimivat vapaaehtoisina PRG:n harrastajajäseninä. Jokainen osallistuu omalla tavallaan ja tekee juuri sitä, mikä innostaa ja kehittää. PRG tarjoaa mahdollisuuden kasvaa, oppia ja tehdä merkityksellistä yhteistyötä pelialalla – riippumatta siitä, missä vaiheessa elämää tai uraa olet.", + "prg-text": "PRG – Psyche’s Royale Gaming ry on voittoa tavoittelematon yhdistys, joka tarjoaa yhteisöllisen alustan osallistavaan pelitoimintaan ja uusien pelien kehittämiseen. Tällä hetkellä päätavoitteenamme on kehittää ALT Zone -mobiilipeliä koulukäyttöön – ei-kaupallisena ja osallistavana projektina. Toimintaamme voi verrata harrastajateatteriin: mukana on ihmisiä erilaisista taustoista – osa suorittaa työharjoittelua pelialan opintojen yhteydessä, osa osallistuu työkokeilun kautta, ja monet toimivat vapaaehtoisina PRG:n harrastajajäseninä. Jokainen osallistuu omalla tavallaan ja tekee juuri sitä, mikä innostaa ja kehittää. PRG tarjoaa mahdollisuuden kasvaa, oppia ja tehdä merkityksellistä yhteistyötä pelialalla – riippumatta siitä, missä vaiheessa elämää tai uraa olet. Toimintaa on tuettu Porin kaupungin kulttuuriyksikön toiminta-avustuksella 2026.", "action-plan": "Toimintasuunnitelma", "action-plan-text": "PRG:n toimintasuunnitelma kertoo, mihin keskitymme seuraavaksi: pelikehityksen painopisteet, yhteisön kasvattaminen ja yhteistyöt koulujen kanssa.", "activity-report": "Toimintakertomus", @@ -22,4 +22,5 @@ "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-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"} \ No newline at end of file + "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