Merge branch 'master' into feature/tracking
This commit is contained in:
@@ -2,5 +2,16 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x1);
|
||||
font-size: var(--typography-Caption-Regular-fontSize);
|
||||
}
|
||||
|
||||
.headerLink:hover {
|
||||
color: var(--Base-Text-High-contrast);
|
||||
}
|
||||
|
||||
.headerLink .icon * {
|
||||
fill: var(--Base-Text-Medium-contrast);
|
||||
}
|
||||
|
||||
.headerLink:hover .icon * {
|
||||
fill: var(--Base-Text-High-contrast);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Link from "next/link"
|
||||
|
||||
import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import styles from "./headerLink.module.css"
|
||||
|
||||
@@ -6,16 +9,19 @@ import type { HeaderLinkProps } from "@/types/components/header/headerLink"
|
||||
|
||||
export default function HeaderLink({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
href,
|
||||
iconName,
|
||||
iconSize = 20,
|
||||
}: HeaderLinkProps) {
|
||||
const Icon = getIconByIconName(iconName)
|
||||
return (
|
||||
<Link
|
||||
color="burgundy"
|
||||
className={`${styles.headerLink} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
<Caption type="regular" color="textMediumContrast" asChild>
|
||||
<Link href={href} className={styles.headerLink}>
|
||||
{Icon ? (
|
||||
<Icon className={styles.icon} width={iconSize} height={iconSize} />
|
||||
) : null}
|
||||
{children}
|
||||
</Link>
|
||||
</Caption>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,25 +3,27 @@
|
||||
import { Suspense, 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 { GiftIcon, SearchIcon, ServiceIcon } from "@/components/Icons"
|
||||
import LanguageSwitcher from "@/components/LanguageSwitcher"
|
||||
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
|
||||
import useMediaQuery from "@/hooks/useMediaQuery"
|
||||
|
||||
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,
|
||||
languageUrls,
|
||||
topLink,
|
||||
isLoggedIn,
|
||||
}: React.PropsWithChildren<MobileMenuProps>) {
|
||||
const intl = useIntl()
|
||||
const {
|
||||
@@ -75,20 +77,13 @@ export default function MobileMenu({
|
||||
className={styles.dialog}
|
||||
aria-label={intl.formatMessage({ id: "Menu" })}
|
||||
>
|
||||
<Suspense fallback={"Loading nav"}>{children}</Suspense>
|
||||
{children}
|
||||
<footer className={styles.footer}>
|
||||
<HeaderLink href="#">
|
||||
<SearchIcon width={20} height={20} color="burgundy" />
|
||||
<HeaderLink href="#" iconName={IconName.Search}>
|
||||
{intl.formatMessage({ id: "Find booking" })}
|
||||
</HeaderLink>
|
||||
{topLink.link ? (
|
||||
<HeaderLink href={topLink.link.url}>
|
||||
<GiftIcon width={20} height={20} color="burgundy" />
|
||||
{topLink.title}
|
||||
</HeaderLink>
|
||||
) : null}
|
||||
<HeaderLink href="#">
|
||||
<ServiceIcon width={20} height={20} color="burgundy" />
|
||||
<TopLink isLoggedIn={isLoggedIn} topLink={topLink} iconSize={20} />
|
||||
<HeaderLink href="#" iconName={IconName.Service}>
|
||||
{intl.formatMessage({ id: "Customer service" })}
|
||||
</HeaderLink>
|
||||
<LanguageSwitcher type="mobileHeader" urls={languageUrls} />
|
||||
@@ -98,3 +93,20 @@ export default function MobileMenu({
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { getHeader, getLanguageSwitcher } from "@/lib/trpc/memoizedRequests"
|
||||
import {
|
||||
getHeader,
|
||||
getLanguageSwitcher,
|
||||
getName,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import MobileMenu from "../MobileMenu"
|
||||
|
||||
@@ -8,13 +12,18 @@ export default async function MobileMenuWrapper({
|
||||
// preloaded
|
||||
const languages = await getLanguageSwitcher()
|
||||
const header = await getHeader()
|
||||
const user = await getName()
|
||||
|
||||
if (!languages || !header) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<MobileMenu languageUrls={languages.urls} topLink={header.data.topLink}>
|
||||
<MobileMenu
|
||||
languageUrls={languages.urls}
|
||||
topLink={header.data.topLink}
|
||||
isLoggedIn={!!user}
|
||||
>
|
||||
{children}
|
||||
</MobileMenu>
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ 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"
|
||||
@@ -47,7 +48,7 @@ export default function MyPagesMenu({
|
||||
onClick={() => toggleDropdown(DropdownTypeEnum.MyPagesMenu)}
|
||||
>
|
||||
<Avatar initials={getInitials(user.firstName, user.lastName)} />
|
||||
<Body textTransform="bold" color="textHighContrast" asChild>
|
||||
<Body textTransform="bold" color="baseTextHighContrast" asChild>
|
||||
<span>
|
||||
{intl.formatMessage({ id: "Hi" })} {user.firstName}!
|
||||
</span>
|
||||
@@ -73,3 +74,15 @@ export default function MyPagesMenu({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function MyPagesMenuSkeleton() {
|
||||
return (
|
||||
<div className={styles.myPagesMenu}>
|
||||
<MainMenuButton>
|
||||
<Avatar />
|
||||
<SkeletonShimmer width="10ch" />
|
||||
<ChevronDownSmallIcon className={`${styles.chevron}`} color="red" />
|
||||
</MainMenuButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@ import LoginButton from "@/components/LoginButton"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import Avatar from "../Avatar"
|
||||
import MyPagesMenu from "../MyPagesMenu"
|
||||
import MyPagesMobileMenu from "../MyPagesMobileMenu"
|
||||
import MyPagesMenu, { MyPagesMenuSkeleton } from "../MyPagesMenu"
|
||||
import MyPagesMobileMenu, {
|
||||
MyPagesMobileMenuSkeleton,
|
||||
} from "../MyPagesMobileMenu"
|
||||
|
||||
import styles from "./myPagesMenuWrapper.module.css"
|
||||
|
||||
@@ -62,3 +64,12 @@ export default async function MyPagesMenuWrapper() {
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function MyPagesMenuWrapperSkeleton() {
|
||||
return (
|
||||
<div>
|
||||
<MyPagesMenuSkeleton />
|
||||
<MyPagesMobileMenuSkeleton />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
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 useMediaQuery from "@/hooks/useMediaQuery"
|
||||
import { getInitials } from "@/utils/user"
|
||||
|
||||
import Avatar from "../Avatar"
|
||||
@@ -76,3 +76,13 @@ export default function MyPagesMobileMenu({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function MyPagesMobileMenuSkeleton() {
|
||||
return (
|
||||
<div className={styles.myPagesMobileMenu}>
|
||||
<MainMenuButton className={styles.button}>
|
||||
<Avatar />
|
||||
</MainMenuButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { cx } from "class-variance-authority"
|
||||
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
|
||||
import NavigationMenuItem from "../NavigationMenuItem"
|
||||
|
||||
import styles from "./navigationMenuList.module.css"
|
||||
@@ -20,3 +24,13 @@ export default function NavigationMenuList({
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export function NavigationMenuListSkeleton() {
|
||||
return (
|
||||
<ul className={cx(styles.navigationMenu, styles.desktop)}>
|
||||
<li className={styles.item}>
|
||||
<SkeletonShimmer width="30ch" />
|
||||
</li>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,37 +5,31 @@ 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 from "./MyPagesMenuWrapper"
|
||||
import MyPagesMenuWrapper, {
|
||||
MyPagesMenuWrapperSkeleton,
|
||||
} from "./MyPagesMenuWrapper"
|
||||
import NavigationMenu from "./NavigationMenu"
|
||||
|
||||
import styles from "./mainMenu.module.css"
|
||||
|
||||
export default async function MainMenu() {
|
||||
const lang = getLang()
|
||||
|
||||
const intl = await getIntl()
|
||||
|
||||
export default function MainMenu() {
|
||||
return (
|
||||
<div className={styles.mainMenu}>
|
||||
<nav className={styles.nav}>
|
||||
<NextLink className={styles.logoLink} href={`/${lang}`}>
|
||||
<Image
|
||||
alt={intl.formatMessage({ id: "Back to scandichotels.com" })}
|
||||
className={styles.logo}
|
||||
height={22}
|
||||
src="/_static/img/scandic-logotype.svg"
|
||||
width={103}
|
||||
/>
|
||||
</NextLink>
|
||||
<Suspense fallback={<Logo alt="..." />}>
|
||||
<MainMenuLogo />
|
||||
</Suspense>
|
||||
<div className={styles.menus}>
|
||||
<Suspense fallback={"Loading nav"}>
|
||||
<Suspense fallback={<NavigationMenuListSkeleton />}>
|
||||
<NavigationMenu isMobile={false} />
|
||||
</Suspense>
|
||||
<Suspense fallback={"Loading profile"}>
|
||||
<Suspense fallback={<MyPagesMenuWrapperSkeleton />}>
|
||||
<MyPagesMenuWrapper />
|
||||
</Suspense>
|
||||
<Suspense fallback={"Loading menu"}>
|
||||
<Suspense fallback={<MobileMenuSkeleton />}>
|
||||
<MobileMenuWrapper>
|
||||
<NavigationMenu isMobile={true} />
|
||||
</MobileMenuWrapper>
|
||||
@@ -45,3 +39,25 @@ export default async function MainMenu() {
|
||||
</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"
|
||||
width={103}
|
||||
/>
|
||||
</NextLink>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.mainMenu {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
padding: var(--Spacing-x2);
|
||||
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||
border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
|
||||
}
|
||||
|
||||
.nav {
|
||||
|
||||
26
components/Header/TopLink/index.tsx
Normal file
26
components/Header/TopLink/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import HeaderLink from "../HeaderLink"
|
||||
|
||||
import type { TopLinkProps } from "@/types/components/header/topLink"
|
||||
import { IconName } from "@/types/components/icon"
|
||||
|
||||
export default function TopLink({
|
||||
isLoggedIn,
|
||||
topLink,
|
||||
iconSize = 16,
|
||||
}: TopLinkProps) {
|
||||
const linkData = isLoggedIn ? topLink.logged_in : topLink.logged_out
|
||||
|
||||
if (!linkData?.link?.url || !linkData?.title) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<HeaderLink
|
||||
href={linkData.link.url}
|
||||
iconName={linkData.icon || IconName.Gift}
|
||||
iconSize={iconSize}
|
||||
>
|
||||
{linkData.title}
|
||||
</HeaderLink>
|
||||
)
|
||||
}
|
||||
@@ -1,21 +1,28 @@
|
||||
import { getHeader, getLanguageSwitcher } from "@/lib/trpc/memoizedRequests"
|
||||
import {
|
||||
getHeader,
|
||||
getLanguageSwitcher,
|
||||
getName,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import { GiftIcon, SearchIcon } from "@/components/Icons"
|
||||
import LanguageSwitcher from "@/components/LanguageSwitcher"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import HeaderLink from "../HeaderLink"
|
||||
import TopLink from "../TopLink"
|
||||
|
||||
import styles from "./topMenu.module.css"
|
||||
|
||||
import { IconName } from "@/types/components/icon"
|
||||
|
||||
export default async function TopMenu() {
|
||||
// cached
|
||||
const intl = await getIntl()
|
||||
// both preloaded
|
||||
const languages = await getLanguageSwitcher()
|
||||
const header = await getHeader()
|
||||
const user = await getName()
|
||||
|
||||
if (!languages || !header) {
|
||||
return null
|
||||
@@ -24,28 +31,27 @@ export default async function TopMenu() {
|
||||
return (
|
||||
<div className={styles.topMenu}>
|
||||
<div className={styles.content}>
|
||||
{header.data.topLink.link ? (
|
||||
<Caption type="regular" color="textMediumContrast" asChild>
|
||||
<Link
|
||||
href={header.data.topLink.link.url}
|
||||
color="peach80"
|
||||
variant="icon"
|
||||
>
|
||||
<GiftIcon width={20} height={20} />
|
||||
{header.data.topLink.title}
|
||||
</Link>
|
||||
</Caption>
|
||||
) : null}
|
||||
<TopLink isLoggedIn={!!user} topLink={header.data.topLink} />
|
||||
<div className={styles.options}>
|
||||
<LanguageSwitcher type="desktopHeader" urls={languages.urls} />
|
||||
|
||||
<Caption type="regular" color="textMediumContrast" asChild>
|
||||
<Link href="#" color="peach80" variant="icon">
|
||||
<SearchIcon width={20} height={20} />
|
||||
<HeaderLink href="#" iconName={IconName.Search}>
|
||||
{intl.formatMessage({ id: "Find booking" })}
|
||||
</Link>
|
||||
</HeaderLink>
|
||||
</Caption>
|
||||
<HeaderLink href="#"></HeaderLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function TopMenuSkeleton() {
|
||||
return (
|
||||
<div className={styles.topMenu}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.options}>
|
||||
<SkeletonShimmer width="25ch" height="1.2em" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
import { Suspense } from "react"
|
||||
|
||||
import { getHeader, getLanguageSwitcher } from "@/lib/trpc/memoizedRequests"
|
||||
import {
|
||||
getHeader,
|
||||
getLanguageSwitcher,
|
||||
getName,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import MainMenu from "./MainMenu"
|
||||
import TopMenu from "./TopMenu"
|
||||
import TopMenu, { TopMenuSkeleton } from "./TopMenu"
|
||||
|
||||
import styles from "./header.module.css"
|
||||
|
||||
export default function Header() {
|
||||
export default async function Header() {
|
||||
void getHeader()
|
||||
void getLanguageSwitcher()
|
||||
void getName()
|
||||
|
||||
return (
|
||||
<header className={styles.header}>
|
||||
<Suspense fallback="Loading top menu">
|
||||
<Suspense fallback={<TopMenuSkeleton />}>
|
||||
<TopMenu />
|
||||
</Suspense>
|
||||
<Suspense fallback="Loading main menu">
|
||||
<MainMenu />
|
||||
</Suspense>
|
||||
<MainMenu />
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user