feat(SW-572): Added support for logged in and logged out variants of the top link inside the header

This commit is contained in:
Erik Tiekstra
2024-11-11 14:03:08 +01:00
parent cc9f0509a1
commit d732138696
17 changed files with 215 additions and 68 deletions

View File

@@ -106,7 +106,7 @@
--max-width-navigation: 89.5rem; --max-width-navigation: 89.5rem;
--main-menu-mobile-height: 75px; --main-menu-mobile-height: 75px;
--main-menu-desktop-height: 129px; --main-menu-desktop-height: 125px;
--booking-widget-mobile-height: 75px; --booking-widget-mobile-height: 75px;
--booking-widget-desktop-height: 77px; --booking-widget-desktop-height: 77px;
--hotel-page-map-desktop-width: 23.75rem; --hotel-page-map-desktop-width: 23.75rem;

View File

@@ -2,5 +2,16 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--Spacing-x1); 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);
} }

View File

@@ -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" import styles from "./headerLink.module.css"
@@ -6,16 +9,19 @@ import type { HeaderLinkProps } from "@/types/components/header/headerLink"
export default function HeaderLink({ export default function HeaderLink({
children, children,
className, href,
...props iconName,
iconSize = 20,
}: HeaderLinkProps) { }: HeaderLinkProps) {
const Icon = getIconByIconName(iconName)
return ( return (
<Link <Caption type="regular" color="textMediumContrast" asChild>
color="burgundy" <Link href={href} className={styles.headerLink}>
className={`${styles.headerLink} ${className}`} {Icon ? (
{...props} <Icon className={styles.icon} width={iconSize} height={iconSize} />
> ) : null}
{children} {children}
</Link> </Link>
</Caption>
) )
} }

View File

