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:
committed by
Linus Flood
parent
667cab6fb6
commit
80100e7631
@@ -0,0 +1,14 @@
|
||||
.avatar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
border-radius: var(--Corner-radius-Rounded);
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
background-color: var(--UI-Grey-40);
|
||||
}
|
||||
|
||||
.initials {
|
||||
background-color: var(--Base-Icon-Low-contrast);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { PersonIcon } from "@/components/Icons"
|
||||
import Image from "@/components/Image"
|
||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||
|
||||
import styles from "./avatar.module.css"
|
||||
|
||||
import type { AvatarProps } from "@/types/components/header/avatar"
|
||||
|
||||
export default function Avatar({ image, initials }: AvatarProps) {
|
||||
let classNames = [styles.avatar]
|
||||
let element = <PersonIcon color="white" />
|
||||
if (image) {
|
||||
classNames.push(styles.image)
|
||||
element = <Image src={image.src} alt={image.alt} width={28} height={28} />
|
||||
} else if (initials) {
|
||||
classNames.push(styles.initials)
|
||||
element = (
|
||||
<Footnote type="label" color="white" textTransform="uppercase" asChild>
|
||||
<span>{initials}</span>
|
||||
</Footnote>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<span data-hj-suppress className={classNames.join(" ")}>
|
||||
{element}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import styles from "./menuButton.module.css"
|
||||
|
||||
import type { MainMenuButtonProps } from "@/types/components/header/mainMenuButton"
|
||||
|
||||
export default function MainMenuButton({
|
||||
className = "",
|
||||
...props
|
||||
}: MainMenuButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.menuButton} ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
.menuButton {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
background-color: transparent;
|
||||
color: var(--Base-Text-High-contrast);
|
||||
border-width: 0;
|
||||
padding: var(--Spacing-x-half) 0;
|
||||
cursor: pointer;
|
||||
font-family: var(--typography-Body-Bold-fontFamily);
|
||||
font-weight: 500; /* Should be fixed when variables starts working: var(--typography-Body-Bold-fontWeight); */
|
||||
font-size: var(--typography-Body-Bold-fontSize);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { Dialog, Modal } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
import { useMediaQuery } from "usehooks-ts"
|
||||
|
||||
import useDropdownStore from "@/stores/main-menu"
|
||||
|
||||
import LanguageSwitcher from "@/components/LanguageSwitcher"
|
||||
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
|
||||
|
||||
import HeaderLink from "../../HeaderLink"
|
||||
import TopLink from "../../TopLink"
|
||||
|
||||
import styles from "./mobileMenu.module.css"
|
||||
|
||||
import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown"
|
||||
import type { MobileMenuProps } from "@/types/components/header/mobileMenu"
|
||||
import { IconName } from "@/types/components/icon"
|
||||
|
||||
export default function MobileMenu({
|
||||
children,
|
||||
topLink,
|
||||
isLoggedIn,
|
||||
}: React.PropsWithChildren<MobileMenuProps>) {
|
||||
const intl = useIntl()
|
||||
const {
|
||||
toggleDropdown,
|
||||
isHamburgerMenuOpen,
|
||||
isMyPagesMobileMenuOpen,
|
||||
isHeaderLanguageSwitcherMobileOpen,
|
||||
isFooterLanguageSwitcherOpen,
|
||||
} = useDropdownStore()
|
||||
|
||||
const isHamburgerExtended =
|
||||
isHamburgerMenuOpen ||
|
||||
isMyPagesMobileMenuOpen ||
|
||||
isHeaderLanguageSwitcherMobileOpen ||
|
||||
isFooterLanguageSwitcherOpen
|
||||
|
||||
const isAboveMobile = useMediaQuery("(min-width: 768px)")
|
||||
useEffect(() => {
|
||||
if (isAboveMobile && isHamburgerMenuOpen) {
|
||||
toggleDropdown(DropdownTypeEnum.HamburgerMenu)
|
||||
}
|
||||
}, [isAboveMobile, isHamburgerMenuOpen, toggleDropdown])
|
||||
|
||||
useHandleKeyUp((event: KeyboardEvent) => {
|
||||
if (event.key === "Escape" && isHamburgerMenuOpen) {
|
||||
toggleDropdown(DropdownTypeEnum.HamburgerMenu)
|
||||
}
|
||||
})
|
||||
|
||||
// Making sure the menu is always opened at the top of the page, just below the header.
|
||||
useEffect(() => {
|
||||
if (isHamburgerMenuOpen) {
|
||||
window.scrollTo({ top: 0, behavior: "instant" })
|
||||
}
|
||||
}, [isHamburgerMenuOpen])
|
||||
|
||||
const closeMsg = intl.formatMessage({
|
||||
id: "Close menu",
|
||||
})
|
||||
const openMsg = intl.formatMessage({
|
||||
id: "Open menu",
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.hamburger} ${isHamburgerExtended ? styles.isExpanded : ""}`}
|
||||
aria-label={isHamburgerExtended ? closeMsg : openMsg}
|
||||
onClick={() => toggleDropdown(DropdownTypeEnum.HamburgerMenu)}
|
||||
>
|
||||
<span className={styles.bar} />
|
||||
</button>
|
||||
<Modal className={styles.modal} isOpen={isHamburgerMenuOpen}>
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
aria-label={intl.formatMessage({ id: "Menu" })}
|
||||
>
|
||||
{children}
|
||||
<footer className={styles.footer}>
|
||||
<HeaderLink href="#" iconName={IconName.Search}>
|
||||
{intl.formatMessage({ id: "Find booking" })}
|
||||
</HeaderLink>
|
||||
<TopLink isLoggedIn={isLoggedIn} topLink={topLink} iconSize={20} />
|
||||
<HeaderLink href="#" iconName={IconName.Service}>
|
||||
{intl.formatMessage({ id: "Customer service" })}
|
||||
</HeaderLink>
|
||||
<LanguageSwitcher type="mobileHeader" />
|
||||
</footer>
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function MobileMenuSkeleton() {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
className={styles.hamburger}
|
||||
aria-label={intl.formatMessage({
|
||||
id: "Open menu",
|
||||
})}
|
||||
>
|
||||
<span className={styles.bar} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
@keyframes slide-in {
|
||||
from {
|
||||
right: -100vw;
|
||||
}
|
||||
|
||||
to {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.hamburger {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
justify-self: flex-start;
|
||||
padding: 11px 8px 16px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.bar,
|
||||
.bar::after,
|
||||
.bar::before {
|
||||
background: var(--Base-Text-High-contrast);
|
||||
border-radius: 2.3px;
|
||||
display: inline-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) + 1px
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
||||
.modal[data-entering] {
|
||||
animation: slide-in 0.3s;
|
||||
}
|
||||
.modal[data-exiting] {
|
||||
animation: slide-in 0.3s reverse;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
display: grid;
|
||||
align-content: space-between;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: var(--Base-Surface-Subtle-Normal);
|
||||
padding: var(--Spacing-x4) var(--Spacing-x2);
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.hamburger,
|
||||
.modal {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { getHeader } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import { auth } from "@/auth"
|
||||
import { isValidSession } from "@/utils/session"
|
||||
|
||||
import MobileMenu from "../MobileMenu"
|
||||
|
||||
export default async function MobileMenuWrapper({
|
||||
children,
|
||||
}: React.PropsWithChildren) {
|
||||
// preloaded
|
||||
const header = await getHeader()
|
||||
const session = await auth()
|
||||
|
||||
if (!header) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<MobileMenu
|
||||
topLink={header.data.topLink}
|
||||
isLoggedIn={isValidSession(session)}
|
||||
>
|
||||
{children}
|
||||
</MobileMenu>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
"use client"
|
||||
|
||||
import { useRef } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import useDropdownStore from "@/stores/main-menu"
|
||||
|
||||
import { ChevronDownSmallIcon } from "@/components/Icons"
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import useClickOutside from "@/hooks/useClickOutside"
|
||||
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
|
||||
import { type FriendsMembership, getInitials } from "@/utils/user"
|
||||
|
||||
import Avatar from "../Avatar"
|
||||
import MainMenuButton from "../MainMenuButton"
|
||||
import MyPagesMenuContent from "../MyPagesMenuContent"
|
||||
|
||||
import styles from "./myPagesMenu.module.css"
|
||||
|
||||
import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown"
|
||||
import type { User } from "@/types/user"
|
||||
import type { LoyaltyLevel } from "@/server/routers/contentstack/loyaltyLevel/output"
|
||||
|
||||
export type MyPagesMenuProps = {
|
||||
user: Pick<User, "firstName" | "lastName">
|
||||
membership?: FriendsMembership | null
|
||||
membershipLevel: LoyaltyLevel | null
|
||||
}
|
||||
|
||||
export default function MyPagesMenu({
|
||||
user,
|
||||
membership,
|
||||
membershipLevel,
|
||||
}: MyPagesMenuProps) {
|
||||
const intl = useIntl()
|
||||
const myPagesMenuRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const { toggleDropdown, isMyPagesMenuOpen } = useDropdownStore()
|
||||
|
||||
useHandleKeyUp((event: KeyboardEvent) => {
|
||||
if (event.key === "Escape" && isMyPagesMenuOpen) {
|
||||
toggleDropdown(DropdownTypeEnum.MyPagesMenu)
|
||||
}
|
||||
})
|
||||
|
||||
useClickOutside(myPagesMenuRef, isMyPagesMenuOpen, () => {
|
||||
toggleDropdown(DropdownTypeEnum.MyPagesMenu)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={styles.myPagesMenu} ref={myPagesMenuRef}>
|
||||
<MainMenuButton
|
||||
onClick={() => toggleDropdown(DropdownTypeEnum.MyPagesMenu)}
|
||||
>
|
||||
<Avatar initials={getInitials(user.firstName, user.lastName)} />
|
||||
<Body textTransform="bold" color="baseTextHighContrast" asChild>
|
||||
<span data-hj-suppress>
|
||||
{intl.formatMessage(
|
||||
{ id: "Hi {firstName}!" },
|
||||
{ firstName: user.firstName }
|
||||
)}
|
||||
</span>
|
||||
</Body>
|
||||
<ChevronDownSmallIcon
|
||||
className={`${styles.chevron} ${isMyPagesMenuOpen ? styles.isExpanded : ""}`}
|
||||
color="red"
|
||||
/>
|
||||
</MainMenuButton>
|
||||
{isMyPagesMenuOpen ? (
|
||||
<div className={styles.dropdown}>
|
||||
<MyPagesMenuContent
|
||||
membershipLevel={membershipLevel}
|
||||
user={user}
|
||||
membership={membership}
|
||||
toggleOpenStateFn={() =>
|
||||
toggleDropdown(DropdownTypeEnum.MyPagesMenu)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function MyPagesMenuSkeleton() {
|
||||
return (
|
||||
<div className={styles.myPagesMenu}>
|
||||
<MainMenuButton>
|
||||
<Avatar />
|
||||
<SkeletonShimmer width="10ch" />
|
||||
<ChevronDownSmallIcon className={`${styles.chevron}`} color="red" />
|
||||
</MainMenuButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
.myPagesMenu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.myPagesMenu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.chevron.isExpanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
top: calc(
|
||||
3.5rem - 2px
|
||||
); /* 3.5rem is the height of the main menu + bottom padding. */
|
||||
right: 0;
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
box-shadow: 0 0 0.875rem 0.375rem rgba(0, 0, 0, 0.1);
|
||||
min-width: 20rem;
|
||||
z-index: var(--menu-overlay-z-index);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { logout } from "@/constants/routes/handleAuth"
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import { ArrowRightIcon } from "@/components/Icons"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { useTrapFocus } from "@/hooks/useTrapFocus"
|
||||
|
||||
import styles from "./myPagesMenuContent.module.css"
|
||||
|
||||
import type { MyPagesMenuProps } from "../MyPagesMenu"
|
||||
|
||||
type Props = MyPagesMenuProps & { toggleOpenStateFn: () => void }
|
||||
|
||||
export default function MyPagesMenuContent({
|
||||
membership,
|
||||
toggleOpenStateFn,
|
||||
user,
|
||||
membershipLevel,
|
||||
}: Props) {
|
||||
const intl = useIntl()
|
||||
const myPagesMenuContentRef = useTrapFocus()
|
||||
const { data: myPagesNavigation } = useMyPagesNavigation()
|
||||
|
||||
const primaryLinks = myPagesNavigation?.primaryLinks ?? []
|
||||
const secondaryLinks = myPagesNavigation?.secondaryLinks ?? []
|
||||
|
||||
const membershipPoints = membership?.currentPoints
|
||||
const introClassName =
|
||||
membershipLevel && membershipPoints
|
||||
? `${styles.intro}`
|
||||
: `${styles.intro} ${styles.noMembership}`
|
||||
|
||||
if (primaryLinks.length === 0 && secondaryLinks.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className={styles.myPagesMenuContent} ref={myPagesMenuContentRef}>
|
||||
<div className={introClassName}>
|
||||
<Subtitle type="two" className={styles.userName}>
|
||||
{intl.formatMessage(
|
||||
{ id: "Hi {firstName}!" },
|
||||
{ firstName: user.firstName }
|
||||
)}
|
||||
</Subtitle>
|
||||
{membershipLevel && membershipPoints ? (
|
||||
<Caption className={styles.friendTypeWrapper}>
|
||||
<span className={styles.friendType}>{membershipLevel.name}</span>
|
||||
<span>
|
||||
{intl.formatMessage(
|
||||
{ id: "{pointsAmount, number} points" },
|
||||
{ pointsAmount: membershipPoints }
|
||||
)}
|
||||
</span>
|
||||
</Caption>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<ul className={styles.groups}>
|
||||
<li>
|
||||
<Divider color="subtle" className={styles.divider} />
|
||||
|
||||
<PrimaryLinks toggleOpenStateFn={toggleOpenStateFn} />
|
||||
|
||||
<Divider color="subtle" className={styles.divider} />
|
||||
<SecondaryLinks toggleOpenStateFn={toggleOpenStateFn} />
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
function PrimaryLinks({
|
||||
toggleOpenStateFn,
|
||||
}: {
|
||||
toggleOpenStateFn: () => void
|
||||
}) {
|
||||
const { data: myPagesNavigation } = useMyPagesNavigation()
|
||||
|
||||
const primaryLinks = myPagesNavigation?.primaryLinks ?? []
|
||||
|
||||
return (
|
||||
<ul className={styles.menuItems}>
|
||||
{primaryLinks.map((link, i) => (
|
||||
<li key={link.href + i}>
|
||||
<Link
|
||||
href={link.href}
|
||||
onClick={toggleOpenStateFn}
|
||||
variant="menu"
|
||||
weight={"bold"}
|
||||
className={styles.link}
|
||||
>
|
||||
{link.text}
|
||||
<ArrowRightIcon className={styles.arrow} color="burgundy" />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
function SecondaryLinks({
|
||||
toggleOpenStateFn,
|
||||
}: {
|
||||
toggleOpenStateFn: () => void
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const { data: myPagesNavigation } = useMyPagesNavigation()
|
||||
|
||||
const secondaryLinks = myPagesNavigation?.secondaryLinks ?? []
|
||||
|
||||
return (
|
||||
<ul className={styles.menuItems}>
|
||||
{secondaryLinks.map((link, i) => (
|
||||
<li key={link.href + i}>
|
||||
<Link
|
||||
href={link.href}
|
||||
onClick={toggleOpenStateFn}
|
||||
variant="menu"
|
||||
className={styles.link}
|
||||
>
|
||||
{link.text}
|
||||
<ArrowRightIcon className={styles.arrow} color="burgundy" />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
<li>
|
||||
<Link
|
||||
href={logout[lang]}
|
||||
prefetch={false}
|
||||
variant="menu"
|
||||
className={styles.link}
|
||||
>
|
||||
{intl.formatMessage({ id: "Log out" })}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
const useMyPagesNavigation = () => {
|
||||
const lang = useLang()
|
||||
return trpc.navigation.myPages.useQuery({
|
||||
lang: lang,
|
||||
})
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
.myPagesMenuContent {
|
||||
padding: var(--Spacing-x3) var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.intro {
|
||||
padding: 0 var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.myPagesMenuContent .friendTypeWrapper {
|
||||
color: var(--UI-Text-Medium-contrast);
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: var(--Spacing-x2) 0;
|
||||
}
|
||||
|
||||
.friendType {
|
||||
font-family: var(--typography-Title-5-fontFamily);
|
||||
letter-spacing: var(--typography-Title-5-letterSpacing);
|
||||
font-size: var(--typography-Caption-Bold-fontSize);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.friendType::after {
|
||||
content: " · ";
|
||||
display: inline;
|
||||
padding: 0 var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.groups,
|
||||
.menuItems {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.link:not(:hover) .arrow {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.myPagesMenuContent {
|
||||
padding: var(--Spacing-x2) var(--Spacing-x4);
|
||||
}
|
||||
.intro.noMembership,
|
||||
.userName {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { MembershipLevelEnum } from "@/constants/membershipLevels"
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import LoginButton from "@/components/LoginButton"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import Avatar from "../Avatar"
|
||||
import MyPagesMenu, { MyPagesMenuSkeleton } from "../MyPagesMenu"
|
||||
import MyPagesMobileMenu, {
|
||||
MyPagesMobileMenuSkeleton,
|
||||
} from "../MyPagesMobileMenu"
|
||||
|
||||
import styles from "./myPagesMenuWrapper.module.css"
|
||||
|
||||
export default function MyPagesMenuWrapper() {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
const { data: user, isLoading: isLoadingUser } = trpc.user.name.useQuery()
|
||||
const { data: membership, isLoading: isLoadingMembership } =
|
||||
trpc.user.safeMembershipLevel.useQuery()
|
||||
|
||||
const { data: membershipLevel, isLoading: isLoadingMembershipLevel } =
|
||||
trpc.contentstack.loyaltyLevels.byLevel.useQuery(
|
||||
{
|
||||
lang: lang,
|
||||
level: MembershipLevelEnum[membership?.membershipLevel ?? "L1"],
|
||||
},
|
||||
{
|
||||
enabled: !!membership?.membershipLevel,
|
||||
}
|
||||
)
|
||||
|
||||
if (isLoadingUser || isLoadingMembership || isLoadingMembershipLevel) {
|
||||
return <MyPagesMenuWrapperSkeleton />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{user ? (
|
||||
<>
|
||||
<MyPagesMenu
|
||||
membershipLevel={membershipLevel ?? null}
|
||||
membership={membership}
|
||||
user={user}
|
||||
/>
|
||||
<MyPagesMobileMenu
|
||||
membershipLevel={membershipLevel ?? null}
|
||||
membership={membership}
|
||||
user={user}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<LoginButton
|
||||
className={styles.loginLink}
|
||||
aria-label={intl.formatMessage({ id: "Log in/Join" })}
|
||||
position="top menu"
|
||||
trackingId="loginStartNewTopMenu"
|
||||
>
|
||||
<Avatar />
|
||||
<span className={styles.loginText}>
|
||||
{intl.formatMessage({ id: "Log in/Join" })}
|
||||
</span>
|
||||
</LoginButton>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function MyPagesMenuWrapperSkeleton() {
|
||||
return (
|
||||
<div>
|
||||
<MyPagesMenuSkeleton />
|
||||
<MyPagesMobileMenuSkeleton />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
.loginLink {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.loginText {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { Dialog, Modal } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
import { useMediaQuery } from "usehooks-ts"
|
||||
|
||||
import useDropdownStore from "@/stores/main-menu"
|
||||
|
||||
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
|
||||
import { getInitials } from "@/utils/user"
|
||||
|
||||
import Avatar from "../Avatar"
|
||||
import MainMenuButton from "../MainMenuButton"
|
||||
import MyPagesMenuContent from "../MyPagesMenuContent"
|
||||
|
||||
import styles from "./myPagesMobileMenu.module.css"
|
||||
|
||||
import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown"
|
||||
import type { MyPagesMenuProps } from "../MyPagesMenu"
|
||||
|
||||
export default function MyPagesMobileMenu({
|
||||
membershipLevel,
|
||||
membership,
|
||||
user,
|
||||
}: MyPagesMenuProps) {
|
||||
const intl = useIntl()
|
||||
const { isMyPagesMobileMenuOpen, toggleDropdown } = useDropdownStore()
|
||||
|
||||
useHandleKeyUp((event: KeyboardEvent) => {
|
||||
if (event.key === "Escape" && isMyPagesMobileMenuOpen) {
|
||||
toggleDropdown(DropdownTypeEnum.MyPagesMobileMenu)
|
||||
}
|
||||
})
|
||||
|
||||
const isAboveMobile = useMediaQuery("(min-width: 768px)")
|
||||
useEffect(() => {
|
||||
if (isAboveMobile && isMyPagesMobileMenuOpen) {
|
||||
toggleDropdown(DropdownTypeEnum.MyPagesMobileMenu)
|
||||
}
|
||||
}, [isAboveMobile, isMyPagesMobileMenuOpen, toggleDropdown])
|
||||
|
||||
// Making sure the menu is always opened at the top of the page, just below the header.
|
||||
useEffect(() => {
|
||||
if (isMyPagesMobileMenuOpen) {
|
||||
window.scrollTo({ top: 0, behavior: "instant" })
|
||||
}
|
||||
}, [isMyPagesMobileMenuOpen])
|
||||
|
||||
return (
|
||||
<div className={styles.myPagesMobileMenu}>
|
||||
<MainMenuButton
|
||||
className={styles.button}
|
||||
onClick={() => toggleDropdown(DropdownTypeEnum.MyPagesMobileMenu)}
|
||||
aria-label={intl.formatMessage({ id: "Open my pages menu" })}
|
||||
>
|
||||
<Avatar initials={getInitials(user.firstName, user.lastName)} />
|
||||
</MainMenuButton>
|
||||
<Modal className={styles.modal} isOpen={isMyPagesMobileMenuOpen}>
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
aria-label={intl.formatMessage({ id: "My pages menu" })}
|
||||
>
|
||||
<MyPagesMenuContent
|
||||
membershipLevel={membershipLevel}
|
||||
membership={membership}
|
||||
user={user}
|
||||
toggleOpenStateFn={() =>
|
||||
toggleDropdown(DropdownTypeEnum.MyPagesMobileMenu)
|
||||
}
|
||||
/>
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function MyPagesMobileMenuSkeleton() {
|
||||
return (
|
||||
<div className={styles.myPagesMobileMenu}>
|
||||
<MainMenuButton className={styles.button}>
|
||||
<Avatar />
|
||||
</MainMenuButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
@keyframes slide-in {
|
||||
from {
|
||||
right: -100vw;
|
||||
}
|
||||
|
||||
to {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: calc(
|
||||
var(--main-menu-mobile-height) + var(--sitewide-alert-height) + 1px
|
||||
);
|
||||
right: auto;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
z-index: var(--menu-overlay-z-index);
|
||||
}
|
||||
|
||||
.modal[data-entering] {
|
||||
animation: slide-in 0.3s;
|
||||
}
|
||||
.modal[data-exiting] {
|
||||
animation: slide-in 0.3s reverse;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.myPagesMobileMenu,
|
||||
.modal {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
"use client"
|
||||
|
||||
import useDropdownStore from "@/stores/main-menu"
|
||||
|
||||
import { ArrowRightIcon, ChevronLeftIcon } from "@/components/Icons"
|
||||
import Card from "@/components/TempDesignSystem/Card"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { useTrapFocus } from "@/hooks/useTrapFocus"
|
||||
|
||||
import styles from "./megaMenu.module.css"
|
||||
|
||||
import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown"
|
||||
import type { MegaMenuProps } from "@/types/components/header/megaMenu"
|
||||
|
||||
export default function MegaMenu({
|
||||
isMobile,
|
||||
title,
|
||||
seeAllLink,
|
||||
submenu,
|
||||
card,
|
||||
}: MegaMenuProps) {
|
||||
const { toggleMegaMenu, toggleDropdown, isHamburgerMenuOpen } =
|
||||
useDropdownStore()
|
||||
const megaMenuRef = useTrapFocus()
|
||||
|
||||
function handleNavigate() {
|
||||
toggleMegaMenu(false)
|
||||
if (isHamburgerMenuOpen) {
|
||||
toggleDropdown(DropdownTypeEnum.HamburgerMenu)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className={styles.megaMenu}>
|
||||
{isMobile ? (
|
||||
<div className={styles.backWrapper}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.backButton}
|
||||
onClick={() => toggleMegaMenu(false)}
|
||||
>
|
||||
<ChevronLeftIcon color="red" height={20} width={20} />
|
||||
<Subtitle type="one" color="burgundy" asChild>
|
||||
<span>{title}</span>
|
||||
</Subtitle>
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={styles.megaMenuContent} ref={megaMenuRef}>
|
||||
<div className={styles.seeAllLink}>
|
||||
{seeAllLink?.link ? (
|
||||
<Link
|
||||
href={seeAllLink.link.url}
|
||||
color="burgundy"
|
||||
variant="icon"
|
||||
weight="bold"
|
||||
onClick={handleNavigate}
|
||||
>
|
||||
{seeAllLink.title}
|
||||
<ArrowRightIcon color="burgundy" />
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
<ul className={styles.submenus}>
|
||||
{submenu.map((item) => (
|
||||
<li key={item.title} className={styles.submenusItem}>
|
||||
<Caption
|
||||
type="label"
|
||||
color="uiTextPlaceholder"
|
||||
textTransform="uppercase"
|
||||
asChild
|
||||
>
|
||||
<span className={styles.submenuTitle}>{item.title}</span>
|
||||
</Caption>
|
||||
<ul className={styles.submenu}>
|
||||
{item.links.map(({ title, link }) =>
|
||||
link ? (
|
||||
<li key={title} className={styles.submenuItem}>
|
||||
<Link
|
||||
href={link.url}
|
||||
variant="menu"
|
||||
className={styles.link}
|
||||
onClick={handleNavigate}
|
||||
>
|
||||
{title}
|
||||
<ArrowRightIcon
|
||||
color="burgundy"
|
||||
className={styles.arrow}
|
||||
/>
|
||||
</Link>
|
||||
</li>
|
||||
) : null
|
||||
)}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{card ? (
|
||||
<div className={styles.cardWrapper}>
|
||||
<Card
|
||||
className={styles.card}
|
||||
backgroundImage={card.backgroundImage}
|
||||
bodyText={card.body_text}
|
||||
heading={card.heading}
|
||||
primaryButton={card.primaryButton}
|
||||
secondaryButton={card.secondaryButton}
|
||||
scriptedTopTitle={card.scripted_top_title}
|
||||
onPrimaryButtonClick={handleNavigate}
|
||||
onSecondaryButtonClick={handleNavigate}
|
||||
imageGradient
|
||||
theme="image"
|
||||
height="dynamic"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
.megaMenuContent {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.seeAllLink {
|
||||
display: flex;
|
||||
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x1);
|
||||
background-color: var(--Base-Surface-Secondary-light-Normal);
|
||||
}
|
||||
|
||||
.submenus {
|
||||
list-style: none;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.submenuTitle {
|
||||
padding-left: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.submenu {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.submenuItem {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.submenusItem {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x1);
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.linkIcon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.link:not(:hover) .arrow {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.backWrapper {
|
||||
padding: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.backButton {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x1);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.megaMenu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
.megaMenuContent {
|
||||
flex-grow: 1;
|
||||
grid-template-rows: max-content 1fr;
|
||||
gap: var(--Spacing-x2);
|
||||
align-items: start;
|
||||
}
|
||||
.megaMenuContent:has(.cardWrapper) {
|
||||
grid-template-rows: max-content 1fr max-content;
|
||||
}
|
||||
|
||||
.submenus {
|
||||
padding: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.submenusItem:first-child {
|
||||
padding-bottom: var(--Spacing-x2);
|
||||
border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
|
||||
}
|
||||
.submenusItem:last-child {
|
||||
padding-top: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.cardWrapper {
|
||||
background-color: var(--Base-Surface-Secondary-light-Normal);
|
||||
padding: var(--Spacing-x4) var(--Spacing-x2);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.megaMenuContent {
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-areas:
|
||||
"seeAllLink"
|
||||
"submenus";
|
||||
width: 600px;
|
||||
max-width: calc(100vw - var(--Spacing-x4));
|
||||
}
|
||||
|
||||
.megaMenuContent:has(.cardWrapper) {
|
||||
width: 900px;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-areas:
|
||||
"seeAllLink seeAllLink card"
|
||||
"submenus submenus card";
|
||||
}
|
||||
|
||||
.seeAllLink {
|
||||
grid-area: seeAllLink;
|
||||
}
|
||||
|
||||
.submenus {
|
||||
grid-area: submenus;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.submenusItem:first-child {
|
||||
padding-right: var(--Spacing-x5);
|
||||
border-right: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
|
||||
}
|
||||
.submenusItem:last-child {
|
||||
padding-left: var(--Spacing-x5);
|
||||
}
|
||||
|
||||
.cardWrapper {
|
||||
grid-area: card;
|
||||
}
|
||||
|
||||
.cardWrapper .card {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
"use client"
|
||||
|
||||
import { useRef } from "react"
|
||||
|
||||
import useDropdownStore from "@/stores/main-menu"
|
||||
|
||||
import { ChevronDownSmallIcon, ChevronRightIcon } from "@/components/Icons"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import useClickOutside from "@/hooks/useClickOutside"
|
||||
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
|
||||
|
||||
import MainMenuButton from "../../MainMenuButton"
|
||||
import MegaMenu from "../MegaMenu"
|
||||
|
||||
import styles from "./navigationMenuItem.module.css"
|
||||
|
||||
import type { NavigationMenuItemProps } from "@/types/components/header/navigationMenuItem"
|
||||
|
||||
export default function MenuItem({ item, isMobile }: NavigationMenuItemProps) {
|
||||
const { openMegaMenu, toggleMegaMenu } = useDropdownStore()
|
||||
const megaMenuRef = useRef<HTMLDivElement>(null)
|
||||
const { submenu, title, link, seeAllLink, card } = item
|
||||
const megaMenuTitle = `${title}-${isMobile ? "mobile" : "desktop"}`
|
||||
const isMegaMenuOpen = openMegaMenu === megaMenuTitle
|
||||
|
||||
useHandleKeyUp((event: KeyboardEvent) => {
|
||||
if (event.key === "Escape" && isMegaMenuOpen) {
|
||||
toggleMegaMenu(false)
|
||||
}
|
||||
})
|
||||
|
||||
useClickOutside(megaMenuRef, isMegaMenuOpen && !isMobile, () => {
|
||||
toggleMegaMenu(false)
|
||||
})
|
||||
|
||||
return submenu.length ? (
|
||||
<>
|
||||
<MainMenuButton
|
||||
onClick={() => toggleMegaMenu(megaMenuTitle)}
|
||||
className={`${styles.navigationMenuItem} ${isMobile ? styles.mobile : styles.desktop}`}
|
||||
>
|
||||
{title}
|
||||
{isMobile ? (
|
||||
<ChevronRightIcon
|
||||
className={`${styles.chevron}`}
|
||||
color="red"
|
||||
height={20}
|
||||
width={20}
|
||||
/>
|
||||
) : (
|
||||
<ChevronDownSmallIcon
|
||||
className={`${styles.chevron} ${isMegaMenuOpen ? styles.isExpanded : ""}`}
|
||||
color="red"
|
||||
/>
|
||||
)}
|
||||
</MainMenuButton>
|
||||
<div
|
||||
ref={megaMenuRef}
|
||||
className={`${styles.dropdown} ${isMegaMenuOpen ? styles.isExpanded : ""}`}
|
||||
>
|
||||
{isMegaMenuOpen ? (
|
||||
<MegaMenu
|
||||
isMobile={isMobile}
|
||||
title={title}
|
||||
seeAllLink={seeAllLink}
|
||||
submenu={submenu}
|
||||
card={card}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Link
|
||||
className={`${styles.navigationMenuItem} ${isMobile ? styles.mobile : styles.desktop}`}
|
||||
variant="navigation"
|
||||
weight="bold"
|
||||
href={link!.url}
|
||||
>
|
||||
{title}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
.navigationMenuItem {
|
||||
font-weight: 500; /* Should be fixed when variables starts working: var(--typography-Body-Bold-fontWeight); */
|
||||
}
|
||||
|
||||
.navigationMenuItem.mobile {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: var(--Spacing-x2) 0;
|
||||
font-size: var(--typography-Subtitle-1-Mobile-fontSize);
|
||||
}
|
||||
|
||||
.chevron {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.chevron.isExpanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
z-index: var(--menu-overlay-z-index);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.dropdown {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
top: calc(
|
||||
var(--main-menu-mobile-height) + var(--sitewide-alert-height) + 1px
|
||||
);
|
||||
right: -100vw;
|
||||
bottom: 0;
|
||||
transition: right 0.3s;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.dropdown.isExpanded {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.dropdown {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(
|
||||
3.5rem - 2px
|
||||
); /* 3.5rem is the height of the main menu + bottom padding. */
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
box-shadow: 0 0 14px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.dropdown.isExpanded {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { cx } from "class-variance-authority"
|
||||
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
|
||||
import NavigationMenuItem from "../NavigationMenuItem"
|
||||
|
||||
import styles from "./navigationMenuList.module.css"
|
||||
|
||||
import type { NavigationMenuListProps } from "@/types/components/header/navigationMenuList"
|
||||
|
||||
export default function NavigationMenuList({
|
||||
isMobile,
|
||||
items,
|
||||
}: NavigationMenuListProps) {
|
||||
return (
|
||||
<ul
|
||||
className={`${styles.navigationMenu} ${isMobile ? styles.mobile : styles.desktop}`}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<li key={item.title} className={styles.item}>
|
||||
<NavigationMenuItem isMobile={isMobile} item={item} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export function NavigationMenuListSkeleton() {
|
||||
return (
|
||||
<ul className={cx(styles.navigationMenu, styles.desktop)}>
|
||||
<li className={styles.item}>
|
||||
<SkeletonShimmer width="30ch" />
|
||||
</li>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
.navigationMenu {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x3);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navigationMenu.mobile {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
gap: 0;
|
||||
justify-content: stretch;
|
||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2) var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.navigationMenu.mobile .item {
|
||||
border-bottom: 1px solid var(--Base-Surface-Subtle-Normal);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.navigationMenu.desktop {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { getHeader } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import NavigationMenuList from "./NavigationMenuList"
|
||||
|
||||
import type { NavigationMenuProps } from "@/types/components/header/navigationMenu"
|
||||
|
||||
export default async function NavigationMenu({
|
||||
isMobile,
|
||||
}: NavigationMenuProps) {
|
||||
const header = await getHeader()
|
||||
|
||||
if (!header) {
|
||||
return null
|
||||
}
|
||||
|
||||
const filteredItems = header.data.menuItems.filter(
|
||||
({ link, submenu }) => submenu.length || link
|
||||
)
|
||||
|
||||
if (!filteredItems?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <NavigationMenuList isMobile={isMobile} items={filteredItems} />
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import NextLink from "next/link"
|
||||
import { Suspense } from "react"
|
||||
|
||||
import Image from "@/components/Image"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import { NavigationMenuListSkeleton } from "./NavigationMenu/NavigationMenuList"
|
||||
import { MobileMenuSkeleton } from "./MobileMenu"
|
||||
import MobileMenuWrapper from "./MobileMenuWrapper"
|
||||
import MyPagesMenuWrapper, {
|
||||
MyPagesMenuWrapperSkeleton,
|
||||
} from "./MyPagesMenuWrapper"
|
||||
import NavigationMenu from "./NavigationMenu"
|
||||
|
||||
import styles from "./mainMenu.module.css"
|
||||
|
||||
export default function MainMenu() {
|
||||
return (
|
||||
<div className={styles.mainMenu}>
|
||||
<nav className={styles.nav}>
|
||||
<Suspense fallback={<Logo alt="..." />}>
|
||||
<MainMenuLogo />
|
||||
</Suspense>
|
||||
<div className={styles.menus}>
|
||||
<Suspense fallback={<NavigationMenuListSkeleton />}>
|
||||
<NavigationMenu isMobile={false} />
|
||||
</Suspense>
|
||||
<Suspense fallback={<MyPagesMenuWrapperSkeleton />}>
|
||||
<MyPagesMenuWrapper />
|
||||
</Suspense>
|
||||
<Suspense fallback={<MobileMenuSkeleton />}>
|
||||
<MobileMenuWrapper>
|
||||
<NavigationMenu isMobile={true} />
|
||||
</MobileMenuWrapper>
|
||||
</Suspense>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
async function MainMenuLogo() {
|
||||
const intl = await getIntl()
|
||||
|
||||
return <Logo alt={intl.formatMessage({ id: "Back to scandichotels.com" })} />
|
||||
}
|
||||
|
||||
function Logo({ alt }: { alt: string }) {
|
||||
const lang = getLang()
|
||||
|
||||
return (
|
||||
<NextLink className={styles.logoLink} href={`/${lang}`}>
|
||||
<Image
|
||||
alt={alt}
|
||||
className={styles.logo}
|
||||
height={22}
|
||||
src="/_static/img/scandic-logotype.svg"
|
||||
priority
|
||||
width={103}
|
||||
/>
|
||||
</NextLink>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
.mainMenu {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
padding: var(--Spacing-x2) 0;
|
||||
border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
|
||||
}
|
||||
|
||||
.nav {
|
||||
position: relative;
|
||||
max-width: var(--max-width-navigation);
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x2);
|
||||
max-width: var(--max-width-page);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.menus {
|
||||
display: flex;
|
||||
justify-self: end;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.logoLink {
|
||||
display: inline-flex;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 1.375rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.mainMenu {
|
||||
padding: var(--Spacing-x2) 0;
|
||||
}
|
||||
.nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
.menus {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user