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
This commit is contained in:
@@ -1,78 +1,17 @@
|
|||||||
"use client"
|
import { Menu } from "../Menu"
|
||||||
|
|
||||||
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 { PoweredByScandic } from "../PoweredByScandic/PoweredByScandic"
|
import { PoweredByScandic } from "../PoweredByScandic/PoweredByScandic"
|
||||||
|
|
||||||
import styles from "./header.module.css"
|
import styles from "./header.module.css"
|
||||||
|
|
||||||
export function Header() {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<Image
|
<Menu />
|
||||||
alt="SAS logotype"
|
<div className={styles.poweredBy}>
|
||||||
className={styles.logo}
|
<PoweredByScandic />
|
||||||
src="/_static/img/sas-logotype-white.svg"
|
</div>
|
||||||
height={32}
|
|
||||||
width={90}
|
|
||||||
sizes="100vw"
|
|
||||||
/>
|
|
||||||
{session.status === "loading" && (
|
|
||||||
<SkeletonShimmer width={"12ch"} height={"1ch"} />
|
|
||||||
)}
|
|
||||||
{session.status === "unauthenticated" && (
|
|
||||||
/** For some reason it complains about RSC-payload if using <Link /> */
|
|
||||||
<a href={`/${lang}/login?redirectTo=${window?.location.href}`}>
|
|
||||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
|
||||||
{"Login here"}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{session.status === "authenticated" && (
|
|
||||||
<div>
|
|
||||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
|
||||||
<span>
|
|
||||||
{session.data?.user && <>{session.data.user.email}</>}
|
|
||||||
</span>
|
|
||||||
</Typography>
|
|
||||||
{isLoading && <SkeletonShimmer width={"6ch"} height={"1ch"} />}
|
|
||||||
{isSuccess && profileData && (
|
|
||||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
|
||||||
<span>
|
|
||||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
|
||||||
{profileData.points.total} Points
|
|
||||||
</span>
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
<Link color={"white"} href={`/${lang}/logout`} prefetch={false}>
|
|
||||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
|
||||||
{"Logout"}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</header>
|
</header>
|
||||||
<div className={styles.poweredBy}>
|
|
||||||
<PoweredByScandic />
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
.poweredBy {
|
||||||
padding: 6px 16px;
|
padding: 6px 16px;
|
||||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
|
|||||||
204
apps/partner-sas/components/LanguageSwitcher/index.tsx
Normal file
204
apps/partner-sas/components/LanguageSwitcher/index.tsx
Normal file
@@ -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 (
|
||||||
|
<div className={styles.languageSwitcher}>
|
||||||
|
<DialogTrigger>
|
||||||
|
<Button className={styles.triggerButton} variant={"Text"} wrapping>
|
||||||
|
{isMobile ? null : (
|
||||||
|
<MaterialIcon
|
||||||
|
icon="globe"
|
||||||
|
size={16}
|
||||||
|
color={"Icon/Inverted"}
|
||||||
|
className={styles.globeIcon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Typography
|
||||||
|
variant={
|
||||||
|
isMobile
|
||||||
|
? "Body/Paragraph/mdRegular"
|
||||||
|
: "Body/Supporting text (caption)/smRegular"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className={styles.triggerText}>
|
||||||
|
{languages[currentLanguage]}
|
||||||
|
<MaterialIcon
|
||||||
|
className={styles.chevron}
|
||||||
|
icon="keyboard_arrow_down"
|
||||||
|
size={isMobile ? 24 : 20}
|
||||||
|
color={isMobile ? "Icon/Default" : "Icon/Inverted"}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
</Button>
|
||||||
|
{isMobile ? (
|
||||||
|
<ModalOverlay isDismissable className={styles.languageModalOverlay}>
|
||||||
|
<Modal className={styles.languageModal}>
|
||||||
|
<Dialog>
|
||||||
|
{({ close }) => (
|
||||||
|
<LanguageSwitcherContent
|
||||||
|
closeModal={close}
|
||||||
|
isMobile={true}
|
||||||
|
currentLanguage={currentLanguage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</Modal>
|
||||||
|
</ModalOverlay>
|
||||||
|
) : (
|
||||||
|
<Popover offset={28} className={styles.languageSwitcherPopover}>
|
||||||
|
<Dialog>
|
||||||
|
{({ close }) => (
|
||||||
|
<LanguageSwitcherContent
|
||||||
|
closeModal={close}
|
||||||
|
isMobile={false}
|
||||||
|
currentLanguage={currentLanguage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
</DialogTrigger>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className={styles.languageSwitcherContent}>
|
||||||
|
{isMobile ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant={"Text"}
|
||||||
|
size={"Medium"}
|
||||||
|
onPress={closeModal}
|
||||||
|
className={styles.arrowBack}
|
||||||
|
>
|
||||||
|
<MaterialIcon
|
||||||
|
icon="chevron_left"
|
||||||
|
size={28}
|
||||||
|
className={styles.arrowBackIcon}
|
||||||
|
color={"CurrentColor"}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
<Typography variant={"Title/Subtitle/md"}>
|
||||||
|
<h3 className={styles.title}>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Select your language",
|
||||||
|
})}
|
||||||
|
</h3>
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
<ul className={styles.languageSwitcherListContainer}>
|
||||||
|
{urlKeys.map((key) => {
|
||||||
|
const url = urls[key]?.url
|
||||||
|
const isActive = currentLanguage === key
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
return (
|
||||||
|
<li key={key} className={styles.languageSwitcherListItem}>
|
||||||
|
<Typography
|
||||||
|
variant={
|
||||||
|
isActive
|
||||||
|
? "Body/Paragraph/mdBold"
|
||||||
|
: "Body/Paragraph/mdRegular"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
className={styles.link}
|
||||||
|
href={replaceUrlPart(pathname, url)}
|
||||||
|
variant="languageSwitcher"
|
||||||
|
keepSearchParams
|
||||||
|
>
|
||||||
|
{languages[key]}
|
||||||
|
{isActive ? (
|
||||||
|
<MaterialIcon
|
||||||
|
icon="check"
|
||||||
|
color="Icon/Interactive/Default"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Link>
|
||||||
|
</Typography>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
49
apps/partner-sas/components/Menu/MobileMenu/index.tsx
Normal file
49
apps/partner-sas/components/Menu/MobileMenu/index.tsx
Normal file
@@ -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 (
|
||||||
|
<div className={styles.mobileMenu}>
|
||||||
|
<UserMenu isMobile={true} />
|
||||||
|
<Button
|
||||||
|
variant={"Text"}
|
||||||
|
type="button"
|
||||||
|
className={`${styles.hamburger} ${isOpen ? styles.isExpanded : ""}`}
|
||||||
|
aria-label={isOpen ? closeMsg : openMsg}
|
||||||
|
onPress={() => setIsOpen(!isOpen)}
|
||||||
|
>
|
||||||
|
<span className={styles.bar} />
|
||||||
|
</Button>
|
||||||
|
<Modal className={styles.modal} isOpen={isOpen}>
|
||||||
|
<Dialog
|
||||||
|
className={styles.dialog}
|
||||||
|
aria-label={intl.formatMessage({
|
||||||
|
defaultMessage: "Menu",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Dialog>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
48
apps/partner-sas/components/Menu/NavigationMenu/index.tsx
Normal file
48
apps/partner-sas/components/Menu/NavigationMenu/index.tsx
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
className={`styles.menuItems ${isMobile ? styles.mobileMenu : styles.desktopMenu}`}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant={
|
||||||
|
isMobile
|
||||||
|
? "Body/Paragraph/mdRegular"
|
||||||
|
: "Body/Supporting text (caption)/smRegular"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href="#"
|
||||||
|
color={isMobile ? "none" : "white"}
|
||||||
|
className={`${styles.menuItem} ${styles.contactLink}`}
|
||||||
|
>
|
||||||
|
{isMobile ? null : (
|
||||||
|
<MaterialIcon icon="call" size={16} color={"CurrentColor"} />
|
||||||
|
)}
|
||||||
|
{intl.formatMessage({ defaultMessage: "Contact us" })}
|
||||||
|
</Link>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<LanguageSwitcher currentLanguage={lang} isMobile={isMobile} />
|
||||||
|
|
||||||
|
{!isMobile && <UserMenu isMobile={isMobile} />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
201
apps/partner-sas/components/Menu/UserMenu/index.tsx
Normal file
201
apps/partner-sas/components/Menu/UserMenu/index.tsx
Normal file
@@ -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 (
|
||||||
|
<div className={styles.userMenu}>
|
||||||
|
{(session.status === "loading" || isLoading) &&
|
||||||
|
(isMobile ? (
|
||||||
|
<SkeletonShimmer width={"4ch"} height={"4ch"} />
|
||||||
|
) : (
|
||||||
|
<SkeletonShimmer width={"12ch"} height={"1ch"} />
|
||||||
|
))}
|
||||||
|
{(session.status === "unauthenticated" || isError) && (
|
||||||
|
<a href={loginLink} className={styles.loginLink}>
|
||||||
|
<Avatar className={styles.avatar} />
|
||||||
|
{isMobile ? null : (
|
||||||
|
<Typography
|
||||||
|
variant={
|
||||||
|
isMobile
|
||||||
|
? "Body/Paragraph/mdRegular"
|
||||||
|
: "Body/Supporting text (caption)/smRegular"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>{intl.formatMessage({ defaultMessage: "Login" })}</span>
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{session.status === "authenticated" && isSuccess && profileData && (
|
||||||
|
<div className={styles.loggedInMenu}>
|
||||||
|
<DialogTrigger>
|
||||||
|
<Button className={styles.userName} variant={"Text"}>
|
||||||
|
<Avatar
|
||||||
|
className={styles.avatar}
|
||||||
|
initials={getInitials(
|
||||||
|
firstName ?? session.data?.user?.name ?? "",
|
||||||
|
lastName ?? session.data?.user?.name ?? ""
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{isMobile ? null : (
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
|
<span>
|
||||||
|
{firstName ?? session.data?.user?.email}
|
||||||
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||||
|
{` ${lastName}`}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
{isMobile ? (
|
||||||
|
<ModalOverlay isDismissable>
|
||||||
|
<Modal className={styles.modal}>
|
||||||
|
<Dialog className={styles.dialog}>
|
||||||
|
{({ close }) => (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
className={styles.closeModal}
|
||||||
|
onPress={close}
|
||||||
|
>
|
||||||
|
<MaterialIcon
|
||||||
|
icon={"close"}
|
||||||
|
size={32}
|
||||||
|
color={"CurrentColor"}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
<UserMenuContent
|
||||||
|
firstName={firstName}
|
||||||
|
lastName={lastName}
|
||||||
|
points={profileData.points.total}
|
||||||
|
isMobile={true}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</Modal>
|
||||||
|
</ModalOverlay>
|
||||||
|
) : (
|
||||||
|
<Popover className={styles.userDetailsPopover} offset={20}>
|
||||||
|
<Dialog className={styles.userDetailsContainer}>
|
||||||
|
<UserMenuContent
|
||||||
|
firstName={firstName}
|
||||||
|
lastName={lastName}
|
||||||
|
points={profileData.points.total}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
</DialogTrigger>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserMenuContent({
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
points,
|
||||||
|
isMobile,
|
||||||
|
}: {
|
||||||
|
firstName?: string
|
||||||
|
lastName?: string
|
||||||
|
points?: number
|
||||||
|
isMobile?: boolean
|
||||||
|
}) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const lang = useLang()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
{isMobile && (
|
||||||
|
<Typography variant={"Title/Subtitle/md"}>
|
||||||
|
<h3>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ defaultMessage: `Hi {fName} {lName}!` },
|
||||||
|
{ fName: firstName, lName: lastName }
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<p className={styles.pointsDetails}>
|
||||||
|
<Typography variant="Title/Overline/sm">
|
||||||
|
<span>{intl.formatMessage({ defaultMessage: "EB Points" })}</span>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Title/Overline/sm">
|
||||||
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||||
|
<span>{"·"}</span>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "{points} points",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
points: points,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Divider className={styles.menuDivider} />
|
||||||
|
<Link
|
||||||
|
href={`/${lang}/logout`}
|
||||||
|
prefetch={false}
|
||||||
|
className={styles.logoutLink}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ defaultMessage: "Logout" })}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
107
apps/partner-sas/components/Menu/UserMenu/user-menu.module.css
Normal file
107
apps/partner-sas/components/Menu/UserMenu/user-menu.module.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
apps/partner-sas/components/Menu/index.tsx
Normal file
45
apps/partner-sas/components/Menu/index.tsx
Normal file
@@ -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 (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<Link href={`/${lang}`}>
|
||||||
|
<Image
|
||||||
|
alt="SAS logotype"
|
||||||
|
className={styles.logo}
|
||||||
|
src="/_static/img/sas-logotype-white.svg"
|
||||||
|
height={32}
|
||||||
|
width={90}
|
||||||
|
sizes="100vw"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
{isMobile ? (
|
||||||
|
<MobileMenu>
|
||||||
|
<NavigationMenu isMobile={true} />
|
||||||
|
</MobileMenu>
|
||||||
|
) : (
|
||||||
|
<NavigationMenu isMobile={false} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
18
apps/partner-sas/components/Menu/menu.module.css
Normal file
18
apps/partner-sas/components/Menu/menu.module.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
6
apps/partner-sas/components/Menu/utils.ts
Normal file
6
apps/partner-sas/components/Menu/utils.ts
Normal file
@@ -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}`
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
--sitewide-alert-height: 0px; /* Will be overridden when a sitewide alert is visible */
|
--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;
|
--main-menu-desktop-height: 125px;
|
||||||
--booking-widget-mobile-height: 75px;
|
--booking-widget-mobile-height: 75px;
|
||||||
--booking-widget-tablet-height: 150px;
|
--booking-widget-tablet-height: 150px;
|
||||||
@@ -41,6 +41,7 @@
|
|||||||
--TEMP-sas-default: #000099;
|
--TEMP-sas-default: #000099;
|
||||||
--TEMP-sas-20: #00175c;
|
--TEMP-sas-20: #00175c;
|
||||||
--TEMP-sas-40: #0030c2;
|
--TEMP-sas-40: #0030c2;
|
||||||
|
--Text-sas-20: #333;
|
||||||
|
|
||||||
@supports (interpolate-size: allow-keywords) {
|
@supports (interpolate-size: allow-keywords) {
|
||||||
interpolate-size: allow-keywords;
|
interpolate-size: allow-keywords;
|
||||||
|
|||||||
@@ -31,9 +31,11 @@
|
|||||||
"next": "15.3.4",
|
"next": "15.3.4",
|
||||||
"next-auth": "5.0.0-beta.29",
|
"next-auth": "5.0.0-beta.29",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
"react-aria-components": "^1.8.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-intl": "^7.1.11",
|
"react-intl": "^7.1.11",
|
||||||
"server-only": "^0.0.1"
|
"server-only": "^0.0.1",
|
||||||
|
"usehooks-ts": "3.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
|
|||||||
@@ -3,13 +3,12 @@
|
|||||||
import { usePathname } from "next/navigation"
|
import { usePathname } from "next/navigation"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { languages } from "@scandic-hotels/common/constants/language"
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
import Link from "@scandic-hotels/design-system/Link"
|
import Link from "@scandic-hotels/design-system/Link"
|
||||||
import Subtitle from "@scandic-hotels/design-system/Subtitle"
|
import Subtitle from "@scandic-hotels/design-system/Subtitle"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { languages } from "@/constants/languages"
|
|
||||||
|
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
import { replaceUrlPart } from "./utils"
|
import { replaceUrlPart } from "./utils"
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import { useRef } from "react"
|
|||||||
import FocusLock from "react-focus-lock"
|
import FocusLock from "react-focus-lock"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { languages } from "@scandic-hotels/common/constants/language"
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer"
|
import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
import { trackLanguageSwitchClick } from "@scandic-hotels/tracking/navigation"
|
import { trackLanguageSwitchClick } from "@scandic-hotels/tracking/navigation"
|
||||||
import { trpc } from "@scandic-hotels/trpc/client"
|
import { trpc } from "@scandic-hotels/trpc/client"
|
||||||
|
|
||||||
import { languages } from "@/constants/languages"
|
|
||||||
import useDropdownStore from "@/stores/main-menu"
|
import useDropdownStore from "@/stores/main-menu"
|
||||||
|
|
||||||
import useClickOutside from "@/hooks/useClickOutside"
|
import useClickOutside from "@/hooks/useClickOutside"
|
||||||
|
|||||||
@@ -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 { profileEdit } from "@scandic-hotels/common/constants/routes/myPages"
|
||||||
import { isValidLang } from "@scandic-hotels/common/utils/languages"
|
import { isValidLang } from "@scandic-hotels/common/utils/languages"
|
||||||
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
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 { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
import { countriesMap } from "@scandic-hotels/trpc/constants/countries"
|
import { countriesMap } from "@scandic-hotels/trpc/constants/countries"
|
||||||
|
|
||||||
import { languages } from "@/constants/languages"
|
|
||||||
import { getProfile } from "@/lib/trpc/memoizedRequests"
|
import { getProfile } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
import CommunicationSlot from "@/components/MyPages/Profile/Communication"
|
import CommunicationSlot from "@/components/MyPages/Profile/Communication"
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
import { Lang } from "@scandic-hotels/common/constants/language"
|
|
||||||
import { ApiLang } from "@scandic-hotels/trpc/constants/apiLang"
|
import { ApiLang } from "@scandic-hotels/trpc/constants/apiLang"
|
||||||
|
|
||||||
export const languages: Record<Lang, string> = {
|
import type { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
[Lang.da]: "Dansk",
|
|
||||||
[Lang.de]: "Deutsch",
|
|
||||||
[Lang.en]: "English",
|
|
||||||
[Lang.fi]: "Suomi",
|
|
||||||
[Lang.no]: "Norsk",
|
|
||||||
[Lang.sv]: "Svenska",
|
|
||||||
}
|
|
||||||
|
|
||||||
const languageSelect = [
|
const languageSelect = [
|
||||||
{ label: "Danish", value: ApiLang.Da },
|
{ label: "Danish", value: ApiLang.Da },
|
||||||
|
|||||||
@@ -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"
|
import type { ReactElement } from "react"
|
||||||
|
|
||||||
export enum LanguageSwitcherTypesEnum {
|
export enum LanguageSwitcherTypesEnum {
|
||||||
|
|||||||
@@ -6,3 +6,26 @@ export enum Lang {
|
|||||||
no = "no",
|
no = "no",
|
||||||
sv = "sv",
|
sv = "sv",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const languages: Record<Lang, string> = {
|
||||||
|
[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
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { getNonContentstackUrls } from "../metadata/output"
|
|||||||
import { getLanguageSwitcherInput } from "./input"
|
import { getLanguageSwitcherInput } from "./input"
|
||||||
import { getUrlsOfAllLanguages } from "./utils"
|
import { getUrlsOfAllLanguages } from "./utils"
|
||||||
|
|
||||||
import type { LanguageSwitcherData } from "../../../types/languageSwitcher"
|
import type { LanguageSwitcherData } from "@scandic-hotels/common/constants/language"
|
||||||
|
|
||||||
export const languageSwitcherQueryRouter = router({
|
export const languageSwitcherQueryRouter = router({
|
||||||
get: publicProcedure
|
get: publicProcedure
|
||||||
|
|||||||
@@ -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 { createLogger } from "@scandic-hotels/common/logger/createLogger"
|
||||||
import { createCounter } from "@scandic-hotels/common/telemetry"
|
import { createCounter } from "@scandic-hotels/common/telemetry"
|
||||||
import { removeTrailingSlash } from "@scandic-hotels/common/utils/url"
|
import { removeTrailingSlash } from "@scandic-hotels/common/utils/url"
|
||||||
@@ -57,10 +60,7 @@ import {
|
|||||||
import { generateTag } from "../../../utils/generateTag"
|
import { generateTag } from "../../../utils/generateTag"
|
||||||
import { validateLanguageSwitcherData } from "./output"
|
import { validateLanguageSwitcherData } from "./output"
|
||||||
|
|
||||||
import type {
|
import type { LanguageSwitcherQueryDataRaw } from "../../../types/languageSwitcher"
|
||||||
LanguageSwitcherData,
|
|
||||||
LanguageSwitcherQueryDataRaw,
|
|
||||||
} from "../../../types/languageSwitcher"
|
|
||||||
|
|
||||||
export const languageSwitcherAffix = "languageSwitcher"
|
export const languageSwitcherAffix = "languageSwitcher"
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ import { RTETypeEnum } from "../../../types/RTEenums"
|
|||||||
import { destinationFilterSchema } from "../schemas/destinationFilters"
|
import { destinationFilterSchema } from "../schemas/destinationFilters"
|
||||||
import { systemSchema } from "../schemas/system"
|
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 { ImageVaultAsset } from "@scandic-hotels/common/utils/imageVault"
|
||||||
|
|
||||||
import type { LanguageSwitcherData } from "../../../types/languageSwitcher"
|
|
||||||
|
|
||||||
const metaDataJsonSchema = z.object({
|
const metaDataJsonSchema = z.object({
|
||||||
children: z.array(
|
children: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ import { getMetadataInput } from "./input"
|
|||||||
import { getNonContentstackUrls, rawMetadataSchema } from "./output"
|
import { getNonContentstackUrls, rawMetadataSchema } from "./output"
|
||||||
import { affix, getCityData, getCountryData } from "./utils"
|
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 { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
|
|
||||||
import type { LanguageSwitcherData } from "../../../types/languageSwitcher"
|
|
||||||
import type { RawMetadataSchema } from "./output"
|
import type { RawMetadataSchema } from "./output"
|
||||||
|
|
||||||
const fetchMetadata = cache(async function fetchMemoizedMetadata<T>(
|
const fetchMetadata = cache(async function fetchMemoizedMetadata<T>(
|
||||||
|
|||||||
@@ -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 = {
|
type LanguageResult = {
|
||||||
web?: {
|
web?: {
|
||||||
original_url?: string | null
|
original_url?: string | null
|
||||||
|
|||||||
@@ -6054,10 +6054,12 @@ __metadata:
|
|||||||
next: "npm:15.3.4"
|
next: "npm:15.3.4"
|
||||||
next-auth: "npm:5.0.0-beta.29"
|
next-auth: "npm:5.0.0-beta.29"
|
||||||
react: "npm:^19.0.0"
|
react: "npm:^19.0.0"
|
||||||
|
react-aria-components: "npm:^1.8.0"
|
||||||
react-dom: "npm:^19.0.0"
|
react-dom: "npm:^19.0.0"
|
||||||
react-intl: "npm:^7.1.11"
|
react-intl: "npm:^7.1.11"
|
||||||
server-only: "npm:^0.0.1"
|
server-only: "npm:^0.0.1"
|
||||||
typescript: "npm:5.8.3"
|
typescript: "npm:5.8.3"
|
||||||
|
usehooks-ts: "npm:3.1.1"
|
||||||
vitest: "npm:^3.2.4"
|
vitest: "npm:^3.2.4"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|||||||
Reference in New Issue
Block a user