@@ -7,21 +7,23 @@ import { useMediaQuery } from "usehooks-ts"
import useDropdownStore from "@/stores/main-menu" import useDropdownStore from "@/stores/main-menu"
import { GiftIcon, SearchIcon, ServiceIcon } from "@/components/Icons"
import LanguageSwitcher from "@/components/LanguageSwitcher" import LanguageSwitcher from "@/components/LanguageSwitcher"
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
import HeaderLink from "../../HeaderLink" import HeaderLink from "../../HeaderLink"
import TopLink from "../../TopLink"
import styles from "./mobileMenu.module.css" import styles from "./mobileMenu.module.css"
import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown" import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown"
import type { MobileMenuProps } from "@/types/components/header/mobileMenu" import type { MobileMenuProps } from "@/types/components/header/mobileMenu"
import { IconName } from "@/types/components/icon"
export default function MobileMenu({ export default function MobileMenu({
children, children,
languageUrls, languageUrls,
topLink, topLink,
isLoggedIn,
}: React.PropsWithChildren<MobileMenuProps>) { }: React.PropsWithChildren<MobileMenuProps>) {
const intl = useIntl() const intl = useIntl()
const { const {
@@ -77,18 +79,11 @@ export default function MobileMenu({
> >
<Suspense fallback={"Loading nav"}>{children}</Suspense> <Suspense fallback={"Loading nav"}>{children}</Suspense>
<footer className={styles.footer}> <footer className={styles.footer}>
<HeaderLink href="#"> <HeaderLink href="#" iconName={IconName.Search}>
<SearchIcon width={20} height={20} color="burgundy" />
{intl.formatMessage({ id: "Find booking" })} {intl.formatMessage({ id: "Find booking" })}
</HeaderLink> </HeaderLink>
{topLink.link ? ( <TopLink isLoggedIn={isLoggedIn} topLink={topLink} iconSize={20} />
<HeaderLink href={topLink.link.url}> <HeaderLink href="#" iconName={IconName.Service}>
<GiftIcon width={20} height={20} color="burgundy" />
{topLink.title}
</HeaderLink>
) : null}
<HeaderLink href="#">
<ServiceIcon width={20} height={20} color="burgundy" />
{intl.formatMessage({ id: "Customer service" })} {intl.formatMessage({ id: "Customer service" })}
</HeaderLink> </HeaderLink>
<LanguageSwitcher type="mobileHeader" urls={languageUrls} /> <LanguageSwitcher type="mobileHeader" urls={languageUrls} />

View File

@@ -1,4 +1,8 @@
import { getHeader, getLanguageSwitcher } from "@/lib/trpc/memoizedRequests" import {
getHeader,
getLanguageSwitcher,
getName,
} from "@/lib/trpc/memoizedRequests"
import MobileMenu from "../MobileMenu" import MobileMenu from "../MobileMenu"
@@ -8,13 +12,18 @@ export default async function MobileMenuWrapper({
// preloaded // preloaded
const languages = await getLanguageSwitcher() const languages = await getLanguageSwitcher()
const header = await getHeader() const header = await getHeader()
const user = await getName()
if (!languages || !header) { if (!languages || !header) {
return null return null
} }
return ( return (
<MobileMenu languageUrls={languages.urls} topLink={header.data.topLink}> <MobileMenu
languageUrls={languages.urls}
topLink={header.data.topLink}
isLoggedIn={!!user}
>
{children} {children}
</MobileMenu> </MobileMenu>
) )

View 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>
)
}

View File

@@ -1,21 +1,27 @@
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 LanguageSwitcher from "@/components/LanguageSwitcher"
import Link from "@/components/TempDesignSystem/Link"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import { getIntl } from "@/i18n" import { getIntl } from "@/i18n"
import HeaderLink from "../HeaderLink" import HeaderLink from "../HeaderLink"
import TopLink from "../TopLink"
import styles from "./topMenu.module.css" import styles from "./topMenu.module.css"
import { IconName } from "@/types/components/icon"
export default async function TopMenu() { export default async function TopMenu() {
// cached // cached
const intl = await getIntl() const intl = await getIntl()
// both preloaded // both preloaded
const languages = await getLanguageSwitcher() const languages = await getLanguageSwitcher()
const header = await getHeader() const header = await getHeader()
const user = await getName()
if (!languages || !header) { if (!languages || !header) {
return null return null
@@ -24,28 +30,15 @@ export default async function TopMenu() {
return ( return (
<div className={styles.topMenu}> <div className={styles.topMenu}>
<div className={styles.content}> <div className={styles.content}>
{header.data.topLink.link ? ( <TopLink isLoggedIn={!!user} topLink={header.data.topLink} />
<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}
<div className={styles.options}> <div className={styles.options}>
<LanguageSwitcher type="desktopHeader" urls={languages.urls} /> <LanguageSwitcher type="desktopHeader" urls={languages.urls} />
<Caption type="regular" color="textMediumContrast" asChild> <Caption type="regular" color="textMediumContrast" asChild>
<Link href="#" color="peach80" variant="icon"> <HeaderLink href="#" iconName={IconName.Search}>
<SearchIcon width={20} height={20} />
{intl.formatMessage({ id: "Find booking" })} {intl.formatMessage({ id: "Find booking" })}
</Link> </HeaderLink>
</Caption> </Caption>
<HeaderLink href="#"></HeaderLink>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,10 @@
import { Suspense } from "react" import { Suspense } from "react"
import { getHeader, getLanguageSwitcher } from "@/lib/trpc/memoizedRequests" import {
getHeader,
getLanguageSwitcher,
getName,
} from "@/lib/trpc/memoizedRequests"
import MainMenu from "./MainMenu" import MainMenu from "./MainMenu"
import TopMenu from "./TopMenu" import TopMenu from "./TopMenu"
@@ -10,6 +14,8 @@ import styles from "./header.module.css"
export default function Header() { export default function Header() {
void getHeader() void getHeader()
void getLanguageSwitcher() void getLanguageSwitcher()
void getName()
return ( return (
<header className={styles.header}> <header className={styles.header}>
<Suspense fallback="Loading top menu"> <Suspense fallback="Loading top menu">

View File

@@ -77,6 +77,7 @@ import {
PhoneIcon, PhoneIcon,
PlusCircleIcon, PlusCircleIcon,
PlusIcon, PlusIcon,
PriceTagIcon,
RestaurantIcon, RestaurantIcon,
RoomServiceIcon, RoomServiceIcon,
SaunaIcon, SaunaIcon,
@@ -101,7 +102,9 @@ import {
import { IconName, IconProps } from "@/types/components/icon" import { IconName, IconProps } from "@/types/components/icon"
export function getIconByIconName(icon?: IconName): FC<IconProps> | null { export function getIconByIconName(
icon: IconName | null = null
): FC<IconProps> | null {
switch (icon) { switch (icon) {
case IconName.Accesories: case IconName.Accesories:
return AccesoriesIcon return AccesoriesIcon
@@ -253,6 +256,8 @@ export function getIconByIconName(icon?: IconName): FC<IconProps> | null {
return PlusIcon return PlusIcon
case IconName.PlusCircle: case IconName.PlusCircle:
return PlusCircleIcon return PlusCircleIcon
case IconName.PriceTag:
return PriceTagIcon
case IconName.Restaurant: case IconName.Restaurant:
return RestaurantIcon return RestaurantIcon
case IconName.RoomService: case IconName.RoomService:

View File

@@ -39,6 +39,7 @@ export default function LanguageSwitcher({
const languageSwitcherRef = useRef<HTMLDivElement>(null) const languageSwitcherRef = useRef<HTMLDivElement>(null)
const isFooter = type === LanguageSwitcherTypesEnum.Footer const isFooter = type === LanguageSwitcherTypesEnum.Footer
const isHeader = !isFooter const isHeader = !isFooter
const globeIconSize = type === "desktopHeader" ? 16 : 20
const position = isFooter ? "footer" : "header" const position = isFooter ? "footer" : "header"
@@ -87,7 +88,7 @@ export default function LanguageSwitcher({
})} })}
onClick={handleClick} onClick={handleClick}
> >
<GlobeIcon width={20} height={20} /> <GlobeIcon width={globeIconSize} height={globeIconSize} />
<Caption className={styles.buttonText} type="regular" asChild> <Caption className={styles.buttonText} type="regular" asChild>
<span>{languages[currentLanguage]}</span> <span>{languages[currentLanguage]}</span>
</Caption> </Caption>

View File

@@ -1,11 +1,15 @@
#import "../Fragments/System.graphql" #import "../Fragments/System.graphql"
#import "../Fragments/PageLink/AccountPageLink.graphql"
#import "../Fragments/PageLink/CollectionPageLink.graphql"
#import "../Fragments/PageLink/ContentPageLink.graphql" #import "../Fragments/PageLink/ContentPageLink.graphql"
#import "../Fragments/PageLink/HotelPageLink.graphql" #import "../Fragments/PageLink/HotelPageLink.graphql"
#import "../Fragments/PageLink/LoyaltyPageLink.graphql" #import "../Fragments/PageLink/LoyaltyPageLink.graphql"
#import "../Fragments/Blocks/Card.graphql" #import "../Fragments/Blocks/Card.graphql"
#import "../Fragments/Blocks/Refs/Card.graphql" #import "../Fragments/Blocks/Refs/Card.graphql"
#import "../Fragments/AccountPage/Ref.graphql"
#import "../Fragments/CollectionPage/Ref.graphql"
#import "../Fragments/ContentPage/Ref.graphql" #import "../Fragments/ContentPage/Ref.graphql"
#import "../Fragments/HotelPage/Ref.graphql" #import "../Fragments/HotelPage/Ref.graphql"
#import "../Fragments/LoyaltyPage/Ref.graphql" #import "../Fragments/LoyaltyPage/Ref.graphql"
@@ -14,11 +18,15 @@ query GetHeader($locale: String!) {
all_header(limit: 1, locale: $locale) { all_header(limit: 1, locale: $locale) {
items { items {
top_link { top_link {
logged_in {
icon
title title
linkConnection { linkConnection {
edges { edges {
node { node {
__typename __typename
...AccountPageLink
...CollectionPageLink
...ContentPageLink ...ContentPageLink
...HotelPageLink ...HotelPageLink
...LoyaltyPageLink ...LoyaltyPageLink
@@ -26,6 +34,23 @@ query GetHeader($locale: String!) {
} }
} }
} }
logged_out {
icon
title
linkConnection {
edges {
node {
__typename
...AccountPageLink
...CollectionPageLink
...ContentPageLink
...HotelPageLink
...LoyaltyPageLink
}
}
}
}
}
menu_items { menu_items {
title title
linkConnection { linkConnection {
@@ -84,10 +109,13 @@ query GetHeaderRef($locale: String!) {
all_header(limit: 1, locale: $locale) { all_header(limit: 1, locale: $locale) {
items { items {
top_link { top_link {
logged_in {
linkConnection { linkConnection {
edges { edges {
node { node {
__typename __typename
...AccountPageRef
...CollectionPageRef
...ContentPageRef ...ContentPageRef
...HotelPageRef ...HotelPageRef
...LoyaltyPageRef ...LoyaltyPageRef
@@ -95,6 +123,21 @@ query GetHeaderRef($locale: String!) {
} }
} }
} }
logged_out {
linkConnection {
edges {
node {
__typename
...AccountPageRef
...CollectionPageRef
...ContentPageRef
...HotelPageRef
...LoyaltyPageRef
}
}
}
}
}
menu_items { menu_items {
linkConnection { linkConnection {
edges { edges {

View File

@@ -14,6 +14,7 @@ import { removeMultipleSlashes } from "@/utils/url"
import { systemSchema } from "../schemas/system" import { systemSchema } from "../schemas/system"
import { IconName } from "@/types/components/icon"
import { AlertTypeEnum } from "@/types/enums/alert" import { AlertTypeEnum } from "@/types/enums/alert"
import type { Image } from "@/types/image" import type { Image } from "@/types/image"
@@ -514,6 +515,11 @@ const menuItemsRefsSchema = z.intersection(
}) })
) )
const topLinkRefsSchema = z.object({
logged_in: linkRefsSchema.nullable(),
logged_out: linkRefsSchema.nullable(),
})
export const headerRefsSchema = z export const headerRefsSchema = z
.object({ .object({
all_header: z.object({ all_header: z.object({
@@ -522,7 +528,7 @@ export const headerRefsSchema = z
z.object({ z.object({
menu_items: z.array(menuItemsRefsSchema), menu_items: z.array(menuItemsRefsSchema),
system: systemSchema, system: systemSchema,
top_link: linkRefsSchema, top_link: topLinkRefsSchema,
}) })
) )
.max(1), .max(1),
@@ -636,6 +642,32 @@ export const menuItemSchema = z
} }
}) })
const topLinkItemSchema = z.intersection(
linkAndTitleSchema,
z.object({
icon: z
.enum(["loyalty", "info", "offer"])
.nullable()
.transform((icon) => {
switch (icon) {
case "loyalty":
return IconName.Gift
case "info":
return IconName.InfoCircle
case "offer":
return IconName.PriceTag
default:
return null
}
}),
})
)
export const topLinkSchema = z.object({
logged_in: topLinkItemSchema.nullable(),
logged_out: topLinkItemSchema.nullable(),
})
export const headerSchema = z export const headerSchema = z
.object({ .object({
all_header: z.object({ all_header: z.object({
@@ -643,7 +675,7 @@ export const headerSchema = z
.array( .array(
z.object({ z.object({
menu_items: z.array(menuItemSchema), menu_items: z.array(menuItemSchema),
top_link: linkAndTitleSchema, top_link: topLinkSchema,
}) })
) )
.max(1), .max(1),

View File

@@ -14,8 +14,13 @@ import type { ContactConfig } from "./output"
export function getConnections({ header }: HeaderRefs) { export function getConnections({ header }: HeaderRefs) {
const connections: System["system"][] = [header.system] const connections: System["system"][] = [header.system]
if (header.top_link?.link) { if (header.top_link) {
connections.push(header.top_link.link) if (header.top_link.logged_in?.link) {
connections.push(header.top_link.logged_in.link)
}
if (header.top_link.logged_out?.link) {
connections.push(header.top_link.logged_out.link)
}
} }
if (header.menu_items.length) { if (header.menu_items.length) {

View File

@@ -1,3 +1,9 @@
import type { LinkProps } from "@/components/TempDesignSystem/Link/link" import type { LinkProps } from "next/link"
export interface HeaderLinkProps extends React.PropsWithChildren<LinkProps> {} import type { IconName } from "../icon"
export interface HeaderLinkProps extends React.PropsWithChildren {
href: LinkProps["href"]
iconName: IconName | null
iconSize?: number
}

View File

@@ -4,4 +4,5 @@ import type { Header } from "@/types/trpc/routers/contentstack/header"
export interface MobileMenuProps { export interface MobileMenuProps {
languageUrls: LanguageSwitcherData languageUrls: LanguageSwitcherData
topLink: Header["header"]["topLink"] topLink: Header["header"]["topLink"]
isLoggedIn: boolean
} }

View File

@@ -0,0 +1,7 @@
import type { Header } from "@/types/trpc/routers/contentstack/header"
export interface TopLinkProps {
isLoggedIn: boolean
topLink: Header["header"]["topLink"]
iconSize?: number
}

View File

@@ -82,6 +82,7 @@ export enum IconName {
Phone = "Phone", Phone = "Phone",
Plus = "Plus", Plus = "Plus",
PlusCircle = "PlusCircle", PlusCircle = "PlusCircle",
PriceTag = "PriceTag",
Restaurant = "Restaurant", Restaurant = "Restaurant",
RoomService = "RoomService", RoomService = "RoomService",
Sauna = "Sauna", Sauna = "Sauna",