Merged in monorepo-step-1 (pull request #1080)

Migrate to a monorepo setup - step 1

* Move web to subfolder /apps/scandic-web

* Yarn + transitive deps

- Move to yarn
- design-system package removed for now since yarn doesn't
support the parameter for token (ie project currently broken)
- Add missing transitive dependencies as Yarn otherwise
prevents these imports
- VS Code doesn't pick up TS path aliases unless you open
/apps/scandic-web instead of root (will be fixed with monorepo)

* Pin framer-motion to temporarily fix typing issue

https://github.com/adobe/react-spectrum/issues/7494

* Pin zod to avoid typ error

There seems to have been a breaking change in the types
returned by zod where error is now returned as undefined
instead of missing in the type. We should just handle this
but to avoid merge conflicts just pin the dependency for
now.

* Pin react-intl version

Pin version of react-intl to avoid tiny type issue where formatMessage
does not accept a generic any more. This will be fixed in a future
commit, but to avoid merge conflicts just pin for now.

* Pin typescript version

Temporarily pin version as newer versions as stricter and results in
a type error. Will be fixed in future commit after merge.

* Setup workspaces

* Add design-system as a monorepo package

* Remove unused env var DESIGN_SYSTEM_ACCESS_TOKEN

* Fix husky for monorepo setup

* Update netlify.toml

* Add lint script to root package.json

* Add stub readme

* Fix react-intl formatMessage types

* Test netlify.toml in root

* Remove root toml

* Update netlify.toml publish path

* Remove package-lock.json

* Update build for branch/preview builds


Approved-by: Linus Flood
This commit is contained in:
Anton Gunnarsson
2025-02-26 10:36:17 +00:00
committed by Linus Flood
parent 667cab6fb6
commit 80100e7631
2731 changed files with 30986 additions and 23708 deletions

View File

@@ -0,0 +1,43 @@
.button {
background-color: #02838e;
color: #fff;
padding: 5px 15px;
display: inline-block;
line-height: 20px;
border: 1px solid transparent;
border-radius: 50px;
height: 38px;
line-height: 20px;
font-size: 14px;
font-family: Helvetica, Arial, sans-serif;
font-weight: 400;
font-size: 16px;
cursor: pointer;
text-decoration: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
align-content: center;
}
.button:hover {
text-decoration: underline;
}
.button:focus,
.button:active {
box-shadow: 0 0 1px 2px #b4defa;
outline: none;
border: 1px solid hsl(0, 0%, 80%);
text-decoration: underline;
}
@media screen and (min-width: 1367px) {
.button {
font-weight: 600;
font-size: 16px;
line-height: 24px;
height: auto;
padding: 12px 32px;
}
}

View File

@@ -0,0 +1,16 @@
"use client"
import "@scandic-hotels/design-system/current/style.css"
import { useIntl } from "react-intl"
import styles from "./bookingButton.module.css"
export default function BookingButton({ href }: { href: string }) {
const intl = useIntl()
return (
<a className={styles.button} href={href}>
{intl.formatMessage({ id: "Book" })}
</a>
)
}

View File

@@ -0,0 +1,13 @@
import { MainMenuSkeleton } from "../MainMenu"
import { TopMenuSkeleton } from "../TopMenu"
import styles from "../header.module.css"
export default async function HeaderFallback() {
return (
<header className={styles.header} role="banner">
<TopMenuSkeleton />
<MainMenuSkeleton />
</header>
)
}

View File

@@ -0,0 +1,113 @@
.desktop {
display: none;
}
.container {
position: relative;
font-family:
Helvetica Neue,
Helvetica,
Arial,
sans-serif;
}
.toggle {
display: flex;
color: #fff;
padding: 3px 15px;
font-size: 14px;
border: none;
background-color: transparent;
margin: 0 auto;
cursor: pointer;
gap: 4px;
align-items: center;
}
.hiddenAccessible {
display: block;
position: absolute;
left: -100000em;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
.caret {
display: inline-block;
width: 0;
height: 0;
margin-left: 2px;
vertical-align: middle;
border-top: 5px dashed;
border-right: 5px solid transparent;
border-left: 5px solid transparent;
}
.icon {
width: 18px;
height: 18px;
vertical-align: -4px;
margin-right: 3px;
fill: #fff;
}
.dropdown {
position: absolute;
top: 100%;
left: 0;
z-index: 100;
display: none;
float: left;
min-width: 160px;
padding: 5px 0;
margin: 2px 0 0;
list-style: none;
font-size: 16px;
text-align: left;
background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 4px;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
background-clip: padding-box;
}
.li {
cursor: pointer;
}
.dropdown.isOpen {
display: block;
}
.link {
clear: both;
color: grey;
display: block;
font-weight: 400;
padding: 3px 20px;
white-space: nowrap;
text-decoration: none;
}
.link:hover {
background-color: #f5f5f5;
color: #737373;
text-decoration: none;
}
.active > .link {
background-color: #00838e;
color: #fff;
outline: 0;
text-decoration: none;
}
@media (min-width: 1366px) {
.desktop {
display: block;
}
}

View File

@@ -0,0 +1,98 @@
"use client"
import { useCallback, useEffect, useRef, useState } from "react"
import { type Lang, languages } from "@/constants/languages"
import Link from "@/components/TempDesignSystem/Link"
import useLang from "@/hooks/useLang"
import styles from "./desktop.module.css"
import type { LanguageSwitcherProps } from "@/types/components/current/languageSwitcher"
export default function Desktop({ urls }: LanguageSwitcherProps) {
const currentLanguage = useLang()
const [isOpen, setIsOpen] = useState(false)
const divRef = useRef<HTMLDivElement>(null)
function toggleOpen() {
setIsOpen((prevIsOpen) => !prevIsOpen)
}
const close = useCallback(() => {
setIsOpen(false)
}, [setIsOpen])
useEffect(() => {
function handleClickOutside(evt: Event) {
const target = evt.target as HTMLElement
if (divRef.current && target && !divRef.current.contains(target)) {
close()
}
}
if (divRef.current) {
document.addEventListener("click", handleClickOutside, false)
}
return () => {
document.removeEventListener("click", handleClickOutside, false)
}
}, [close])
const urlKeys = Object.keys(urls)
if (urlKeys.length === 1 && urlKeys[0] === currentLanguage) {
return (
<div className={styles.container} ref={divRef}>
<section className={styles.languageSwitcher}>
<svg focusable="false" className={styles.icon} viewBox="0 0 32 32">
<use xlinkHref="/_static/img/icons/sprites.svg#icon-globe"></use>
</svg>
{languages[currentLanguage]}
</section>
</div>
)
}
return (
<section className={styles.desktop}>
<div className={styles.container} ref={divRef}>
<button
aria-pressed="false"
className={styles.toggle}
onClick={toggleOpen}
>
<svg focusable="false" className={styles.icon} viewBox="0 0 32 32">
<use xlinkHref="/_static/img/icons/sprites.svg#icon-globe"></use>
</svg>
{languages[currentLanguage]}
<span className={styles.hiddenAccessible}>Choose language</span>
<span className={styles.caret}></span>
</button>
<ul className={`${styles.dropdown} ${isOpen ? styles.isOpen : ""}`}>
{urlKeys.map((key) => {
const url = urls[key as Lang]?.url
if (url) {
return (
<li
key={key}
className={`${styles.li} ${currentLanguage === key ? styles.active : ""}`}
>
{urls[key as Lang]?.isExternal ? (
<Link className={styles.link} href={url}>
{languages[key as Lang]}
</Link>
) : (
<a className={styles.link} href={url}>
{languages[key as Lang]}
</a>
)}
</li>
)
}
})}
</ul>
</div>
</section>
)
}

View File

@@ -0,0 +1,56 @@
"use client"
import { useState } from "react"
import { type Lang, languages } from "@/constants/languages"
import useLang from "@/hooks/useLang"
import styles from "./mobile.module.css"
import type { LanguageSwitcherProps } from "@/types/components/current/languageSwitcher"
export default function Mobile({ urls }: LanguageSwitcherProps) {
const currentLanguage = useLang()
const [isOpen, setIsOpen] = useState(false)
function toggleOpen() {
setIsOpen((prevIsOpen) => !prevIsOpen)
}
const urlKeys = Object.keys(urls)
if (urlKeys.length === 1 && urlKeys[0] === currentLanguage) {
return <div className={styles.toggle}>{languages[currentLanguage]}</div>
}
return (
<section className={styles.mobile}>
<div>
<button
aria-pressed="false"
className={styles.toggle}
onClick={toggleOpen}
>
{languages[currentLanguage]}{" "}
<span
className={`${styles.arrow} ${isOpen ? styles.open : ""}`}
></span>
<span className={styles.hiddenAccessible}>Choose language</span>
</button>
<ul className={`${styles.dropdown} ${isOpen ? styles.isOpen : ""}`}>
{urlKeys.map((key) => {
const url = urls[key as Lang]?.url
if (url) {
return (
<li key={key} className={styles.li}>
<a href={url} className={styles.link}>
{languages[key as Lang]}
</a>
</li>
)
}
})}
</ul>
</div>
</section>
)
}

View File

@@ -0,0 +1,74 @@
.mobile {
display: block;
font-family: Helvetica, Arial, sans-serif;
}
.toggle {
font-size: 14px;
padding: 5px 0;
display: block;
border: none;
background-color: transparent;
margin: 0 auto;
color: #333;
}
.hiddenAccessible {
display: block;
position: absolute;
left: -100000em;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
.li {
list-style: none;
font-size: 14px;
font-family: Helvetica, Arial, sans-serif;
}
.link {
color: #333;
text-decoration: none;
line-height: 22.4px;
}
.dropdown {
display: none;
}
.dropdown.isOpen {
display: block;
}
.arrow {
background-image: url("/_static/img/icons/arrows/arrow-down-grey.png");
background-position: 50%;
background-repeat: no-repeat;
display: inline-block;
margin-left: 5px;
padding: 5px 10px;
}
.arrow.open {
background-image: url("/_static/img/icons/arrows/arrow-up-grey.png");
}
.link {
color: grey;
display: block;
text-transform: capitalize;
}
.link:hover {
color: #7f7369;
text-decoration: none;
}
@media (min-width: 1366px) {
.mobile {
display: none;
}
}

View File

@@ -0,0 +1,37 @@
"use client"
import { usePathname } from "next/navigation"
import { trpc } from "@/lib/trpc/client"
import SkeletonShimmer from "@/components/SkeletonShimmer"
import useLang from "@/hooks/useLang"
import Desktop from "./Desktop"
import Mobile from "./Mobile"
export default function LanguageSwitcher() {
const currentLanguage = useLang()
const pathName = usePathname()
const { data: languagesResponse, isLoading } =
trpc.contentstack.languageSwitcher.get.useQuery({
pathName,
lang: currentLanguage,
})
if (isLoading) {
return <SkeletonShimmer width="12ch" />
}
if (!languagesResponse?.urls) {
return null
}
return (
<>
<Desktop urls={languagesResponse.urls} />
<Mobile urls={languagesResponse.urls} />
</>
)
}

View File

@@ -0,0 +1,276 @@
"use client"
import { usePathname } from "next/navigation"
import { useIntl } from "react-intl"
import { logout } from "@/constants/routes/handleAuth"
import { myPages } from "@/constants/routes/myPages"
import useDropdownStore from "@/stores/main-menu"
import Image from "@/components/Image"
import LoginButton from "@/components/LoginButton"
import Avatar from "@/components/MyPages/Avatar"
import SkeletonShimmer from "@/components/SkeletonShimmer"
import Link from "@/components/TempDesignSystem/Link"
import useLang from "@/hooks/useLang"
import { trackClick } from "@/utils/tracking"
import BookingButton from "../BookingButton"
import styles from "./mainMenu.module.css"
import type { MainMenuProps } from "@/types/components/current/header/mainMenu"
import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown"
export function MainMenu({
frontpageLinkText,
homeHref,
links,
logo,
topMenuMobileLinks,
languageSwitcher,
myPagesMobileDropdown,
bookingHref,
user,
}: MainMenuProps) {
const intl = useIntl()
const lang = useLang()
const pathname = usePathname()
const isThreeStaticPagesPathnames = [
"/de/sponsoring",
"/en/sponsoring",
"/da/sponsorering",
"/fi/sponsorointi",
"/no/vi-sponser",
"/sv/vi-sponsrar",
"/de/scandic-entdecken/wlan",
"/en/explore-scandic/wifi",
"/da/oplev-scandic/wifi",
"/fi/koe-scandic/maksuton-internetyhteys",
"/no/utforsk-scandic/wifi",
"/sv/utforska-scandic/wi-fi",
"/de/kundenbetreuung/haufig-gestellte-fragen/nutzung-der-internetseite",
"/en/customer-service/frequently-asked-questions/using-the-website",
"/da/kundeservice/sporgsmal-og-svar/om-scandics-website",
"/fi/asiakaspalvelu/usein-kysytyt-kysymykset/tietoja-internetsivuista",
"/no/kundeservice/sporsmal-og-svar/bruk-av-nettsiden",
"/sv/kundservice/fragor-och-svar/om-scandics-webbplats",
"/de/current-content-page",
"/en/current-content-page",
"/da/current-content-page",
"/fi/current-content-page",
"/no/current-content-page",
"/sv/current-content-page",
].includes(pathname)
const { toggleDropdown, isMyPagesMobileMenuOpen, isHamburgerMenuOpen } =
useDropdownStore()
function handleMyPagesMobileMenuClick() {
// Only track click when opening it
if (!isMyPagesMobileMenuOpen) {
trackClick("profile picture icon")
}
toggleDropdown(DropdownTypeEnum.MyPagesMobileMenu)
}
return (
<div className={styles.mainMenu}>
<div
className={styles.container}
itemScope
itemType="http://schema.org/Organization"
>
<meta itemProp="name" content="Scandic" />
<nav className={styles.navBar}>
<button
aria-pressed="false"
className={`${styles.expanderBtn} ${isHamburgerMenuOpen ? styles.expanded : ""}`}
onClick={() => toggleDropdown(DropdownTypeEnum.HamburgerMenu)}
type="button"
>
<span className={styles.iconBars}></span>
<span className={styles.hiddenAccessible}>Menu</span>
</button>
<a className={styles.logoLink} href={homeHref}>
<span className={styles.hiddenAccessible}>{frontpageLinkText}</span>
<Image
alt="Scandic Hotels logo"
className={styles.logo}
data-js="scandiclogoimg"
data-nosvgsrc="/_static/img/scandic-logotype.png"
itemProp="logo"
height={22}
src={logo.url}
width={logo.dimension.width}
/>
</a>
<ul
className={`${styles.listWrapper} ${isHamburgerMenuOpen ? styles.isOpen : ""}`}
>
<ul className={styles.linkRow}>
{user ? (
<li className={styles.mobileLinkRow}>
<Link
className={styles.mobileLinkButton}
href={myPages[lang]}
>
{intl.formatMessage({ id: "My pages" })}
</Link>
</li>
) : (
<>
<li>
<Image
src="/_static/img/icon-scandic-friends.svg"
alt="Scanidc Friends Logo"
height={35}
width={35}
className={styles.scandicFriendsLogo}
/>
</li>
<li className={styles.mobileLinkRow}>
<LoginButton
position="hamburger menu"
trackingId="loginStartHamburgerMenu"
className={styles.mobileLinkButton}
>
{intl.formatMessage({ id: "Log in" })}
</LoginButton>
</li>
</>
)}
<li className={styles.mobileLinkRow}>
<span className={styles.mobileSeparator} />
</li>
<li className={styles.mobileLinkRow}>
<a className={styles.mobileLinkButton} href="">
{intl.formatMessage({ id: "Find booking" })}
</a>
</li>
</ul>
<ul className={styles.mainLinks}>
{links.map((link, i) => (
<li className={styles.li} key={link.href + i}>
<Link
className={styles.link}
href={link.href}
trackingId={
isHamburgerMenuOpen
? `hamburger - ${link.title}`
: undefined
}
>
{link.title}
</Link>
</li>
))}
</ul>
<ul className={styles.mobileList}>
{topMenuMobileLinks.map(({ link }, i) => (
<li className={styles.mobileLi} key={link.href + i}>
<Link
className={styles.mobileLink}
href={link.href}
trackingId={
isHamburgerMenuOpen
? `hamburger - ${link.title}`
: undefined
}
>
{link.title}
</Link>
</li>
))}
</ul>
{languageSwitcher ? (
<li className={styles.mobileLi}>{languageSwitcher}</li>
) : null}
{!!user ? (
<li className={`${styles.mobileLi} ${styles.logout}`}>
<Link
href={logout[lang]}
className={styles.mobileLink}
prefetch={false}
>
{intl.formatMessage({ id: "Log out" })}
</Link>
</li>
) : null}
</ul>
<div className={styles.buttonContainer}>
<div className={styles.myPagesDesktopLink}>
{!isThreeStaticPagesPathnames && user ? (
<Link className={styles.link} href={myPages[lang]}>
{intl.formatMessage({ id: "My pages" })}
</Link>
) : null}
</div>
<BookingButton href={bookingHref} />
{!isThreeStaticPagesPathnames && myPagesMobileDropdown && user ? (
<div
role="button"
onClick={handleMyPagesMobileMenuClick}
className={styles.avatarButton}
>
<Avatar firstName={user.firstName} lastName={user.lastName} />
</div>
) : null}
</div>
{isMyPagesMobileMenuOpen ? myPagesMobileDropdown : null}
</nav>
</div>
</div>
)
}
export function MainMenuSkeleton() {
return (
<div className={styles.mainMenu}>
<div
className={styles.container}
itemScope
itemType="http://schema.org/Organization"
>
<meta itemProp="name" content="Scandic" />
<nav className={styles.navBar}>
<button
aria-pressed="false"
className={styles.expanderBtn}
type="button"
>
<span className={styles.iconBars}></span>
<span className={styles.hiddenAccessible}>Menu</span>
</button>
<a className={styles.logoLink} href={""}>
<Image
alt="Scandic Hotels logo"
className={styles.logo}
data-js="scandiclogoimg"
itemProp="logo"
height={20}
src={"/_static/img/scandic-logotype.png"}
width={200}
/>
</a>
<ul className={styles.listWrapper}>
{Array.from({ length: 5 }, () => "").map((_, i) => (
<li className={`${styles.li} ${styles.skeletonWrapper}`} key={i}>
<SkeletonShimmer height="22px" width="130px" />
</li>
))}
</ul>
<div className={styles.buttonContainer}>
<BookingButton href={""} />
</div>
</nav>
</div>
</div>
)
}

View File

@@ -0,0 +1,348 @@
.mainMenu {
background-color: var(--Main-Grey-White);
background-image: none;
box-shadow: 0px 1.001px 1.001px 0px rgba(0, 0, 0, 0.05);
max-height: 100%;
overflow: visible;
width: 100%;
height: var(--current-mobile-site-header-height);
max-width: var(--max-width-navigation);
margin: 0 auto;
}
.container {
box-sizing: content-box;
height: 100%;
margin: 0 auto;
padding: 0;
}
.mainLinks {
background-color: #f3f2f1;
}
.navBar {
display: grid;
grid-template-columns: 1fr 80px 1fr;
grid-template-areas: "expanderBtn logoLink . buttonContainer";
grid-template-rows: 100%;
height: 100%;
}
.expanderBtn {
background-color: transparent;
border: none;
cursor: pointer;
justify-self: flex-start;
padding: 11px 8px 16px;
transition: 0.3s;
user-select: none;
}
.iconBars,
.iconBars::after,
.iconBars::before {
background: #757575;
border-radius: 2.3px;
display: inline-block;
height: 5px;
position: relative;
transition: 0.3s;
width: 32px;
}
.iconBars::after,
.iconBars::before {
content: "";
left: 0;
position: absolute;
transform-origin: 2.286px center;
}
.iconBars::after {
top: -8px;
}
.iconBars::before {
top: 8px;
}
.expanded .iconBars {
background: transparent;
}
.expanded .iconBars::after,
.expanded .iconBars::before {
top: 0;
transform-origin: 50% 50%;
width: 32px;
}
.expanded .iconBars::after {
transform: rotate(-45deg);
}
.expanded .iconBars::before {
transform: rotate(45deg);
}
.hiddenAccessible {
display: block;
height: 1px;
left: -100000em;
overflow: hidden;
position: absolute;
top: auto;
width: 1px;
}
.logoLink {
/*padding: 16px 0 8px;*/
display: inline-flex;
align-items: center;
height: 100%;
width: 80px;
}
.logo {
width: 80px;
object-fit: fill;
}
.listWrapper {
background-color: #fff;
border-top: 1px solid #e3e0db;
display: none;
list-style: none;
overflow-y: visible;
padding-bottom: 20px;
margin: 0;
padding-inline-start: 0;
}
.listWrapper.isOpen {
display: block;
left: 0;
position: absolute;
right: 0;
top: 100%;
}
.li {
border-bottom: none;
display: block;
line-height: 17px;
position: relative;
text-align: center;
}
.link {
color: #000;
text-decoration: none;
display: block;
font-family:
Helvetica Neue,
Helvetica,
Arial,
sans-serif;
font-size: 14px;
font-weight: 700;
padding-bottom: 20px;
padding-top: 20px;
text-transform: uppercase;
}
.link:hover {
color: #7f7369;
text-decoration: none;
}
.linkRow {
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid #e3e0db;
background-color: #f3f2f1 !important;
list-style: none;
}
.scandicFriendsLogo {
margin-right: 4px;
margin-left: -4px;
}
.mobileLinkRow {
margin: 6px 0;
padding: 15px 15px 15px 5px;
}
.mobileLinkButton {
font-size: 14px;
font-family:
Helvetica Neue,
Helvetica,
Arial,
sans-serif !important;
font-weight: 700;
background-color: transparent !important;
text-decoration: none;
color: #000;
outline-color: transparent;
}
.mobileSeparator {
border-left: 1px solid #e3e0db;
height: 35px;
margin-bottom: -12px;
margin-left: -1px;
margin-top: -12px;
display: block;
}
.mobileList {
padding-top: 6px;
}
.mobileLi {
display: block;
position: relative;
text-align: center;
line-height: 22.4px;
padding: 5px 0;
}
.mobileLi.logout {
padding: 0;
}
.mobileLink {
color: #000;
display: block;
font-family: Helvetica !important;
font-size: 14px;
text-decoration: none;
}
.buttonContainer {
display: inline-flex;
justify-content: flex-end;
align-items: center;
margin-right: 8px;
gap: var(--Spacing-x3);
}
.myPagesDesktopLink {
display: none;
}
.skeletonWrapper {
padding: 4px 10px;
height: 100%;
align-content: center;
}
@media (min-width: 1367px) {
.navBar {
grid-template-columns: 140px auto 1fr;
height: 82.4px;
align-content: center;
padding: 0px 0px var(--Spacing-x-quarter) 0px;
overflow: hidden;
}
.logoLink {
display: inline-block;
width: 100%;
padding: 27px 30px 26px 0;
text-align: center;
align-items: center;
}
.mainMenu {
box-shadow: none;
background-color: hsla(0, 0%, 100%, 0.95);
position: relative;
z-index: unset;
height: 82.4px;
}
.container {
padding: 0 var(--Spacing-x5) 0 120px;
}
.mainLinks {
padding-top: 2.5px;
background-color: transparent;
height: 100%;
}
.expanderBtn {
display: none;
}
.logo {
width: 102.17px;
height: 100%;
padding-bottom: 4px;
}
.listWrapper {
border-top: none;
display: flex;
align-items: center;
padding-top: 0;
position: static;
width: 100%;
padding-bottom: 0px;
height: 100%;
}
.listWrapper.isOpen {
position: static;
}
.li {
display: inline-grid;
float: none;
vertical-align: middle;
line-height: 1.15;
}
.link {
background-image: none;
font-family: var(--typography-Body-Regular-fontFamily);
font-size: var(--typography-Body-Regular-fontSize);
font-feature-settings:
"clig" off,
"liga" off;
font-weight: 600;
line-height: 125%;
padding: 30px 15px;
text-transform: uppercase;
color: var(--text-black); /* Design system should return #404040 */
}
.linkRow {
display: none;
}
.mobileList {
display: none;
padding-top: 0px;
}
.mobileLi {
display: none;
}
.buttonContainer {
margin-right: 0;
}
.avatarButton {
display: none;
}
.myPagesDesktopLink {
display: block;
}
}

View File

@@ -0,0 +1,148 @@
"use client"
import { useIntl } from "react-intl"
import { logout } from "@/constants/routes/handleAuth"
import { trpc } from "@/lib/trpc/client"
import useDropdownStore from "@/stores/main-menu"
import SkeletonShimmer from "@/components/SkeletonShimmer"
import Divider from "@/components/TempDesignSystem/Divider"
import Link from "@/components/TempDesignSystem/Link"
import Title from "@/components/TempDesignSystem/Text/Title"
import useLang from "@/hooks/useLang"
import styles from "./my-pages-mobile-dropdown.module.css"
import type { ReactNode } from "react"
import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown"
export default function MyPagesMobileDropdown() {
const intl = useIntl()
const { toggleDropdown, isMyPagesMobileMenuOpen } = useDropdownStore()
const handleOnClick = () => toggleDropdown(DropdownTypeEnum.MyPagesMobileMenu)
return (
<nav
className={`${styles.navigationMenu} ${isMyPagesMobileMenuOpen ? styles.navigationMenuIsOpen : ""}`}
>
<Title textTransform="capitalize" level="h5">
<div className={styles.heading}>
{intl.formatMessage({ id: "My pages" })}
</div>
</Title>
<List>
<PrimaryLinks handleOnClick={handleOnClick} />
</List>
<List>
<SecondaryLinks handleOnClick={handleOnClick} />
</List>
</nav>
)
}
function List({ children }: { children: ReactNode }) {
return (
<>
<div className={styles.dividerWrapper}>
<Divider color="subtle" />
</div>
<ul className={styles.dropdownWrapper}>
<ul className={styles.dropdownLinks}>{children}</ul>
</ul>
</>
)
}
function PrimaryLinks({ handleOnClick }: { handleOnClick: () => void }) {
const {
data: myPagesNavigation,
isLoading,
isSuccess,
} = useMyPagesNavigation()
const primaryLinks = myPagesNavigation?.primaryLinks ?? []
return (
<>
{isLoading && <Skeletons count={4} />}
{isSuccess &&
primaryLinks.map((link, i) => (
<li key={link.href + i}>
<Link
href={link.href}
partialMatch
size={"regular"}
variant="myPageMobileDropdown"
color="burgundy"
onClick={handleOnClick}
>
{link.text}
</Link>
</li>
))}
</>
)
}
function SecondaryLinks({ handleOnClick }: { handleOnClick: () => void }) {
const {
data: myPagesNavigation,
isLoading,
isSuccess,
} = useMyPagesNavigation()
const secondaryLinks = myPagesNavigation?.secondaryLinks ?? []
const intl = useIntl()
const lang = useLang()
return (
<>
{isLoading && <Skeletons count={3} />}
{isSuccess &&
secondaryLinks.map((link, i) => (
<li key={link.href + i}>
<Link
href={link.href}
partialMatch
size={"small"}
variant="myPageMobileDropdown"
color="burgundy"
onClick={handleOnClick}
>
{link.text}
</Link>
</li>
))}
<li>
<Link
href={logout[lang]}
prefetch={false}
size="small"
color="burgundy"
variant="myPageMobileDropdown"
>
{intl.formatMessage({ id: "Log out" })}
</Link>
</li>
</>
)
}
function Skeletons({ count }: { count: number }) {
return (
<>
{Array.from({ length: count }).map((_, i) => (
<li key={i} className={styles.skeletonItem}>
<SkeletonShimmer width="100px" height="20px" />
</li>
))}
</>
)
}
function useMyPagesNavigation() {
const lang = useLang()
return trpc.navigation.myPages.useQuery({ lang })
}

View File

@@ -0,0 +1,67 @@
.navigationMenu {
background-color: #fff;
border-top: 1px solid #e3e0db;
display: none;
list-style: none;
overflow-y: visible;
margin: 0;
padding-inline-start: 0;
}
.navigationMenu.navigationMenuIsOpen {
display: block;
left: 0;
position: absolute;
right: 0;
top: 100%;
}
.dropdownWrapper {
display: flex;
width: 100%;
padding: 20px var(--Spacing-x2);
flex-direction: column;
justify-content: center;
align-items: flex-start;
background-color: var(--Main-Grey-White);
box-shadow:
0px 276px 77px 0px rgba(0, 0, 0, 0),
0px 177px 71px 0px rgba(0, 0, 0, 0.01),
0px 99px 60px 0px rgba(0, 0, 0, 0.05),
0px 44px 44px 0px rgba(0, 0, 0, 0.09),
0px 11px 24px 0px rgba(0, 0, 0, 0.1);
}
.dividerWrapper {
background-color: var(--Main-Grey-White);
padding: 0 var(--Spacing-x2);
margin: auto;
place-content: center;
display: flex;
}
.heading {
padding: 20px var(--Spacing-x2);
}
.dropdownLinks {
display: flex;
flex-direction: column;
gap: var(--Spacing-x-half);
width: 100%;
list-style: none;
}
@media screen and (min-width: 1367px) {
.navigationMenu {
display: none;
}
.navigationMenu.navigationMenuIsOpen {
display: none;
}
}
.skeletonItem {
padding: var(--Spacing-x1);
}

View File

@@ -0,0 +1,23 @@
.banner {
align-items: center;
background: #606060;
color: #fff;
display: flex;
justify-content: space-between;
padding: 10px;
position: relative;
z-index: 10;
}
.hidden {
display: none;
}
.reloadBtn {
color: #fff;
background-color: #00838e;
border: 0;
border-radius: 18px;
outline: 0 none;
padding: 5px 15px;
}

View File

@@ -0,0 +1,10 @@
import styles from "./banner.module.css"
export default function OfflineBanner() {
return (
<div className={`${styles.banner} ${styles.hidden}`}>
You are offline, some content may be out of date.
<button className={styles.reloadBtn} type="button">Reload</button>
</div>
)
}

View File

@@ -0,0 +1,112 @@
import { logout } from "@/constants/routes/handleAuth"
import { overview } from "@/constants/routes/myPages"
import { getName } from "@/lib/trpc/memoizedRequests"
import LoginButton from "@/components/LoginButton"
import SkeletonShimmer from "@/components/SkeletonShimmer"
import Link from "@/components/TempDesignSystem/Link"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import styles from "./topMenu.module.css"
import type { TopMenuProps } from "@/types/components/current/header/topMenu"
function capitalize(str: string) {
return str.charAt(0).toUpperCase().toUpperCase() + str.slice(1).toLowerCase()
}
export default async function TopMenu({
frontpageLinkText,
homeHref,
links,
languageSwitcher,
}: TopMenuProps) {
const intl = await getIntl()
const user = await getName()
return (
<div className={styles.topMenu}>
<div className={styles.container}>
<a className={styles.homeLink} href={homeHref}>
{frontpageLinkText}
</a>
<ul className={styles.list}>
{languageSwitcher ? (
<li className={styles.langSwitcher}>{languageSwitcher}</li>
) : null}
{links.map(({ link }, i) => (
<li key={link.href + i}>
<a className={styles.link} href={link.href}>
{link.title}
</a>
</li>
))}
<li className={styles.sessionContainer}>
{user ? (
<>
{user ? (
<Link
href={overview[getLang()]}
className={styles.sessionLink}
prefetch={false}
>
<span data-hj-suppress>{capitalize(user.firstName)}</span>
</Link>
) : null}
<div className={styles.loginSeparator} />
<Link
href={logout[getLang()]}
className={styles.sessionLink}
prefetch={false}
>
{intl.formatMessage({ id: "Log out" })}
</Link>
</>
) : (
<LoginButton
position="hamburger menu"
trackingId="loginStartTopMenu"
className={`${styles.sessionLink} ${styles.loginLink}`}
variant="default"
size="small"
>
{intl.formatMessage({ id: "Log in" })}
</LoginButton>
)}
</li>
</ul>
</div>
</div>
)
}
export async function TopMenuSkeleton() {
const intl = await getIntl()
const links = new Array(5).fill("")
return (
<div className={styles.topMenu}>
<div className={styles.container}>
<ul className={styles.list}>
{links.map((_link, i) => (
<li key={i} className={styles.skeletonWrapper}>
<SkeletonShimmer width="100px" height="16px" />
</li>
))}
<li className={styles.sessionContainer}>
<LoginButton
position="hamburger menu"
trackingId="loginStartTopMenu"
className={`${styles.sessionLink} ${styles.loginLink}`}
variant="default"
size="small"
>
{intl.formatMessage({ id: "Log in" })}
</LoginButton>
</li>
</ul>
</div>
</div>
)
}

View File

@@ -0,0 +1,129 @@
.topMenu {
background-color: #8d3a7c;
color: #fff;
display: none;
font-size: 14px;
padding: 0;
position: relative;
z-index: 1;
}
.container {
box-sizing: content-box;
display: flex;
justify-content: flex-end;
margin: 0 auto;
max-width: 1200px;
padding: 0 10px;
}
.homeLink {
display: none;
}
.list {
display: flex;
list-style: none;
line-height: 22.4px;
}
.link {
color: #fff;
display: inline-block;
padding: 3px 10px;
text-decoration: none;
font-family:
Helvetica Neue,
Helvetica,
Arial,
sans-serif;
font-size: 13px;
font-weight: 400;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
.langSwitcher {
text-align: center;
position: relative;
display: block;
}
.skeletonWrapper {
padding: 4px 10px;
height: 30px;
align-content: center;
}
@media screen and (min-width: 768px) {
.container {
padding: 0 30px;
}
}
@media screen and (min-width: 950px) {
.topMenu {
background-color: #3d3835;
display: block;
}
.list {
align-items: center;
}
.link {
padding-top: 4px;
padding-bottom: 4px;
}
.loginContainer {
margin-left: 10px;
background-color: #f3f2f1;
}
.loginLink {
padding-left: 30px;
padding-right: 30px;
color: #000;
font-family:
Helvetica Neue,
Helvetica,
Arial,
sans-serif !important;
}
.sessionContainer {
margin-left: 10px;
background-color: #f3f2f1;
height: 100%;
display: flex;
align-items: center;
}
.loginSeparator {
height: 15px;
border-right: 1px solid #000;
}
.sessionLink {
padding: 4px 15px;
color: #000;
font-family:
Helvetica Neue,
Helvetica,
Arial,
sans-serif !important;
font-size: 13px;
font-weight: 400;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
cursor: pointer;
text-align: center;
text-decoration: none;
}
.sessionLink.loginLink {
padding-left: 30px;
padding-right: 30px;
}
}

View File

@@ -0,0 +1,14 @@
.header {
display: grid;
background-color: var(--Main-Grey-White);
position: relative;
z-index: var(--header-z-index);
}
@media screen and (max-width: 950px) {
.header {
position: sticky;
top: 0;
z-index: var(--header-z-index);
}
}

View File

@@ -0,0 +1,52 @@
import { homeHrefs } from "@/constants/homeHrefs"
import { env } from "@/env/server"
import { getCurrentHeader, getName } from "@/lib/trpc/memoizedRequests"
import { getLang } from "@/i18n/serverContext"
import LanguageSwitcher from "./LanguageSwitcher"
import { MainMenu } from "./MainMenu"
import MyPagesMobileDropdown from "./MyPagesMobileDropdown"
import OfflineBanner from "./OfflineBanner"
import TopMenu from "./TopMenu"
import styles from "./header.module.css"
export default async function Header() {
const lang = getLang()
const [data, user] = await Promise.all([getCurrentHeader(lang), getName()])
if (!data?.header) {
return null
}
const homeHref = homeHrefs[env.NODE_ENV][getLang()]
const { frontpageLinkText, logo, menu, topMenu } = data.header
const topMenuMobileLinks = topMenu.links
.filter((link) => link.show_on_mobile)
.sort((a, b) => (a.sort_order_mobile < b.sort_order_mobile ? 1 : -1))
return (
<header className={styles.header} role="banner">
<OfflineBanner />
<TopMenu
frontpageLinkText={frontpageLinkText}
homeHref={homeHref}
links={topMenu.links}
languageSwitcher={<LanguageSwitcher />}
/>
<MainMenu
frontpageLinkText={frontpageLinkText}
homeHref={homeHref}
links={menu.links}
logo={logo}
topMenuMobileLinks={topMenuMobileLinks}
languageSwitcher={<LanguageSwitcher />}
myPagesMobileDropdown={<MyPagesMobileDropdown />}
bookingHref={homeHref}
user={user}
/>
</header>
)
}