diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/PublicController.java b/backend/vkt/src/main/java/fi/oph/vkt/api/PublicController.java index 88e7c2476..4747e829c 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/PublicController.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/PublicController.java @@ -8,6 +8,7 @@ import fi.oph.vkt.api.dto.PublicEnrollmentDTO; import fi.oph.vkt.api.dto.PublicEnrollmentInitialisationDTO; import fi.oph.vkt.api.dto.PublicExamEventDTO; +import fi.oph.vkt.api.dto.PublicExaminerDTO; import fi.oph.vkt.api.dto.PublicPersonDTO; import fi.oph.vkt.api.dto.PublicReservationDTO; import fi.oph.vkt.model.Enrollment; @@ -24,6 +25,7 @@ import fi.oph.vkt.service.PublicEnrollmentAppointmentService; import fi.oph.vkt.service.PublicEnrollmentService; import fi.oph.vkt.service.PublicExamEventService; +import fi.oph.vkt.service.PublicExaminerService; import fi.oph.vkt.service.PublicPersonService; import fi.oph.vkt.service.PublicReservationService; import fi.oph.vkt.service.koski.KoskiService; @@ -95,11 +97,19 @@ public class PublicController { @Resource private FeatureFlagService featureFlagService; + @Resource + private PublicExaminerService publicExaminerService; + @GetMapping(path = "/examEvent") public List list() { return publicExamEventService.listExamEvents(ExamLevel.EXCELLENT); } + @GetMapping(path = "/examiner") + public List listExaminers() { + return publicExaminerService.listExaminers(); + } + @PostMapping(path = "/enrollment/reservation/{reservationId:\\d+}") @ResponseStatus(HttpStatus.CREATED) public PublicEnrollmentDTO createEnrollment( diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExaminerDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExaminerDTO.java new file mode 100644 index 000000000..64656baeb --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExaminerDTO.java @@ -0,0 +1,18 @@ +package fi.oph.vkt.api.dto; + +import fi.oph.vkt.model.type.ExamLanguage; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; +import java.util.List; +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record PublicExaminerDTO( + @NonNull @NotNull Long id, + @NonNull @NotNull String lastName, + @NonNull @NotNull String firstName, + @NonNull @NotNull List languages, + @NonNull @NotNull List municipalities, + @NonNull @NotNull List examDates +) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicMunicipalityDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicMunicipalityDTO.java new file mode 100644 index 000000000..14d40cb70 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicMunicipalityDTO.java @@ -0,0 +1,8 @@ +package fi.oph.vkt.api.dto; + +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record PublicMunicipalityDTO(@NonNull @NotNull String fi, @NonNull @NotNull String sv) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/FeatureFlag.java b/backend/vkt/src/main/java/fi/oph/vkt/model/FeatureFlag.java index 4a95dbaed..0f58cd21c 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/model/FeatureFlag.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/FeatureFlag.java @@ -1,7 +1,8 @@ package fi.oph.vkt.model; public enum FeatureFlag { - FREE_ENROLLMENT_FOR_HIGHEST_LEVEL_ALLOWED("freeEnrollmentAllowed"); + FREE_ENROLLMENT_FOR_HIGHEST_LEVEL_ALLOWED("freeEnrollmentAllowed"), + GOOD_AND_SATISFACTORY_LEVEL("goodAndSatisfactoryLevel"); private final String propertyKey; diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExaminerService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExaminerService.java new file mode 100644 index 000000000..143ef0182 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExaminerService.java @@ -0,0 +1,35 @@ +package fi.oph.vkt.service; + +import fi.oph.vkt.api.dto.PublicExaminerDTO; +import fi.oph.vkt.api.dto.PublicMunicipalityDTO; +import fi.oph.vkt.model.type.ExamLanguage; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class PublicExaminerService { + + public List listExaminers() { + final PublicMunicipalityDTO helsinki = PublicMunicipalityDTO.builder().fi("Helsinki").sv("Helsingfors").build(); + final PublicMunicipalityDTO espoo = PublicMunicipalityDTO.builder().fi("Espoo").sv("Esbo").build(); + final PublicMunicipalityDTO vantaa = PublicMunicipalityDTO.builder().fi("Vantaa").sv("Vanda").build(); + final PublicMunicipalityDTO kauniainen = PublicMunicipalityDTO.builder().fi("Kauniainen").sv("Grankulla").build(); + final List examiners = new ArrayList<>(); + examiners.add( + PublicExaminerDTO + .builder() + .id(1L) + .lastName("Laine") + .firstName("Eemeli") + .languages(List.of(ExamLanguage.FI)) + .municipalities(List.of(helsinki, espoo, vantaa, kauniainen)) + .examDates(List.of()) + .build() + ); + + return examiners; + } +} diff --git a/backend/vkt/src/main/resources/application.yaml b/backend/vkt/src/main/resources/application.yaml index 20c4b3b40..77b466e79 100644 --- a/backend/vkt/src/main/resources/application.yaml +++ b/backend/vkt/src/main/resources/application.yaml @@ -96,5 +96,6 @@ app: account: ${payment.paytrail.account:null} featureFlags: freeEnrollmentAllowed: ${feature-flags.free-enrollment-allowed:false} + goodAndSatisfactoryLevel: ${feature-flags.good-and-satisfactory-level:false} aws: s3-bucket: ${aws.s3-bucket:test} diff --git a/backend/vkt/src/main/resources/oph-configuration/vkt.properties.template b/backend/vkt/src/main/resources/oph-configuration/vkt.properties.template index 1e845099c..f513d1c73 100644 --- a/backend/vkt/src/main/resources/oph-configuration/vkt.properties.template +++ b/backend/vkt/src/main/resources/oph-configuration/vkt.properties.template @@ -33,5 +33,6 @@ koski.user={{vkt_koski_user}} koski.password={{vkt_koski_password}} feature-flags.free-enrollment-allowed={{vkt_feature_free_enrollment_allowed}} +feature-flags.good-and-satisfactory-level={{vkt_feature_good_and_satisfactory_level}} aws.s3-bucket={{vkt_aws_s3_bucket}} diff --git a/frontend/package.json b/frontend/package.json index 7bc4471b3..e341be630 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,7 @@ "@emotion/react": "^11.13.0", "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.0.14", + "@mui/base": "5.0.0-beta.58", "@mui/icons-material": "^5.16.7", "@mui/material": "^5.16.7", "@mui/system": "^5.16.7", @@ -109,8 +110,5 @@ "webpack": "^5.94.0", "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.1" - }, - "@comments": { - "react-router-dom": "Do not update past 6.13.0 until useBlocker is fixed, see https://github.com/remix-run/react-router/issues/11155" } } diff --git a/frontend/packages/shared/CHANGELOG.MD b/frontend/packages/shared/CHANGELOG.MD index db4d7c429..95ae899eb 100644 --- a/frontend/packages/shared/CHANGELOG.MD +++ b/frontend/packages/shared/CHANGELOG.MD @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Released] +## [1.11.5] - 2024-09-27 + +### Added +- MobileNavigationMenuWithPortal component + ## [1.11.4] - 2024-10-07 ### Fixed diff --git a/frontend/packages/shared/package.json b/frontend/packages/shared/package.json index 048f83414..6ee36fd3a 100644 --- a/frontend/packages/shared/package.json +++ b/frontend/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@opetushallitus/kieli-ja-kaantajatutkinnot.shared", - "version": "1.11.4", + "version": "1.11.5", "description": "Shared Frontend Package", "exports": { "./components": "./src/components/index.tsx", diff --git a/frontend/packages/shared/src/components/MobileNavigationMenu/MobileNavigationMenu.scss b/frontend/packages/shared/src/components/MobileNavigationMenu/MobileNavigationMenu.scss new file mode 100644 index 000000000..2ea3cd75d --- /dev/null +++ b/frontend/packages/shared/src/components/MobileNavigationMenu/MobileNavigationMenu.scss @@ -0,0 +1,30 @@ +@use '../../styles/abstracts/colors'; + +button.navigation-menu-toggle { + background-color: colors.$color-primary; + border: 0; +} + +.navigation-menu-contents { + ul { + list-style-type: none; + display: flex; + flex-direction: column; + padding: 1rem; + } + + li { + > a { + text-decoration: none; + } + padding: 1rem; + } + + li.active { + p { + color: colors.$color-secondary; + font-weight: 700; + } + border-left: 2px solid colors.$color-secondary; + } +} diff --git a/frontend/packages/shared/src/components/MobileNavigationMenu/MobileNavigationMenu.tsx b/frontend/packages/shared/src/components/MobileNavigationMenu/MobileNavigationMenu.tsx new file mode 100644 index 000000000..b6a7ec3c3 --- /dev/null +++ b/frontend/packages/shared/src/components/MobileNavigationMenu/MobileNavigationMenu.tsx @@ -0,0 +1,164 @@ +import { FocusTrap } from '@mui/base/FocusTrap'; +import CloseIcon from '@mui/icons-material/Close'; +import MenuIcon from '@mui/icons-material/Menu'; +import { ClickAwayListener, Divider, Paper } from '@mui/material'; +import { Fragment, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { Link } from 'react-router-dom'; + +import { Color } from '../../enums'; +import { NavigationLinksProps } from '../NavigationLinks/NavigationLinks'; +import { Text } from '../Text/Text'; + +import './MobileNavigationMenu.scss'; + +const MobileNavigationMenuToggle = ({ + openStateLabel, + openStateAriaLabel, + closedStateLabel, + closedStateAriaLabel, + isOpen, + setIsOpen, +}: { + openStateLabel: string; + openStateAriaLabel: string; + closedStateLabel: string; + closedStateAriaLabel: string; + isOpen: boolean; + setIsOpen: (state: boolean) => void; +}) => { + const handleClick = () => { + setIsOpen(!isOpen); + }; + + return ( + + ); +}; + +interface MobileNavigationMenuProps extends NavigationLinksProps { + closeMenu: () => void; +} + +export const MobileNavigationMenuContents = ({ + navigationAriaLabel, + links, + closeMenu, +}: MobileNavigationMenuProps) => { + const handleClickAway = (e: MouseEvent | TouchEvent) => { + // Prevent event default so that when user clicks on menu close button (outside actual menu contents), + // the menu isn't immediately opened again. + e.preventDefault(); + closeMenu(); + }; + + const handleEsc = (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + closeMenu(); + } + }; + + return ( + + + + + + ); +}; + +export const MobileNavigationMenuWithPortal = ({ + navigationAriaLabel, + openStateLabel, + openStateAriaLabel, + closedStateLabel, + closedStateAriaLabel, + links, + portalContainer, +}: { + openStateLabel: string; + openStateAriaLabel: string; + closedStateLabel: string; + closedStateAriaLabel: string; + portalContainer: HTMLElement; +} & NavigationLinksProps) => { + const [isMenuOpen, setIsMenuOpen] = useState(false); + + return ( + <> + setIsMenuOpen(true)} + /> + {isMenuOpen && + createPortal( + setIsMenuOpen(false)} + />, + portalContainer, + )} + + ); +}; diff --git a/frontend/packages/shared/src/components/NavigationLinks/NavigationLinks.scss b/frontend/packages/shared/src/components/NavigationLinks/NavigationLinks.scss index 688f40358..905ba60f0 100644 --- a/frontend/packages/shared/src/components/NavigationLinks/NavigationLinks.scss +++ b/frontend/packages/shared/src/components/NavigationLinks/NavigationLinks.scss @@ -11,6 +11,7 @@ li { padding-bottom: 2rem; + > a { text-decoration: none; } diff --git a/frontend/packages/shared/src/components/NavigationLinks/NavigationLinks.tsx b/frontend/packages/shared/src/components/NavigationLinks/NavigationLinks.tsx index 546a4c237..2a0a21aa6 100644 --- a/frontend/packages/shared/src/components/NavigationLinks/NavigationLinks.tsx +++ b/frontend/packages/shared/src/components/NavigationLinks/NavigationLinks.tsx @@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'; import { Text } from '../Text/Text'; import './NavigationLinks.scss'; -interface NavigationLinksProps { +export interface NavigationLinksProps { navigationAriaLabel: string; links: Array; } diff --git a/frontend/packages/shared/src/components/index.tsx b/frontend/packages/shared/src/components/index.tsx index ccf99f818..127b51bc4 100644 --- a/frontend/packages/shared/src/components/index.tsx +++ b/frontend/packages/shared/src/components/index.tsx @@ -53,3 +53,4 @@ export { NativeSelectWithLabel, } from './NativeSelect/NativeSelect'; export { NavigationLinks } from './NavigationLinks/NavigationLinks'; +export { MobileNavigationMenuWithPortal } from './MobileNavigationMenu/MobileNavigationMenu'; diff --git a/frontend/packages/vkt/package.json b/frontend/packages/vkt/package.json index 7f720bfd3..6518fa781 100644 --- a/frontend/packages/vkt/package.json +++ b/frontend/packages/vkt/package.json @@ -26,6 +26,6 @@ }, "dependencies": { "reduxjs-toolkit-persist": "^7.2.1", - "shared": "npm:@opetushallitus/kieli-ja-kaantajatutkinnot.shared@1.11.4" + "shared": "npm:@opetushallitus/kieli-ja-kaantajatutkinnot.shared@1.11.5" } } diff --git a/frontend/packages/vkt/public/i18n/fi-FI/common.json b/frontend/packages/vkt/public/i18n/fi-FI/common.json index e88025b21..5dbd7e469 100644 --- a/frontend/packages/vkt/public/i18n/fi-FI/common.json +++ b/frontend/packages/vkt/public/i18n/fi-FI/common.json @@ -100,7 +100,13 @@ "header": { "accessibility": { "continueToMain": "Jatka sisältöön", - "langSelectorAriaLabel": "Kieli / Språk" + "langSelectorAriaLabel": "Kieli / Språk", + "mainNavigation": "Päänavigaatio" + }, + "publicNavigationLinks": { + "excellentLevel": "Erinomaisen taidon tutkinnot", + "frontPage": "Etusivu", + "goodAndSatisfactoryLevel": "Hyvän ja tyydyttävän taidon tutkinnot" }, "sessionState": { "logOut": "Kirjaudu ulos" @@ -148,8 +154,10 @@ "clerkHomepage": "Virkailija", "contactDetails": "Ilmoittautuminen - täytä yhteystietosi", "educationDetails": "Ilmoittautuminen - koulutustiedot", + "excellentLevelLanding": "Erinomaisen taidon tutkinnot", "done": "Ilmoittautuminen - valmis", "frontPage": "Etusivu", + "goodAndSatisfactoryLevelLanding": "Hyvän ja tyydyttävän taidon tutkinnot", "logoutSuccess": "Uloskirjautuminen onnnistui", "notFound": "Etsimääsi sivua ei löytynyt", "paymentFail": "Ilmoittautuminen - maksua ei suoritettu", diff --git a/frontend/packages/vkt/public/i18n/fi-FI/public.json b/frontend/packages/vkt/public/i18n/fi-FI/public.json index 70fbadf29..7fb7acd6b 100644 --- a/frontend/packages/vkt/public/i18n/fi-FI/public.json +++ b/frontend/packages/vkt/public/i18n/fi-FI/public.json @@ -320,9 +320,30 @@ }, "title": "Tulevat tutkintotilaisuudet" }, + "publicExaminerListing": { + "examLanguage": { + "ALL": "suomi & ruotsi", + "FI": "suomi", + "SV": "ruotsi" + } + }, "logoutSuccessPage": { "heading": "Uloskirjautuminen onnnistui", "info": "Olet kirjautunut ulos. Suljethan vielä kaikki selainikkunat." + }, + "goodAndSatisfactoryLevel": { + "title": "Hyvän ja tyydyttävän taidon tutkinnot" + }, + "publicHomePage": { + "description": { + "part1": "Valtionhallinnon kielitutkinnot (VKT) on tarkoitettu julkishallinnon henkilöstön toisen kotimaisen kielen hallinnan osoittamiseen.", + "part2": "Tutkinnoilla osoitetaan suomen tai ruotsin kielen suullinen, kirjallinen ja ymmärtämisen taito." + }, + "selectExamination": { + "description": "Valtionhallinnon kielitutkintoja on kahta eri tyyppiä: toisella voit osoittaa erinomaisen taidon hallintaa ja toisella joko hyvän tai tyydyttävän taidon hallintaa.", + "heading": "Valitse tutkintosi" + }, + "title": "Valtionhallinnon kielitutkinnot (VKT)" } } } diff --git a/frontend/packages/vkt/public/i18n/sv-SE/common.json b/frontend/packages/vkt/public/i18n/sv-SE/common.json index 73db44729..c836a2c34 100644 --- a/frontend/packages/vkt/public/i18n/sv-SE/common.json +++ b/frontend/packages/vkt/public/i18n/sv-SE/common.json @@ -97,7 +97,13 @@ "header": { "accessibility": { "continueToMain": "Fortsätt till innehållet", - "langSelectorAriaLabel": "Kieli / Språk" + "langSelectorAriaLabel": "Kieli / Språk", + "mainNavigation": "Huvudnavigering" + }, + "publicNavigationLinks": { + "excellentLevel": "Examina som gäller utmärkta språkkunskaper", + "frontPage": "Framsida", + "goodAndSatisfactoryLevel": "Examina som gäller goda eller nöjaktiga språkkunskaper" }, "sessionState": { "logOut": "Logga ut" diff --git a/frontend/packages/vkt/public/i18n/sv-SE/public.json b/frontend/packages/vkt/public/i18n/sv-SE/public.json index 3b365c016..1396ccdef 100644 --- a/frontend/packages/vkt/public/i18n/sv-SE/public.json +++ b/frontend/packages/vkt/public/i18n/sv-SE/public.json @@ -321,6 +321,9 @@ "logoutSuccessPage": { "heading": "Utloggning lyckades", "info": "Du har loggat ut. Stäng alla fönster i webbläsaren." + }, + "goodAndSatisfactoryLevel": { + "title": "Examina som gäller goda eller nöjaktiga språkkunskaper" } } } diff --git a/frontend/packages/vkt/src/components/clerkExamEvent/listing/ClerkExamEventListing.tsx b/frontend/packages/vkt/src/components/clerkExamEvent/listing/ClerkExamEventListing.tsx index 6eda210e9..7f7c9b821 100644 --- a/frontend/packages/vkt/src/components/clerkExamEvent/listing/ClerkExamEventListing.tsx +++ b/frontend/packages/vkt/src/components/clerkExamEvent/listing/ClerkExamEventListing.tsx @@ -6,7 +6,7 @@ import { Color, Severity, Variant } from 'shared/enums'; import { ClerkExamEventListingHeader } from 'components/clerkExamEvent/listing/ClerkExamEventListingHeader'; import { ClerkExamEventListingRow } from 'components/clerkExamEvent/listing/ClerkExamEventListingRow'; import { ClerkExamEventToggleFilters } from 'components/clerkExamEvent/listing/ClerkExamEventToggleFilters'; -import { LanguageFilter } from 'components/publicExamEvent/LanguageFilter'; +import { LanguageFilter } from 'components/common/LanguageFilter'; import { useClerkTranslation, useCommonTranslation } from 'configs/i18n'; import { useAppDispatch, useAppSelector } from 'configs/redux'; import { AppRoutes, ExamLanguage } from 'enums/app'; diff --git a/frontend/packages/vkt/src/components/publicExamEvent/LanguageFilter.tsx b/frontend/packages/vkt/src/components/common/LanguageFilter.tsx similarity index 96% rename from frontend/packages/vkt/src/components/publicExamEvent/LanguageFilter.tsx rename to frontend/packages/vkt/src/components/common/LanguageFilter.tsx index 6c751218e..0cc927267 100644 --- a/frontend/packages/vkt/src/components/publicExamEvent/LanguageFilter.tsx +++ b/frontend/packages/vkt/src/components/common/LanguageFilter.tsx @@ -27,7 +27,7 @@ export const LanguageFilter = ({ {translateCommon('languageFilter.label')}: + !!matchPath({ path: route, end: false }, currentPath); + +const getNavigationLinks = ( + pathname: string, + goodAndSatisfactoryLevel: boolean, + translateCommon: TFunction, +) => { + const excellentLevelLink = { + active: isPathActive(pathname, AppRoutes.PublicExcellentLevelLanding), + label: translateCommon( + `header.publicNavigationLinks.${PublicNavigationLink.ExcellentLevel}`, + ), + href: AppRoutes.PublicExcellentLevelLanding, + }; + + const navigationLinks = goodAndSatisfactoryLevel + ? [ + { + active: isPathActive(pathname, AppRoutes.PublicHomePage), + label: translateCommon( + `header.publicNavigationLinks.${PublicNavigationLink.FrontPage}`, + ), + href: AppRoutes.PublicHomePage, + }, + excellentLevelLink, + { + active: isPathActive( + pathname, + AppRoutes.PublicGoodAndSatisfactoryLevelLanding, + ), + label: translateCommon( + `header.publicNavigationLinks.${PublicNavigationLink.GoodAndSatisfactoryLevel}`, + ), + href: AppRoutes.PublicGoodAndSatisfactoryLevelLanding, + }, + ] + : [excellentLevelLink]; + + return navigationLinks; +}; + +const PublicNavigationLinks = () => { + const translateCommon = useCommonTranslation(); + const { pathname } = useLocation(); + const { goodAndSatisfactoryLevel } = useAppSelector(featureFlagsSelector); + + const navigationLinks = getNavigationLinks( + pathname, + !!goodAndSatisfactoryLevel, + translateCommon, + ); + + return ( + + ); +}; + +const PublicMobileNavigationMenu = () => { + const translateCommon = useCommonTranslation(); + const { pathname } = useLocation(); + const { goodAndSatisfactoryLevel } = useAppSelector(featureFlagsSelector); + + const navigationLinks = getNavigationLinks( + pathname, + !!goodAndSatisfactoryLevel, + translateCommon, + ); + + const portalContainer = document.getElementById('mobile-menu-placeholder'); + + if (!portalContainer) { + return null; + } + + return ( + + ); +}; export const Header = (): JSX.Element => { const dispatch = useAppDispatch(); @@ -43,6 +147,8 @@ export const Header = (): JSX.Element => { const logoRedirectURL = isAuthenticated ? AppRoutes.ClerkHomePage : AppRoutes.PublicHomePage; + const activeUrl = window.location.href; + const isPublicUrl = !activeUrl.includes(AppRoutes.ClerkHomePage); const { isPhone } = useWindowProperties(); const isClerkAuthenticationValid = @@ -89,7 +195,7 @@ export const Header = (): JSX.Element => { /> )} -
+
{isClerkUI ? ( { /> ) : ( )}
-
+
{isAuthenticated && } + {isPublicUrl && !isPhone && } + {isPublicUrl && isPhone && }
-
+
{isAuthenticated && } {!isPhone && ( - - {isPhone ? ( - - ) : ( - - )} - - + + {isPhone ? ( + + ) : ( + + )} + ); }; diff --git a/frontend/packages/vkt/src/components/publicExaminerListing/PublicExaminerListing.tsx b/frontend/packages/vkt/src/components/publicExaminerListing/PublicExaminerListing.tsx new file mode 100644 index 000000000..1488bb76f --- /dev/null +++ b/frontend/packages/vkt/src/components/publicExaminerListing/PublicExaminerListing.tsx @@ -0,0 +1,176 @@ +import { + Box, + Paper, + SelectChangeEvent, + TableCell, + TableHead, + TableRow, +} from '@mui/material'; +import { Fragment } from 'react'; +import { + CustomButtonLink, + CustomCircularProgress, + CustomTable, + H2, + Text, +} from 'shared/components'; +import { APIResponseStatus, AppLanguage, Color, Variant } from 'shared/enums'; +import { useWindowProperties } from 'shared/hooks'; +import { DateUtils } from 'shared/utils'; + +import { LanguageFilter } from 'components/common/LanguageFilter'; +import { + getCurrentLang, + useCommonTranslation, + usePublicTranslation, +} from 'configs/i18n'; +import { useAppDispatch, useAppSelector } from 'configs/redux'; +import { ExamLanguage } from 'enums/app'; +import { PublicExaminer } from 'interfaces/publicExaminer'; +import { setPublicExaminerLanguageFilter } from 'redux/reducers/publicExaminer'; +import { + publicExaminerSelector, + selectFilteredPublicExaminers, +} from 'redux/selectors/publicExaminer'; + +const PublicExaminerListingHeader = () => { + const { isPhone } = useWindowProperties(); + + return ( + + {!isPhone && ( + + Tutkinnon vastaanottaja + Kieli + Paikkakunta + Tutkintopäivät + Toiminnot + + )} + + ); +}; + +const DesktopExaminerRow = ({ + name, + language, + municipalities, + examDates, +}: Omit) => { + // TODO Rendering for mobile users + const { t } = usePublicTranslation({ + keyPrefix: 'vkt.component.publicExaminerListing', + }); + const appLanguage = getCurrentLang(); + + return ( + + + {name} + + + {t('examLanguage.' + language)} + + + + {municipalities.length > 0 + ? municipalities + .map(({ fi, sv }) => + appLanguage === AppLanguage.Swedish ? sv : fi, + ) + .join(', ') + : ''} + + + + + {examDates.length > 0 + ? examDates.map((v, i) => ( + + {i > 0 ?
: undefined} + {DateUtils.formatOptionalDate(v)} +
+ )) + : 'Ei määritelty'} +
+
+ + + Ota yhteyttä + + +
+ ); +}; + +const getRowDetails = ({ + name, + language, + municipalities, + examDates, +}: PublicExaminer) => { + return ( + + ); +}; + +export const PublicExaminerListing = () => { + const { languageFilter, status } = useAppSelector(publicExaminerSelector); + const filteredExaminers = useAppSelector(selectFilteredPublicExaminers); + const dispatch = useAppDispatch(); + + const handleLanguageFilterChange = (event: SelectChangeEvent) => { + dispatch( + setPublicExaminerLanguageFilter(event.target.value as ExamLanguage), + ); + }; + + const translateCommon = useCommonTranslation(); + + switch (status) { + case APIResponseStatus.NotStarted: + case APIResponseStatus.InProgress: + return ; + case APIResponseStatus.Cancelled: + case APIResponseStatus.Error: + return ( + +

{translateCommon('errors.loadingFailed')}

+
+ ); + case APIResponseStatus.Success: + return ( + +
+
+

Ota yhteyttä tutkinnon vastaanottajiin

+
+
+ + } + /> +
+ ); + } +}; diff --git a/frontend/packages/vkt/src/enums/api.ts b/frontend/packages/vkt/src/enums/api.ts index 94ceda0d8..f4ffc86b1 100644 --- a/frontend/packages/vkt/src/enums/api.ts +++ b/frontend/packages/vkt/src/enums/api.ts @@ -3,6 +3,7 @@ export enum APIEndpoints { PublicAuthLogout = '/vkt/api/v1/auth/logout', PublicExamEvent = '/vkt/api/v1/examEvent', PublicEnrollmentAppointment = '/vkt/api/v1/enrollment/appointment', + PublicExaminer = '/vkt/api/v1/examiner', PublicEnrollment = '/vkt/api/v1/enrollment', PublicReservation = '/vkt/api/v1/reservation', PublicEducation = '/vkt/api/v1/education', diff --git a/frontend/packages/vkt/src/enums/app.ts b/frontend/packages/vkt/src/enums/app.ts index 62bfc5788..67dae7a7a 100644 --- a/frontend/packages/vkt/src/enums/app.ts +++ b/frontend/packages/vkt/src/enums/app.ts @@ -2,32 +2,50 @@ export enum AppConstants { CallerID = '1.2.246.562.10.00000000001.vkt', } +const excellentLevelRoutePrefix = '/vkt/erinomainen-taito'; +const excellentLevelEnrollmentRoute = + excellentLevelRoutePrefix + '/ilmoittaudu'; + export enum AppRoutes { PublicRoot = '/vkt', PublicHomePage = '/vkt/etusivu', - PublicEnrollment = '/vkt/ilmoittaudu', - PublicAuth = '/vkt/ilmoittaudu/:examEventId/tunnistaudu', - PublicEnrollmentContactDetails = '/vkt/ilmoittaudu/:examEventId/tiedot', - PublicEnrollmentEducationDetails = '/vkt/ilmoittaudu/:examEventId/koulutus', - PublicEnrollmentSelectExam = '/vkt/ilmoittaudu/:examEventId/tutkinto', - PublicEnrollmentPreview = '/vkt/ilmoittaudu/:examEventId/esikatsele', - PublicEnrollmentPaymentFail = '/vkt/ilmoittaudu/:examEventId/maksu/peruutettu', - PublicEnrollmentPaymentSuccess = '/vkt/ilmoittaudu/:examEventId/maksu/valmis', - PublicEnrollmentDoneQueued = '/vkt/ilmoittaudu/:examEventId/jono-valmis', - PublicEnrollmentDone = '/vkt/ilmoittaudu/:examEventId/valmis', - PublicEnrollmentAppointment = '/vkt/markkinapaikka', - PublicAuthAppointment = '/vkt/markkinapaikka/:enrollmentId/tunnistaudu', + + // Routes for excellent level + PublicExcellentLevelLanding = excellentLevelRoutePrefix, + PublicEnrollment = excellentLevelEnrollmentRoute, + PublicAuth = excellentLevelEnrollmentRoute + '/:examEventId/tunnistaudu', + PublicEnrollmentContactDetails = excellentLevelEnrollmentRoute + + '/:examEventId/tiedot', + PublicEnrollmentEducationDetails = excellentLevelEnrollmentRoute + + '/:examEventId/koulutus', + PublicEnrollmentSelectExam = excellentLevelEnrollmentRoute + + '/:examEventId/tutkinto', + PublicEnrollmentPreview = excellentLevelEnrollmentRoute + + '/:examEventId/esikatsele', + PublicEnrollmentPaymentFail = excellentLevelEnrollmentRoute + + '/:examEventId/maksu/peruutettu', + PublicEnrollmentPaymentSuccess = excellentLevelEnrollmentRoute + + '/:examEventId/maksu/valmis', + PublicEnrollmentDoneQueued = excellentLevelEnrollmentRoute + + '/:examEventId/jono-valmis', + PublicEnrollmentDone = excellentLevelEnrollmentRoute + '/:examEventId/valmis', + + // Routes for good and satisfactory level - TODO + PublicGoodAndSatisfactoryLevelLanding = '/vkt/hyva-ja-tyydyttava-taito', PublicEnrollmentAppointmentContactDetails = '/vkt/markkinapaikka/:enrollmentId/tiedot', PublicEnrollmentAppointmentPreview = '/vkt/markkinapaikka/:enrollmentId/esikatsele', PublicEnrollmentAppointmentPaymentFail = '/vkt/markkinapaikka/:enrollmentId/maksu/peruutettu', PublicEnrollmentAppointmentPaymentSuccess = '/vkt/markkinapaikka/:enrollmentId/maksu/valmis', + + // Routes for clerk user ClerkHomePage = '/vkt/virkailija', ClerkExamEventCreatePage = '/vkt/virkailija/tutkintotilaisuus/luo', ClerkExamEventOverviewPage = '/vkt/virkailija/tutkintotilaisuus/:examEventId', ClerkEnrollmentOverviewPage = '/vkt/virkailija/tutkintotilaisuus/:examEventId/ilmoittautuminen', ClerkLocalLogoutPage = '/vkt/cas/localLogout', + + // Miscellaneous AccessibilityStatementPage = '/vkt/saavutettavuusseloste', - PrivacyPolicyPage = '/vkt/tietosuojaseloste', LogoutSuccess = '/vkt/uloskirjautuminen-onnistui', NotFoundPage = '*', } @@ -73,3 +91,9 @@ export enum PaymentStatus { PENDING = 'PENDING', DELAYED = 'DELAYED', } + +export enum PublicNavigationLink { + FrontPage = 'frontPage', + ExcellentLevel = 'excellentLevel', + GoodAndSatisfactoryLevel = 'goodAndSatisfactoryLevel', +} diff --git a/frontend/packages/vkt/src/interfaces/featureFlags.ts b/frontend/packages/vkt/src/interfaces/featureFlags.ts index b56e45771..39a1a2334 100644 --- a/frontend/packages/vkt/src/interfaces/featureFlags.ts +++ b/frontend/packages/vkt/src/interfaces/featureFlags.ts @@ -1,5 +1,6 @@ export interface FeatureFlags { freeEnrollmentAllowed: boolean; + goodAndSatisfactoryLevel: boolean; } export interface FeatureFlagsResponse extends Partial {} diff --git a/frontend/packages/vkt/src/interfaces/publicExaminer.ts b/frontend/packages/vkt/src/interfaces/publicExaminer.ts new file mode 100644 index 000000000..8cd737c8d --- /dev/null +++ b/frontend/packages/vkt/src/interfaces/publicExaminer.ts @@ -0,0 +1,31 @@ +import { Dayjs } from 'dayjs'; +import { WithId } from 'shared/interfaces'; + +import { ExamLanguage } from 'enums/app'; +import { APIResponseStatus } from 'shared/enums'; + +interface PublicMunicipality { + fi: string; + sv: string; +} + +export interface PublicExaminer extends WithId { + name: string; + language: ExamLanguage; + municipalities: Array; + examDates: Array; +} + +export interface PublicExaminerResponse extends WithId { + lastName: string; + firstName: string; + languages: Array; + municipalities: Array; + examDates: Array; +} + +export interface PublicExaminerState { + status: APIResponseStatus; + examiners: Array; + languageFilter: ExamLanguage; +} diff --git a/frontend/packages/vkt/src/pages/PublicHomePage.tsx b/frontend/packages/vkt/src/pages/PublicHomePage.tsx index 046134309..81a428420 100644 --- a/frontend/packages/vkt/src/pages/PublicHomePage.tsx +++ b/frontend/packages/vkt/src/pages/PublicHomePage.tsx @@ -1,9 +1,164 @@ -import { Box, Grid } from '@mui/material'; -import { FC } from 'react'; +import { Box, Button, Container, Grid, Paper, Typography } from '@mui/material'; +import { TFunction } from 'i18next'; +import React, { FC } from 'react'; +import { Trans } from 'react-i18next'; +import { Link } from 'react-router-dom'; +import { H1, H2, HeaderSeparator, Text } from 'shared/components'; +import { Color, I18nNamespace, Variant } from 'shared/enums'; +import { useWindowProperties } from 'shared/hooks'; -import { PublicExamEventGrid } from 'components/publicExamEvent/PublicExamEventGrid'; +import { useCommonTranslation, usePublicTranslation } from 'configs/i18n'; +import { AppRoutes } from 'enums/app'; + +const LinkButton = ({ to, label }: { to: AppRoutes; label: string }) => { + return ( + + + + ); +}; + +const LevelCard = ({ + heading, + contents, + linkLabel, + linkTo, +}: { + heading: string; + contents: React.JSX.Element; + linkLabel: string; + linkTo: AppRoutes; +}) => { + return ( + +
+
+ + {heading} + + {contents} +
+ +
+
+ ); +}; + +const ExcellentLevelCard = () => { + return ( + + Tekstiä. + + Toinen kappale. Hieman pituuttakin tälle paragraaaaaafffille. + + + } + linkLabel="Ilmoittaudu erinomaisen taidon tutkintoihin" + linkTo={AppRoutes.PublicExcellentLevelLanding} + /> + ); +}; + +const GoodAndSatisfactoryLevelCard = () => { + return ( + + Yhdellä kokeella voit osoittaa tuloksesta riippuen hyvää tai + tyydyttävää taitoa. + + } + linkLabel="Ota yhteyttä tutkinnon vastaanottajiin" + linkTo={AppRoutes.PublicGoodAndSatisfactoryLevelLanding} + /> + ); +}; + +const BoldedTranslationString = ({ + i18nKey, + t, +}: { + i18nKey: string; + t: TFunction; +}) => { + return ]} />; +}; + +const BulletList = ({ + keyPrefix, + points, +}: { + keyPrefix: string; + points: Array; +}) => { + const { t } = usePublicTranslation({ + keyPrefix, + }); + + return ( + + {points.map((point, i) => ( +
  • + +
  • + ))} +
    + ); +}; + +const DescriptionBox = () => { + const { t } = usePublicTranslation({ + keyPrefix: 'vkt.component.publicExamEventGrid.description', + }); + const translateCommon = useCommonTranslation(); + + return ( + +
    +

    {t('title')}

    +
    + {t('skills')} + +
    + + {translateCommon('info.selectExam')} + + , ]} + > + +
    +
    + ); +}; export const PublicHomePage: FC = () => { + const { t } = usePublicTranslation({ + keyPrefix: 'vkt.component.publicHomePage', + }); + const { isPhone } = useWindowProperties(); + return ( { direction="column" className="public-homepage__grid-container" > - + +

    {t('title')}

    + +
    + + {t('description.part1')} +
    + {t('description.part2')} +
    +

    {t('selectExamination.heading')}

    + {t('selectExamination.description')} + +
    + + +
    +
    +
    ); diff --git a/frontend/packages/vkt/src/pages/PublicEnrollmentPage.tsx b/frontend/packages/vkt/src/pages/excellentLevel/PublicEnrollmentPage.tsx similarity index 100% rename from frontend/packages/vkt/src/pages/PublicEnrollmentPage.tsx rename to frontend/packages/vkt/src/pages/excellentLevel/PublicEnrollmentPage.tsx diff --git a/frontend/packages/vkt/src/pages/excellentLevel/PublicExcellentLevelLandingPage.tsx b/frontend/packages/vkt/src/pages/excellentLevel/PublicExcellentLevelLandingPage.tsx new file mode 100644 index 000000000..9f7532058 --- /dev/null +++ b/frontend/packages/vkt/src/pages/excellentLevel/PublicExcellentLevelLandingPage.tsx @@ -0,0 +1,19 @@ +import { Box, Grid } from '@mui/material'; +import { FC } from 'react'; + +import { PublicExamEventGrid } from 'components/publicExamEvent/PublicExamEventGrid'; + +export const PublicExcellentLevelLandingPage: FC = () => { + return ( + + + + + + ); +}; diff --git a/frontend/packages/vkt/src/pages/goodAndSatisfactoryLevel/PublicGoodAndSatisfactoryLevelLandingPage.tsx b/frontend/packages/vkt/src/pages/goodAndSatisfactoryLevel/PublicGoodAndSatisfactoryLevelLandingPage.tsx new file mode 100644 index 000000000..cd9fff971 --- /dev/null +++ b/frontend/packages/vkt/src/pages/goodAndSatisfactoryLevel/PublicGoodAndSatisfactoryLevelLandingPage.tsx @@ -0,0 +1,42 @@ +import { Box, Grid } from '@mui/material'; +import { FC, useEffect } from 'react'; +import { H1, HeaderSeparator, Text } from 'shared/components'; + +import { PublicExaminerListing } from 'components/publicExaminerListing/PublicExaminerListing'; +import { usePublicTranslation } from 'configs/i18n'; +import { useAppDispatch } from 'configs/redux'; +import { loadPublicExaminers } from 'redux/reducers/publicExaminer'; + +export const PublicGoodAndSatisfactoryLevelLandingPage: FC = () => { + const { t } = usePublicTranslation({ + keyPrefix: 'vkt.component.goodAndSatisfactoryLevel', + }); + const dispatch = useAppDispatch(); + useEffect(() => { + dispatch(loadPublicExaminers()); + }, [dispatch]); + + return ( + + + +

    {t('title')}

    + + + Jotain infotekstiä hyvän ja tyydyttävän taidon tutkinnoista.
    + Mahdollisesti lyhyet ohjeet yhteydenottoon liittyen.
    + Kenties linkki OPH:n sivuille. +
    +
    + + + +
    +
    + ); +}; diff --git a/frontend/packages/vkt/src/redux/reducers/featureFlags.ts b/frontend/packages/vkt/src/redux/reducers/featureFlags.ts index fd64cb5b2..a5ee4fae9 100644 --- a/frontend/packages/vkt/src/redux/reducers/featureFlags.ts +++ b/frontend/packages/vkt/src/redux/reducers/featureFlags.ts @@ -3,7 +3,7 @@ import { APIResponseStatus } from 'shared/enums'; import { FeatureFlags } from 'interfaces/featureFlags'; -interface FeatureFlagsState extends Partial { +export interface FeatureFlagsState extends Partial { status: APIResponseStatus; } diff --git a/frontend/packages/vkt/src/redux/reducers/publicExaminer.ts b/frontend/packages/vkt/src/redux/reducers/publicExaminer.ts new file mode 100644 index 000000000..59009f28e --- /dev/null +++ b/frontend/packages/vkt/src/redux/reducers/publicExaminer.ts @@ -0,0 +1,42 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { APIResponseStatus } from 'shared/enums'; + +import { ExamLanguage } from 'enums/app'; +import { PublicExaminer, PublicExaminerState } from 'interfaces/publicExaminer'; + +const initialState: PublicExaminerState = { + status: APIResponseStatus.NotStarted, + examiners: [], + languageFilter: ExamLanguage.ALL, +}; + +const publicExaminerSlice = createSlice({ + name: 'publicExaminer', + initialState, + reducers: { + loadPublicExaminers(state) { + state.status = APIResponseStatus.InProgress; + }, + rejectPublicExaminers(state) { + state.status = APIResponseStatus.Error; + }, + storePublicExaminers(state, action: PayloadAction>) { + state.status = APIResponseStatus.Success; + state.examiners = action.payload; + }, + setPublicExaminerLanguageFilter( + state, + action: PayloadAction, + ) { + state.languageFilter = action.payload; + }, + }, +}); + +export const publicExaminerReducer = publicExaminerSlice.reducer; +export const { + loadPublicExaminers, + rejectPublicExaminers, + storePublicExaminers, + setPublicExaminerLanguageFilter, +} = publicExaminerSlice.actions; diff --git a/frontend/packages/vkt/src/redux/sagas/index.ts b/frontend/packages/vkt/src/redux/sagas/index.ts index 69b3a6c80..b343e893f 100644 --- a/frontend/packages/vkt/src/redux/sagas/index.ts +++ b/frontend/packages/vkt/src/redux/sagas/index.ts @@ -10,6 +10,7 @@ import { watchPublicEducation } from 'redux/sagas/publicEducation'; import { watchPublicEnrollments } from 'redux/sagas/publicEnrollment'; import { watchPublicEnrollmentAppointments } from 'redux/sagas/publicEnrollmentAppointment'; import { watchPublicExamEvents } from 'redux/sagas/publicExamEvent'; +import { watchPublicExaminers } from 'redux/sagas/publicExaminer'; import { watchFileUpload } from 'redux/sagas/publicFileUpload'; import { watchPublicUser } from 'redux/sagas/publicUser'; @@ -27,5 +28,6 @@ export default function* rootSaga() { watchFileUpload(), watchPublicEducation(), watchPublicEnrollmentAppointments(), + watchPublicExaminers(), ]); } diff --git a/frontend/packages/vkt/src/redux/sagas/publicExaminer.ts b/frontend/packages/vkt/src/redux/sagas/publicExaminer.ts new file mode 100644 index 000000000..117bee571 --- /dev/null +++ b/frontend/packages/vkt/src/redux/sagas/publicExaminer.ts @@ -0,0 +1,31 @@ +import { AxiosResponse } from 'axios'; +import { call, put, takeLatest } from 'redux-saga/effects'; + +import axiosInstance from 'configs/axios'; +import { APIEndpoints } from 'enums/api'; +import { PublicExaminerResponse } from 'interfaces/publicExaminer'; +import { + loadPublicExaminers, + rejectPublicExaminers, + storePublicExaminers, +} from 'redux/reducers/publicExaminer'; +import { SerializationUtils } from 'utils/serialization'; + +function* loadPublicExaminersSaga() { + try { + const response: AxiosResponse> = yield call( + axiosInstance.get, + APIEndpoints.PublicExaminer, + ); + const examiners = response.data.map( + SerializationUtils.deserializePublicExaminer, + ); + yield put(storePublicExaminers(examiners)); + } catch (error) { + yield put(rejectPublicExaminers()); + } +} + +export function* watchPublicExaminers() { + yield takeLatest(loadPublicExaminers.type, loadPublicExaminersSaga); +} diff --git a/frontend/packages/vkt/src/redux/selectors/featureFlags.ts b/frontend/packages/vkt/src/redux/selectors/featureFlags.ts index ab1750508..2fb3e93de 100644 --- a/frontend/packages/vkt/src/redux/selectors/featureFlags.ts +++ b/frontend/packages/vkt/src/redux/selectors/featureFlags.ts @@ -1,3 +1,5 @@ import { RootState } from 'configs/redux'; +import { FeatureFlagsState } from 'redux/reducers/featureFlags'; -export const featureFlagsSelector = (state: RootState) => state.featureFlags; +export const featureFlagsSelector = (state: RootState): FeatureFlagsState => + state.featureFlags; diff --git a/frontend/packages/vkt/src/redux/selectors/publicExaminer.ts b/frontend/packages/vkt/src/redux/selectors/publicExaminer.ts new file mode 100644 index 000000000..0d0ba0bc3 --- /dev/null +++ b/frontend/packages/vkt/src/redux/selectors/publicExaminer.ts @@ -0,0 +1,24 @@ +import { createSelector } from '@reduxjs/toolkit'; + +import { RootState } from 'configs/redux'; +import { ExamLanguage } from 'enums/app'; +import { PublicExaminer, PublicExaminerState } from 'interfaces/publicExaminer'; + +export const publicExaminerSelector: ( + state: RootState, +) => PublicExaminerState = (state: RootState) => state.publicExaminer; + +export const selectFilteredPublicExaminers = createSelector( + (state: RootState) => state.publicExaminer.examiners, + (state: RootState) => state.publicExaminer.languageFilter, + (publicExaminers: Array, languageFilter: ExamLanguage) => { + if (languageFilter === ExamLanguage.ALL) { + return publicExaminers; + } else { + return publicExaminers.filter( + ({ language }) => + language === ExamLanguage.ALL || language === languageFilter, + ); + } + }, +); diff --git a/frontend/packages/vkt/src/redux/store/index.ts b/frontend/packages/vkt/src/redux/store/index.ts index c994904ee..875037c78 100644 --- a/frontend/packages/vkt/src/redux/store/index.ts +++ b/frontend/packages/vkt/src/redux/store/index.ts @@ -15,6 +15,7 @@ import { publicEducationReducer } from 'redux/reducers/publicEducation'; import { publicEnrollmentReducer } from 'redux/reducers/publicEnrollment'; import { publicEnrollmentAppointmentReducer } from 'redux/reducers/publicEnrollmentAppointment'; import { publicExamEventReducer } from 'redux/reducers/publicExamEvent'; +import { publicExaminerReducer } from 'redux/reducers/publicExaminer'; import { publicFileUploadReducer } from 'redux/reducers/publicFileUpload'; import { publicUserReducer } from 'redux/reducers/publicUser'; import rootSaga from 'redux/sagas/index'; @@ -40,6 +41,7 @@ const reducer = combineReducers({ publicFileUpload: publicFileUploadReducer, publicEducation: publicEducationReducer, publicEnrollmentAppointment: publicEnrollmentAppointmentReducer, + publicExaminer: publicExaminerReducer, }); const persistedReducer = persistReducer(persistConfig, reducer); diff --git a/frontend/packages/vkt/src/routers/AppRouter.tsx b/frontend/packages/vkt/src/routers/AppRouter.tsx index 6f2975753..29f8de9cd 100644 --- a/frontend/packages/vkt/src/routers/AppRouter.tsx +++ b/frontend/packages/vkt/src/routers/AppRouter.tsx @@ -30,6 +30,9 @@ import { ClerkEnrollmentOverviewPage } from 'pages/ClerkEnrollmentOverviewPage'; import { ClerkExamEventCreatePage } from 'pages/ClerkExamEventCreatePage'; import { ClerkExamEventOverviewPage } from 'pages/ClerkExamEventOverviewPage'; import { ClerkHomePage } from 'pages/ClerkHomePage'; +import { PublicEnrollmentPage } from 'pages/excellentLevel/PublicEnrollmentPage'; +import { PublicExcellentLevelLandingPage } from 'pages/excellentLevel/PublicExcellentLevelLandingPage'; +import { PublicGoodAndSatisfactoryLevelLandingPage } from 'pages/goodAndSatisfactoryLevel/PublicGoodAndSatisfactoryLevelLandingPage'; import { LogoutSuccess } from 'pages/LogoutSuccess'; import { NotFoundPage } from 'pages/NotFoundPage'; import { PublicEnrollmentAppointmentPage } from 'pages/PublicEnrollmentAppointmentPage'; @@ -70,7 +73,8 @@ export const AppRouter: FC = () => {
    -
    +
    +
    @@ -79,17 +83,35 @@ export const AppRouter: FC = () => {
    ); + // TODO Consider serving different page as front page when feature flag for good and satisfactory levels is enabled? const FrontPage = ( ); + // TODO Enable / disable routes for good and satisfactory level based on feature flag? const router = createBrowserRouter( createRoutesFromElements( + + + + } + /> + + + + } + /> a { @@ -34,16 +47,24 @@ } } - &__center { - align-self: flex-end; - justify-self: flex-end; - - [role='tablist'] { - gap: 3rem; + &__navigation { + @include not-phone { + grid-area: navigation; + align-self: flex-end; + justify-self: start; + } + @include phone { + margin-left: auto; } } - &__right { + &__language-select { + @include not-phone { + grid-area: language-select; + } + @include phone { + align-self: center; + } align-self: center; color: $color-secondary-dark; display: flex; @@ -51,10 +72,6 @@ gap: 2rem; justify-content: flex-end; - @include phone { - align-self: center; - } - & &__avatar { background-color: $color-secondary-dark; font-size: 1.1rem; diff --git a/frontend/packages/vkt/src/styles/components/publicExaminerListing/_public-examiner-listing.scss b/frontend/packages/vkt/src/styles/components/publicExaminerListing/_public-examiner-listing.scss new file mode 100644 index 000000000..667728d4c --- /dev/null +++ b/frontend/packages/vkt/src/styles/components/publicExaminerListing/_public-examiner-listing.scss @@ -0,0 +1,3 @@ +.public-examiner-listing { + padding: 3rem; +} diff --git a/frontend/packages/vkt/src/styles/pages/_public-homepage.scss b/frontend/packages/vkt/src/styles/pages/_public-homepage.scss index dbd87ebaf..b4780de64 100644 --- a/frontend/packages/vkt/src/styles/pages/_public-homepage.scss +++ b/frontend/packages/vkt/src/styles/pages/_public-homepage.scss @@ -35,4 +35,18 @@ padding: 3rem 2rem; } } + + & &__cards { + @include not-phone { + width: 90%; + } + } + + & &__level-description-card { + padding: 2rem; + @include not-phone { + width: 50%; + height: 50rem; + } + } } diff --git a/frontend/packages/vkt/src/styles/styles.scss b/frontend/packages/vkt/src/styles/styles.scss index 43f320c15..de9247a5a 100644 --- a/frontend/packages/vkt/src/styles/styles.scss +++ b/frontend/packages/vkt/src/styles/styles.scss @@ -17,6 +17,7 @@ @import 'components/layouts/footer'; @import 'components/layouts/session-header'; @import 'components/publicEnrollment/public-enrollment'; +@import 'components/publicExaminerListing/public-examiner-listing'; // Pages @import 'pages/accessibility-statement-page'; diff --git a/frontend/packages/vkt/src/tests/cypress/support/commands.ts b/frontend/packages/vkt/src/tests/cypress/support/commands.ts index deef88404..e0a75e627 100644 --- a/frontend/packages/vkt/src/tests/cypress/support/commands.ts +++ b/frontend/packages/vkt/src/tests/cypress/support/commands.ts @@ -5,7 +5,7 @@ Cypress.Commands.add('openPublicHomePage', () => { cy.window().then((win) => { win.sessionStorage.setItem('persist:root', '{}'); }); - cy.visit(AppRoutes.PublicHomePage); + cy.visit(AppRoutes.PublicExcellentLevelLanding); }); Cypress.Commands.add( diff --git a/frontend/packages/vkt/src/tests/cypress/support/page-objects/clerkHomePage.ts b/frontend/packages/vkt/src/tests/cypress/support/page-objects/clerkHomePage.ts index 77b9c2890..7275bb2fa 100644 --- a/frontend/packages/vkt/src/tests/cypress/support/page-objects/clerkHomePage.ts +++ b/frontend/packages/vkt/src/tests/cypress/support/page-objects/clerkHomePage.ts @@ -5,7 +5,7 @@ const row = (id: number) => `clerk-exam-events__id-${id}-row`; class ClerkHomePage { elements = { examEventRow: (id: number) => cy.findByTestId(row(id)), - languageFilter: () => cy.findByTestId('exam-events__language-filter'), + languageFilter: () => cy.findByTestId('language-filter'), pagination: () => cy.get('.table__head-box__pagination'), toggleFilter: (toggleFilter: ExamEventToggleFilter) => cy.findByTestId(`clerk-exam-event-toggle-filters__${toggleFilter}-btn`), diff --git a/frontend/packages/vkt/src/tests/cypress/support/page-objects/publicHomePage.ts b/frontend/packages/vkt/src/tests/cypress/support/page-objects/publicHomePage.ts index 41f1fa9c9..e07461954 100644 --- a/frontend/packages/vkt/src/tests/cypress/support/page-objects/publicHomePage.ts +++ b/frontend/packages/vkt/src/tests/cypress/support/page-objects/publicHomePage.ts @@ -13,7 +13,7 @@ class PublicHomePage { cy .get('.public-homepage__grid-container__result-box') .find('table > tbody > tr'), - languageFilter: () => cy.findByTestId('exam-events__language-filter'), + languageFilter: () => cy.findByTestId('language-filter'), reservationTimerText: () => cy.findByTestId('public-enrollment__reservation-timer-text'), reservationRenewButton: () => diff --git a/frontend/packages/vkt/src/tests/jest/components/layouts/__snapshots__/Header.test.tsx.snap b/frontend/packages/vkt/src/tests/jest/components/layouts/__snapshots__/Header.test.tsx.snap index 5f449f0b4..61f87ac94 100644 --- a/frontend/packages/vkt/src/tests/jest/components/layouts/__snapshots__/Header.test.tsx.snap +++ b/frontend/packages/vkt/src/tests/jest/components/layouts/__snapshots__/Header.test.tsx.snap @@ -21,7 +21,7 @@ exports[`Header should render Header correctly 1`] = ` className="MuiToolbar-root MuiToolbar-gutters MuiToolbar-regular header__toolbar css-hyum1k-MuiToolbar-root" >