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:
Hrishikesh Vaipurkar
2025-10-06 08:46:26 +00:00
parent e18bba79c6
commit d3368e9b85
27 changed files with 985 additions and 125 deletions
@@ -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;
}
}