From d3368e9b850be0a47e06ff68809932ffcf7edc2d Mon Sep 17 00:00:00 2001 From: Hrishikesh Vaipurkar Date: Mon, 6 Oct 2025 08:46:26 +0000 Subject: [PATCH] Merged in feat/SW-2782-create-sas-branded-header (pull request #2878) feat(SW-2782): Updated header as per design, added language switcher and user menu * feat(SW-2782): Updated header as per design, added language switcher and user menu * feat(SW-2782): Updated UI as per design * feat(SW-2782): Optimised code with use of Popover and modal from RAC Approved-by: Anton Gunnarsson --- apps/partner-sas/components/Header/Header.tsx | 71 +----- .../components/Header/header.module.css | 18 -- .../components/LanguageSwitcher/index.tsx | 204 ++++++++++++++++++ .../languageSwitcher.module.css | 102 +++++++++ .../components/Menu/MobileMenu/index.tsx | 49 +++++ .../Menu/MobileMenu/mobile-menu.module.css | 104 +++++++++ .../components/Menu/NavigationMenu/index.tsx | 48 +++++ .../NavigationMenu/navigation-menu.module.css | 50 +++++ .../components/Menu/UserMenu/index.tsx | 201 +++++++++++++++++ .../Menu/UserMenu/user-menu.module.css | 107 +++++++++ apps/partner-sas/components/Menu/index.tsx | 45 ++++ .../components/Menu/menu.module.css | 18 ++ apps/partner-sas/components/Menu/utils.ts | 6 + apps/partner-sas/globals.css | 3 +- apps/partner-sas/package.json | 4 +- .../LanguageSwitcherContent/index.tsx | 3 +- .../components/LanguageSwitcher/index.tsx | 2 +- .../components/MyPages/Profile/index.tsx | 3 +- apps/scandic-web/constants/languages.ts | 10 +- .../languageSwitcher/languageSwitcher.ts | 2 +- packages/common/constants/language.ts | 23 ++ .../contentstack/languageSwitcher/query.ts | 2 +- .../contentstack/languageSwitcher/utils.ts | 10 +- .../routers/contentstack/metadata/output.ts | 7 +- .../routers/contentstack/metadata/query.ts | 2 +- packages/trpc/lib/types/languageSwitcher.ts | 14 -- yarn.lock | 2 + 27 files changed, 985 insertions(+), 125 deletions(-) create mode 100644 apps/partner-sas/components/LanguageSwitcher/index.tsx create mode 100644 apps/partner-sas/components/LanguageSwitcher/languageSwitcher.module.css create mode 100644 apps/partner-sas/components/Menu/MobileMenu/index.tsx create mode 100644 apps/partner-sas/components/Menu/MobileMenu/mobile-menu.module.css create mode 100644 apps/partner-sas/components/Menu/NavigationMenu/index.tsx create mode 100644 apps/partner-sas/components/Menu/NavigationMenu/navigation-menu.module.css create mode 100644 apps/partner-sas/components/Menu/UserMenu/index.tsx create mode 100644 apps/partner-sas/components/Menu/UserMenu/user-menu.module.css create mode 100644 apps/partner-sas/components/Menu/index.tsx create mode 100644 apps/partner-sas/components/Menu/menu.module.css create mode 100644 apps/partner-sas/components/Menu/utils.ts diff --git a/apps/partner-sas/components/Header/Header.tsx b/apps/partner-sas/components/Header/Header.tsx index dd3862859..08586d9b6 100644 --- a/apps/partner-sas/components/Header/Header.tsx +++ b/apps/partner-sas/components/Header/Header.tsx @@ -1,78 +1,17 @@ -"use client" - -import { useSession } from "next-auth/react" - -import Image from "@scandic-hotels/design-system/Image" -import Link from "@scandic-hotels/design-system/Link" -import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer" -import { Typography } from "@scandic-hotels/design-system/Typography" -import { trpc } from "@scandic-hotels/trpc/client" - -import useLang from "@/hooks/useLang" - +import { Menu } from "../Menu" import { PoweredByScandic } from "../PoweredByScandic/PoweredByScandic" import styles from "./header.module.css" export function Header() { - const lang = useLang() - const session = useSession() - - const { - data: profileData, - isLoading, - isSuccess, - } = trpc.partner.sas.getEuroBonusProfile.useQuery(undefined, { - enabled: session.status === "authenticated", - }) - return ( <>
- SAS logotype - {session.status === "loading" && ( - - )} - {session.status === "unauthenticated" && ( - /** For some reason it complains about RSC-payload if using */ - - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {"Login here"} - - )} - {session.status === "authenticated" && ( -
- - - {session.data?.user && <>{session.data.user.email}} - - - {isLoading && } - {isSuccess && profileData && ( - - - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {profileData.points.total} Points - - - )} - - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {"Logout"} - -
- )} + +
+ +
-
- -
) } diff --git a/apps/partner-sas/components/Header/header.module.css b/apps/partner-sas/components/Header/header.module.css index 7a48486b0..ba5303a8e 100644 --- a/apps/partner-sas/components/Header/header.module.css +++ b/apps/partner-sas/components/Header/header.module.css @@ -1,21 +1,3 @@ -.header { - background-color: var(--TEMP-sas-default); - color: white; - display: flex; - align-items: center; - padding: 16px; - justify-content: space-between; - - @media screen and (min-width: 768px) { - padding: 20px 40px; - } -} - -.logo { - height: auto; - width: 90px; -} - .poweredBy { padding: 6px 16px; background-color: var(--Base-Surface-Primary-light-Normal); diff --git a/apps/partner-sas/components/LanguageSwitcher/index.tsx b/apps/partner-sas/components/LanguageSwitcher/index.tsx new file mode 100644 index 000000000..164e972da --- /dev/null +++ b/apps/partner-sas/components/LanguageSwitcher/index.tsx @@ -0,0 +1,204 @@ +"use client" + +import { usePathname } from "next/navigation" +import { + Dialog, + DialogTrigger, + Modal, + ModalOverlay, + Popover, +} from "react-aria-components" +import { useIntl } from "react-intl" + +import { + type Lang, + languages, + type LanguageSwitcherData, +} from "@scandic-hotels/common/constants/language" +import { Button } from "@scandic-hotels/design-system/Button" +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import Link from "@scandic-hotels/design-system/Link" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import styles from "./languageSwitcher.module.css" + +type LanguageSwitcherProps = { + currentLanguage: Lang + isMobile?: boolean +} + +export function replaceUrlPart(currentPath: string, newPart: string): string { + const pathSegments = currentPath.split("/").filter((segment) => segment) + + const newPathSegments = newPart + .replace(/\/$/, "") + .split("/") + .filter((segment) => segment) + + const isFullPathReplacement = newPathSegments.length > 1 + + if (isFullPathReplacement) { + return `/${newPathSegments.join("/")}` + } + + const updatedPathSegments = pathSegments.slice(1) + const updatedPath = `/${newPathSegments.concat(updatedPathSegments).join("/")}` + + return updatedPath +} + +export function LanguageSwitcher({ + currentLanguage, + isMobile = false, +}: LanguageSwitcherProps) { + return ( +
+ + + {isMobile ? ( + + + + {({ close }) => ( + + )} + + + + ) : ( + + + {({ close }) => ( + + )} + + + )} + +
+ ) +} + +function LanguageSwitcherContent({ + closeModal, + currentLanguage, + isMobile, +}: { + closeModal: () => void + currentLanguage: Lang + isMobile?: boolean +}) { + const intl = useIntl() + const pathname = usePathname() + + const urls: LanguageSwitcherData = { + da: { url: "/da/" }, + de: { url: "/de/" }, + en: { url: "/en/" }, + fi: { url: "/fi/" }, + no: { url: "/no/" }, + sv: { url: "/sv/" }, + } + + const urlKeys = (Object.keys(urls) as (keyof typeof urls)[]).sort((a, b) => { + return languages[a].localeCompare(languages[b]) + }) + + return ( +
+ {isMobile ? ( + <> + + +

+ {intl.formatMessage({ + defaultMessage: "Select your language", + })} +

+
+ + ) : null} + +
+ ) +} diff --git a/apps/partner-sas/components/LanguageSwitcher/languageSwitcher.module.css b/apps/partner-sas/components/LanguageSwitcher/languageSwitcher.module.css new file mode 100644 index 000000000..c531e4f3e --- /dev/null +++ b/apps/partner-sas/components/LanguageSwitcher/languageSwitcher.module.css @@ -0,0 +1,102 @@ +.languageSwitcher { + .triggerButton { + gap: var(--Space-x1); + padding: var(--Space-x1); + justify-content: flex-start; + width: 100%; + border: 0 none; + } + + .triggerText { + display: flex; + justify-content: space-between; + width: 100%; + color: var(--Text-sas-20); + } +} + +.languageSwitcherContent { + background: white; + gap: var(--Space-x3); + padding: 0 var(--Space-x2); + flex-direction: column; + display: flex; + align-items: flex-start; + + .arrowBack { + color: var(--TEMP-sas-40); + padding: var(--Space-x2) 0; + width: 100%; + justify-content: flex-start; + } + + ul { + list-style: none; + width: 100%; + } +} + +.languageSwitcherListItem .link { + padding: var(--Space-x1); + display: flex; + justify-content: space-between; + align-items: center; + border-radius: var(--Space-x1); +} + +.languageModalOverlay { + position: fixed; + width: 100%; + height: 100%; + z-index: 1000; +} + +.languageModal { + position: fixed; + top: calc(var(--main-menu-mobile-height) + var(--sitewide-alert-height)); + left: 0; + right: 0; + bottom: 0; + background: white; + z-index: 1001; + + .closeModal { + position: fixed; + top: var(--Space-x2); + right: var(--Space-x2); + background-color: transparent; + color: transparent; + } +} + +@media screen and (min-width: 768px) { + .languageSwitcher { + .triggerText { + color: white; + } + + .triggerButton { + color: white; + padding: 0; + } + + .triggerButton:hover { + text-decoration: none; + } + + .triggerButton[aria-expanded="true"] .chevron { + transform: rotate(180deg); + } + } + + .languageSwitcherContent { + min-width: 200px; + border-radius: var(--Space-x15); + padding: var(--Space-x2) var(--Space-x3); + box-shadow: 0 0 14px 6px rgba(0, 0, 0, 0.1); + } + + .chevron { + transition: 0.3s; + } +} diff --git a/apps/partner-sas/components/Menu/MobileMenu/index.tsx b/apps/partner-sas/components/Menu/MobileMenu/index.tsx new file mode 100644 index 000000000..e460415f4 --- /dev/null +++ b/apps/partner-sas/components/Menu/MobileMenu/index.tsx @@ -0,0 +1,49 @@ +"use client" + +import { useState } from "react" +import { Dialog, Modal } from "react-aria-components" +import { useIntl } from "react-intl" + +import { Button } from "@scandic-hotels/design-system/Button" + +import { UserMenu } from "../UserMenu" + +import styles from "./mobile-menu.module.css" + +export function MobileMenu({ children }: React.PropsWithChildren) { + const intl = useIntl() + + const closeMsg = intl.formatMessage({ + defaultMessage: "Close menu", + }) + const openMsg = intl.formatMessage({ + defaultMessage: "Open menu", + }) + + const [isOpen, setIsOpen] = useState(false) + + return ( +
+ + + + + {children} + + +
+ ) +} diff --git a/apps/partner-sas/components/Menu/MobileMenu/mobile-menu.module.css b/apps/partner-sas/components/Menu/MobileMenu/mobile-menu.module.css new file mode 100644 index 000000000..06dcf7dd3 --- /dev/null +++ b/apps/partner-sas/components/Menu/MobileMenu/mobile-menu.module.css @@ -0,0 +1,104 @@ +.mobileMenu { + display: flex; + align-items: center; + gap: var(--Space-x1); +} + +.mobileMenu .avatar { + background-color: white; + span { + color: var(--TEMP-sas-20); + } +} + +.hamburger { + background-color: transparent; + border: none; + cursor: pointer; + justify-self: flex-start; + padding: 19px 8px 18px; + user-select: none; +} + +.bar, +.bar::after, +.bar::before { + background: white; + border-radius: 2.3px; + display: block; + height: 3px; + position: relative; + transition: all 0.3s; + width: 32px; +} + +.bar::after, +.bar::before { + content: ""; + left: 0; + position: absolute; + transform-origin: 2.286px center; +} + +.bar::after { + top: -8px; +} + +.bar::before { + top: 8px; +} + +.isExpanded .bar { + background: transparent; +} + +.isExpanded .bar::after, +.isExpanded .bar::before { + top: 0; + transform-origin: 50% 50%; + width: 32px; +} + +.isExpanded .bar::after { + transform: rotate(-45deg); +} + +.isExpanded .bar::before { + transform: rotate(45deg); +} + +.modal { + position: fixed; + top: calc(var(--main-menu-mobile-height) + var(--sitewide-alert-height)); + right: auto; + bottom: 0; + width: 100%; + background-color: var(--Base-Surface-Primary-light-Normal); + transition: right 0.3s; + z-index: var(--menu-overlay-z-index); +} + +.dialog { + height: 100%; + overflow-y: auto; + display: flex; + align-content: space-between; + padding: var(--Space-x3) var(--Space-x2) var(--Space-x4); + flex-direction: column; + gap: var(--Space-x2); +} + +.footer { + background-color: var(--Base-Surface-Subtle-Normal); + padding: var(--Space-x4) var(--Space-x2); + display: grid; + gap: var(--Space-x2); +} + +@media screen and (min-width: 768px) { + .avatar, + .hamburger, + .modal { + display: none; + } +} diff --git a/apps/partner-sas/components/Menu/NavigationMenu/index.tsx b/apps/partner-sas/components/Menu/NavigationMenu/index.tsx new file mode 100644 index 000000000..dd98ae81e --- /dev/null +++ b/apps/partner-sas/components/Menu/NavigationMenu/index.tsx @@ -0,0 +1,48 @@ +"use client" + +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import Link from "@scandic-hotels/design-system/Link" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { LanguageSwitcher } from "@/components/LanguageSwitcher" +import useLang from "@/hooks/useLang" + +import { UserMenu } from "../UserMenu" + +import styles from "./navigation-menu.module.css" + +export function NavigationMenu({ isMobile = false }: { isMobile?: boolean }) { + const intl = useIntl() + const lang = useLang() + + return ( +
+ + + {isMobile ? null : ( + + )} + {intl.formatMessage({ defaultMessage: "Contact us" })} + + + + + + {!isMobile && } +
+ ) +} diff --git a/apps/partner-sas/components/Menu/NavigationMenu/navigation-menu.module.css b/apps/partner-sas/components/Menu/NavigationMenu/navigation-menu.module.css new file mode 100644 index 000000000..96994c938 --- /dev/null +++ b/apps/partner-sas/components/Menu/NavigationMenu/navigation-menu.module.css @@ -0,0 +1,50 @@ +.menuItems, +.menuItem { + display: flex; + direction: column; + align-items: center; +} + +.desktopMenu { + display: none; +} + +.menuItem { + gap: var(--Space-x1); + padding: var(--Space-x1); +} + +.menuDivider { + margin: var(--Space-x2) 0; +} + +.contactLink { + color: var(--Text-sas-20); +} + +@media screen and (min-width: 768px) { + .menuItems, + .menuItem { + display: flex; + align-items: center; + padding: 0; + } + + .menuItems { + gap: var(--Space-x3); + } + + .mobileMenu { + display: none; + } + + .desktopMenu { + display: flex; + gap: var(--Space-x3); + align-items: center; + } + + .contactLink { + color: white; + } +} diff --git a/apps/partner-sas/components/Menu/UserMenu/index.tsx b/apps/partner-sas/components/Menu/UserMenu/index.tsx new file mode 100644 index 000000000..30a560a63 --- /dev/null +++ b/apps/partner-sas/components/Menu/UserMenu/index.tsx @@ -0,0 +1,201 @@ +"use client" + +import { useSession } from "next-auth/react" +import { useEffect, useState } from "react" +import { + Dialog, + DialogTrigger, + Modal, + ModalOverlay, + Popover, +} from "react-aria-components" +import { useIntl } from "react-intl" + +import { Avatar } from "@scandic-hotels/design-system/Avatar" +import { Button } from "@scandic-hotels/design-system/Button" +import { Divider } from "@scandic-hotels/design-system/Divider" +import { IconButton } from "@scandic-hotels/design-system/IconButton" +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import Link from "@scandic-hotels/design-system/Link" +import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer" +import { Typography } from "@scandic-hotels/design-system/Typography" +import { trpc } from "@scandic-hotels/trpc/client" + +import useLang from "@/hooks/useLang" + +import { getInitials } from "../utils" + +import styles from "./user-menu.module.css" + +export function UserMenu({ isMobile = false }: { isMobile?: boolean }) { + const intl = useIntl() + const lang = useLang() + const session = useSession() + const [loginLink, setLoginLink] = useState(`/${lang}/login`) + + const { + data: profileData, + isLoading, + isSuccess, + isError, + } = trpc.partner.sas.getEuroBonusProfile.useQuery(undefined, { + enabled: session.status === "authenticated", + }) + + useEffect(() => { + setLoginLink(`/${lang}/login?redirectTo=${window?.location.href}`) + }, [lang, setLoginLink]) + + const firstName = profileData?.firstName + const lastName = profileData?.lastName + + return ( +
+ {(session.status === "loading" || isLoading) && + (isMobile ? ( + + ) : ( + + ))} + {(session.status === "unauthenticated" || isError) && ( + + + {isMobile ? null : ( + + {intl.formatMessage({ defaultMessage: "Login" })} + + )} + + )} + {session.status === "authenticated" && isSuccess && profileData && ( +
+ + + {isMobile ? ( + + + + {({ close }) => ( + <> + + + + + + )} + + + + ) : ( + + + + + + )} + +
+ )} +
+ ) +} + +function UserMenuContent({ + firstName, + lastName, + points, + isMobile, +}: { + firstName?: string + lastName?: string + points?: number + isMobile?: boolean +}) { + const intl = useIntl() + const lang = useLang() + + return ( + <> +
+ {isMobile && ( + +

+ {intl.formatMessage( + { defaultMessage: `Hi {fName} {lName}!` }, + { fName: firstName, lName: lastName } + )} +

+
+ )} +

+ + {intl.formatMessage({ defaultMessage: "EB Points" })} + + + {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} + {"ยท"} + + + + {intl.formatMessage( + { + defaultMessage: "{points} points", + }, + { + points: points, + } + )} + + +

+
+ + + {intl.formatMessage({ defaultMessage: "Logout" })} + + + ) +} diff --git a/apps/partner-sas/components/Menu/UserMenu/user-menu.module.css b/apps/partner-sas/components/Menu/UserMenu/user-menu.module.css new file mode 100644 index 000000000..66a98c24b --- /dev/null +++ b/apps/partner-sas/components/Menu/UserMenu/user-menu.module.css @@ -0,0 +1,107 @@ +.userMenu { + position: relative; + + .userName { + display: flex; + align-items: center; + gap: var(--Space-x1); + cursor: pointer; + color: white; + padding: 0; + + &:hover { + color: white; + text-decoration: none; + } + } +} + +.userMenu .avatar { + background-color: white; + color: var(--TEMP-sas-default); + + span { + color: currentColor; + } +} + +.userDetailsContainer { + padding: var(--Space-x1); + color: var(--Text-sas-20); +} + +.logoutLink, +.loginLink { + color: var(--Text-sas-20); + font-weight: 400; + text-decoration: none; +} + +.modal { + position: fixed; + top: calc(var(--main-menu-mobile-height) + var(--sitewide-alert-height)); + right: auto; + bottom: 0; + width: 100%; + background-color: var(--Base-Surface-Primary-light-Normal); + transition: right 0.3s; + z-index: var(--menu-overlay-z-index); +} + +.dialog { + height: 100%; + overflow-y: auto; + display: flex; + align-content: space-between; + padding: var(--Space-x3) var(--Space-x2) var(--Space-x4); + flex-direction: column; + gap: var(--Space-x2); + + .closeModal { + position: fixed; + top: var(--Space-x2); + right: var(--Space-x2); + background-color: var(--TEMP-sas-default); + padding: var(--Space-x05); + color: white; + + &:hover, + &:hover:not(:disabled) { + background-color: var(--TEMP-sas-default); + } + } +} + +.pointsDetails { + display: flex; + gap: var(--Space-x1); + align-items: center; +} + +@media screen and (min-width: 768px) { + .userMenu { + padding: 0; + color: white; + } + + .userDetailsContainer { + background-color: white; + border-radius: 12px; + padding: var(--Space-x2) var(--Space-x3); + box-shadow: 0 0 14px 6px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + gap: var(--Space-x2); + } + + .loginLink { + color: white; + display: flex; + align-items: center; + gap: var(--Space-x1); + + &:hover { + color: white; + } + } +} diff --git a/apps/partner-sas/components/Menu/index.tsx b/apps/partner-sas/components/Menu/index.tsx new file mode 100644 index 000000000..1e9bb37ba --- /dev/null +++ b/apps/partner-sas/components/Menu/index.tsx @@ -0,0 +1,45 @@ +"use client" + +import { useEffect, useState } from "react" +import { useMediaQuery } from "usehooks-ts" + +import Image from "@scandic-hotels/design-system/Image" +import Link from "@scandic-hotels/design-system/Link" + +import useLang from "@/hooks/useLang" + +import { MobileMenu } from "./MobileMenu" +import { NavigationMenu } from "./NavigationMenu" + +import styles from "./menu.module.css" + +export function Menu() { + const lang = useLang() + const checkIfMobile = useMediaQuery("(max-width: 767px)") + const [isMobile, setIsMobile] = useState(false) + + useEffect(() => { + setIsMobile(checkIfMobile) + }, [checkIfMobile]) + return ( +
+ + SAS logotype + + {isMobile ? ( + + + + ) : ( + + )} +
+ ) +} diff --git a/apps/partner-sas/components/Menu/menu.module.css b/apps/partner-sas/components/Menu/menu.module.css new file mode 100644 index 000000000..52a803568 --- /dev/null +++ b/apps/partner-sas/components/Menu/menu.module.css @@ -0,0 +1,18 @@ +.container { + background-color: var(--TEMP-sas-default); + color: white; + display: flex; + align-items: center; + padding: var(--Space-x2); + justify-content: space-between; + + @media screen and (min-width: 768px) { + padding: 20px 40px; + } +} + +.logo { + height: auto; + width: 90px; + display: block; +} diff --git a/apps/partner-sas/components/Menu/utils.ts b/apps/partner-sas/components/Menu/utils.ts new file mode 100644 index 000000000..8566dbee3 --- /dev/null +++ b/apps/partner-sas/components/Menu/utils.ts @@ -0,0 +1,6 @@ +export function getInitials(firstName: string, lastName: string) { + if (!firstName || !lastName) return null + const firstInitial = firstName.charAt(0).toUpperCase() + const lastInitial = lastName.charAt(0).toUpperCase() + return `${firstInitial}${lastInitial}` +} diff --git a/apps/partner-sas/globals.css b/apps/partner-sas/globals.css index 554110c00..8cce1f8c6 100644 --- a/apps/partner-sas/globals.css +++ b/apps/partner-sas/globals.css @@ -15,7 +15,7 @@ ); --sitewide-alert-height: 0px; /* Will be overridden when a sitewide alert is visible */ - --main-menu-mobile-height: 73px; + --main-menu-mobile-height: 103px; --main-menu-desktop-height: 125px; --booking-widget-mobile-height: 75px; --booking-widget-tablet-height: 150px; @@ -41,6 +41,7 @@ --TEMP-sas-default: #000099; --TEMP-sas-20: #00175c; --TEMP-sas-40: #0030c2; + --Text-sas-20: #333; @supports (interpolate-size: allow-keywords) { interpolate-size: allow-keywords; diff --git a/apps/partner-sas/package.json b/apps/partner-sas/package.json index 94dbcd485..5e973c1dd 100644 --- a/apps/partner-sas/package.json +++ b/apps/partner-sas/package.json @@ -31,9 +31,11 @@ "next": "15.3.4", "next-auth": "5.0.0-beta.29", "react": "^19.0.0", + "react-aria-components": "^1.8.0", "react-dom": "^19.0.0", "react-intl": "^7.1.11", - "server-only": "^0.0.1" + "server-only": "^0.0.1", + "usehooks-ts": "3.1.1" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", diff --git a/apps/scandic-web/components/LanguageSwitcher/LanguageSwitcherContent/index.tsx b/apps/scandic-web/components/LanguageSwitcher/LanguageSwitcherContent/index.tsx index bf8fbb55c..fe76027c7 100644 --- a/apps/scandic-web/components/LanguageSwitcher/LanguageSwitcherContent/index.tsx +++ b/apps/scandic-web/components/LanguageSwitcher/LanguageSwitcherContent/index.tsx @@ -3,13 +3,12 @@ import { usePathname } from "next/navigation" import { useIntl } from "react-intl" +import { languages } from "@scandic-hotels/common/constants/language" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import Link from "@scandic-hotels/design-system/Link" import Subtitle from "@scandic-hotels/design-system/Subtitle" import { Typography } from "@scandic-hotels/design-system/Typography" -import { languages } from "@/constants/languages" - import useLang from "@/hooks/useLang" import { replaceUrlPart } from "./utils" diff --git a/apps/scandic-web/components/LanguageSwitcher/index.tsx b/apps/scandic-web/components/LanguageSwitcher/index.tsx index 850b7f864..cc44a2d1c 100644 --- a/apps/scandic-web/components/LanguageSwitcher/index.tsx +++ b/apps/scandic-web/components/LanguageSwitcher/index.tsx @@ -5,13 +5,13 @@ import { useRef } from "react" import FocusLock from "react-focus-lock" import { useIntl } from "react-intl" +import { languages } from "@scandic-hotels/common/constants/language" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer" import { Typography } from "@scandic-hotels/design-system/Typography" import { trackLanguageSwitchClick } from "@scandic-hotels/tracking/navigation" import { trpc } from "@scandic-hotels/trpc/client" -import { languages } from "@/constants/languages" import useDropdownStore from "@/stores/main-menu" import useClickOutside from "@/hooks/useClickOutside" diff --git a/apps/scandic-web/components/MyPages/Profile/index.tsx b/apps/scandic-web/components/MyPages/Profile/index.tsx index 0b451fc8f..b110ba3ac 100644 --- a/apps/scandic-web/components/MyPages/Profile/index.tsx +++ b/apps/scandic-web/components/MyPages/Profile/index.tsx @@ -1,4 +1,4 @@ -import { Lang } from "@scandic-hotels/common/constants/language" +import { Lang, languages } from "@scandic-hotels/common/constants/language" import { profileEdit } from "@scandic-hotels/common/constants/routes/myPages" import { isValidLang } from "@scandic-hotels/common/utils/languages" import ButtonLink from "@scandic-hotels/design-system/ButtonLink" @@ -7,7 +7,6 @@ import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { Typography } from "@scandic-hotels/design-system/Typography" import { countriesMap } from "@scandic-hotels/trpc/constants/countries" -import { languages } from "@/constants/languages" import { getProfile } from "@/lib/trpc/memoizedRequests" import CommunicationSlot from "@/components/MyPages/Profile/Communication" diff --git a/apps/scandic-web/constants/languages.ts b/apps/scandic-web/constants/languages.ts index 729a1e3b9..376e01a4a 100644 --- a/apps/scandic-web/constants/languages.ts +++ b/apps/scandic-web/constants/languages.ts @@ -1,14 +1,6 @@ -import { Lang } from "@scandic-hotels/common/constants/language" import { ApiLang } from "@scandic-hotels/trpc/constants/apiLang" -export const languages: Record = { - [Lang.da]: "Dansk", - [Lang.de]: "Deutsch", - [Lang.en]: "English", - [Lang.fi]: "Suomi", - [Lang.no]: "Norsk", - [Lang.sv]: "Svenska", -} +import type { Lang } from "@scandic-hotels/common/constants/language" const languageSelect = [ { label: "Danish", value: ApiLang.Da }, diff --git a/apps/scandic-web/types/components/languageSwitcher/languageSwitcher.ts b/apps/scandic-web/types/components/languageSwitcher/languageSwitcher.ts index 0be8a005d..6ef1893ff 100644 --- a/apps/scandic-web/types/components/languageSwitcher/languageSwitcher.ts +++ b/apps/scandic-web/types/components/languageSwitcher/languageSwitcher.ts @@ -1,4 +1,4 @@ -import type { LanguageSwitcherData } from "@scandic-hotels/trpc/types/languageSwitcher" +import type { LanguageSwitcherData } from "@scandic-hotels/common/constants/language" import type { ReactElement } from "react" export enum LanguageSwitcherTypesEnum { diff --git a/packages/common/constants/language.ts b/packages/common/constants/language.ts index 97732c943..f1facacd0 100644 --- a/packages/common/constants/language.ts +++ b/packages/common/constants/language.ts @@ -6,3 +6,26 @@ export enum Lang { no = "no", sv = "sv", } + +export const languages: Record = { + [Lang.da]: "Dansk", + [Lang.de]: "Deutsch", + [Lang.en]: "English", + [Lang.fi]: "Suomi", + [Lang.no]: "Norsk", + [Lang.sv]: "Svenska", +} + +type LanguageResult = { + url: string + isExternal?: boolean +} + +export type LanguageSwitcherData = { + da?: LanguageResult + de?: LanguageResult + en?: LanguageResult + fi?: LanguageResult + no?: LanguageResult + sv?: LanguageResult +} diff --git a/packages/trpc/lib/routers/contentstack/languageSwitcher/query.ts b/packages/trpc/lib/routers/contentstack/languageSwitcher/query.ts index 36748d286..6fc9ed818 100644 --- a/packages/trpc/lib/routers/contentstack/languageSwitcher/query.ts +++ b/packages/trpc/lib/routers/contentstack/languageSwitcher/query.ts @@ -5,7 +5,7 @@ import { getNonContentstackUrls } from "../metadata/output" import { getLanguageSwitcherInput } from "./input" import { getUrlsOfAllLanguages } from "./utils" -import type { LanguageSwitcherData } from "../../../types/languageSwitcher" +import type { LanguageSwitcherData } from "@scandic-hotels/common/constants/language" export const languageSwitcherQueryRouter = router({ get: publicProcedure diff --git a/packages/trpc/lib/routers/contentstack/languageSwitcher/utils.ts b/packages/trpc/lib/routers/contentstack/languageSwitcher/utils.ts index 0750c40ec..cb669bed5 100644 --- a/packages/trpc/lib/routers/contentstack/languageSwitcher/utils.ts +++ b/packages/trpc/lib/routers/contentstack/languageSwitcher/utils.ts @@ -1,4 +1,7 @@ -import { Lang } from "@scandic-hotels/common/constants/language" +import { + Lang, + type LanguageSwitcherData, +} from "@scandic-hotels/common/constants/language" import { createLogger } from "@scandic-hotels/common/logger/createLogger" import { createCounter } from "@scandic-hotels/common/telemetry" import { removeTrailingSlash } from "@scandic-hotels/common/utils/url" @@ -57,10 +60,7 @@ import { import { generateTag } from "../../../utils/generateTag" import { validateLanguageSwitcherData } from "./output" -import type { - LanguageSwitcherData, - LanguageSwitcherQueryDataRaw, -} from "../../../types/languageSwitcher" +import type { LanguageSwitcherQueryDataRaw } from "../../../types/languageSwitcher" export const languageSwitcherAffix = "languageSwitcher" diff --git a/packages/trpc/lib/routers/contentstack/metadata/output.ts b/packages/trpc/lib/routers/contentstack/metadata/output.ts index 58bbe79b5..b9a756b4c 100644 --- a/packages/trpc/lib/routers/contentstack/metadata/output.ts +++ b/packages/trpc/lib/routers/contentstack/metadata/output.ts @@ -12,11 +12,12 @@ import { RTETypeEnum } from "../../../types/RTEenums" import { destinationFilterSchema } from "../schemas/destinationFilters" import { systemSchema } from "../schemas/system" -import type { Lang } from "@scandic-hotels/common/constants/language" +import type { + Lang, + LanguageSwitcherData, +} from "@scandic-hotels/common/constants/language" import type { ImageVaultAsset } from "@scandic-hotels/common/utils/imageVault" -import type { LanguageSwitcherData } from "../../../types/languageSwitcher" - const metaDataJsonSchema = z.object({ children: z.array( z.object({ diff --git a/packages/trpc/lib/routers/contentstack/metadata/query.ts b/packages/trpc/lib/routers/contentstack/metadata/query.ts index 383ce1da8..db3131459 100644 --- a/packages/trpc/lib/routers/contentstack/metadata/query.ts +++ b/packages/trpc/lib/routers/contentstack/metadata/query.ts @@ -26,9 +26,9 @@ import { getMetadataInput } from "./input" import { getNonContentstackUrls, rawMetadataSchema } from "./output" import { affix, getCityData, getCountryData } from "./utils" +import type { LanguageSwitcherData } from "@scandic-hotels/common/constants/language" import type { Lang } from "@scandic-hotels/common/constants/language" -import type { LanguageSwitcherData } from "../../../types/languageSwitcher" import type { RawMetadataSchema } from "./output" const fetchMetadata = cache(async function fetchMemoizedMetadata( diff --git a/packages/trpc/lib/types/languageSwitcher.ts b/packages/trpc/lib/types/languageSwitcher.ts index 90f033f35..62eb77b9c 100644 --- a/packages/trpc/lib/types/languageSwitcher.ts +++ b/packages/trpc/lib/types/languageSwitcher.ts @@ -1,17 +1,3 @@ -type CurrentLanguageResult = { - url: string - isExternal?: boolean -} - -export type LanguageSwitcherData = { - da?: CurrentLanguageResult - de?: CurrentLanguageResult - en?: CurrentLanguageResult - fi?: CurrentLanguageResult - no?: CurrentLanguageResult - sv?: CurrentLanguageResult -} - type LanguageResult = { web?: { original_url?: string | null diff --git a/yarn.lock b/yarn.lock index f5c7f6218..c49d883c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6054,10 +6054,12 @@ __metadata: next: "npm:15.3.4" next-auth: "npm:5.0.0-beta.29" react: "npm:^19.0.0" + react-aria-components: "npm:^1.8.0" react-dom: "npm:^19.0.0" react-intl: "npm:^7.1.11" server-only: "npm:^0.0.1" typescript: "npm:5.8.3" + usehooks-ts: "npm:3.1.1" vitest: "npm:^3.2.4" languageName: unknown linkType: soft