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 (
<>
-
- {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 ? (
+
+
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ )
+}
+
+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}
+
+ {urlKeys.map((key) => {
+ const url = urls[key]?.url
+ const isActive = currentLanguage === key
+
+ if (url) {
+ return (
+ -
+
+
+ {languages[key]}
+ {isActive ? (
+
+ ) : 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 (
+
+
+
+
+
+
+
+ )
+}
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 ? (
+
+
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ )}
+
+ )
+}
+
+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 (
+
+
+
+
+ {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