feat(SW-186): implement cms data into new header

This commit is contained in:
Erik Tiekstra
2024-09-03 15:41:49 +02:00
parent bf7d22c728
commit 52fdc1daac
25 changed files with 123 additions and 154 deletions

View File

@@ -4,9 +4,17 @@ import styles from "./headerLink.module.css"
import type { HeaderLinkProps } from "@/types/components/header/headerLink" import type { HeaderLinkProps } from "@/types/components/header/headerLink"
export default function HeaderLink({ children, ...props }: HeaderLinkProps) { export default function HeaderLink({
children,
className,
...props
}: HeaderLinkProps) {
return ( return (
<Link color="burgundy" className={styles.headerLink} {...props}> <Link
color="burgundy"
className={`${styles.headerLink} ${className}`}
{...props}
>
{children} {children}
</Link> </Link>
) )

View File

@@ -17,7 +17,7 @@ import styles from "./mobileMenu.module.css"
import type { MobileMenuProps } from "@/types/components/header/mobileMenu" import type { MobileMenuProps } from "@/types/components/header/mobileMenu"
export default function MobileMenu({ export default function MobileMenu({
mainNavigation, menuItems,
languageUrls, languageUrls,
}: MobileMenuProps) { }: MobileMenuProps) {
const intl = useIntl() const intl = useIntl()
@@ -65,7 +65,7 @@ export default function MobileMenu({
className={styles.dialog} className={styles.dialog}
aria-label={intl.formatMessage({ id: "Menu" })} aria-label={intl.formatMessage({ id: "Menu" })}
> >
<NavigationMenu isMobile={true} items={mainNavigation} /> <NavigationMenu isMobile={true} items={menuItems} />
<footer className={styles.footer}> <footer className={styles.footer}>
<HeaderLink href="#"> <HeaderLink href="#">
<SearchIcon width={20} height={20} color="burgundy" /> <SearchIcon width={20} height={20} color="burgundy" />

View File

@@ -12,14 +12,18 @@ import styles from "./navigationMenuItem.module.css"
import type { NavigationMenuItemProps } from "@/types/components/header/navigationMenuItem" import type { NavigationMenuItemProps } from "@/types/components/header/navigationMenuItem"
export default function MenuItem({ item, isMobile }: NavigationMenuItemProps) { export default function MenuItem({ item, isMobile }: NavigationMenuItemProps) {
const { children, title, href, seeAllLinkText, infoCard } = item const { submenu, title, link, seeAllLink, card } = item
const [isExpanded, setIsExpanded] = useState(false) const [isExpanded, setIsExpanded] = useState(false)
function handleButtonClick() { function handleButtonClick() {
setIsExpanded((prev) => !prev) setIsExpanded((prev) => !prev)
} }
return children?.length ? ( if (!submenu.length && !link) {
return null
}
return submenu.length ? (
<MainMenuButton <MainMenuButton
onClick={handleButtonClick} onClick={handleButtonClick}
className={`${styles.navigationMenuItem} ${isMobile ? styles.mobile : styles.desktop}`} className={`${styles.navigationMenuItem} ${isMobile ? styles.mobile : styles.desktop}`}
@@ -36,7 +40,7 @@ export default function MenuItem({ item, isMobile }: NavigationMenuItemProps) {
</MainMenuButton> </MainMenuButton>
) : ( ) : (
<Link <Link
href={href} href={link!.href}
color="burgundy" color="burgundy"
className={`${styles.navigationMenuItem} ${isMobile ? styles.mobile : styles.desktop}`} className={`${styles.navigationMenuItem} ${isMobile ? styles.mobile : styles.desktop}`}
> >

View File

@@ -8,12 +8,20 @@ export default function NavigationMenu({
items, items,
isMobile, isMobile,
}: NavigationMenuProps) { }: NavigationMenuProps) {
const filteredItems = items.filter(
({ link, submenu }) => submenu.length || link
)
if (!filteredItems.length) {
return null
}
return ( return (
<ul <ul
className={`${styles.navigationMenu} ${isMobile ? styles.mobile : styles.desktop}`} className={`${styles.navigationMenu} ${isMobile ? styles.mobile : styles.desktop}`}
> >
{items.map((item) => ( {filteredItems.map((item) => (
<li key={item.id} className={styles.item}> <li key={item.title} className={styles.item}>
<NavigationMenuItem isMobile={isMobile} item={item} /> <NavigationMenuItem isMobile={isMobile} item={item} />
</li> </li>
))} ))}

View File

@@ -8,7 +8,6 @@ import Link from "@/components/TempDesignSystem/Link"
import { getIntl } from "@/i18n" import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext" import { getLang } from "@/i18n/serverContext"
import { navigationMenuItems } from "../tempHeaderData"
import Avatar from "./Avatar" import Avatar from "./Avatar"
import MobileMenu from "./MobileMenu" import MobileMenu from "./MobileMenu"
import MyPagesMenu from "./MyPagesMenu" import MyPagesMenu from "./MyPagesMenu"
@@ -19,7 +18,10 @@ import styles from "./mainMenu.module.css"
import type { MainMenuProps } from "@/types/components/header/mainMenu" import type { MainMenuProps } from "@/types/components/header/mainMenu"
export default async function MainMenu({ languageUrls }: MainMenuProps) { export default async function MainMenu({
languageUrls,
menuItems,
}: MainMenuProps) {
const intl = await getIntl() const intl = await getIntl()
const lang = getLang() const lang = getLang()
const myPagesNavigation = const myPagesNavigation =
@@ -44,7 +46,9 @@ export default async function MainMenu({ languageUrls }: MainMenuProps) {
/> />
</NextLink> </NextLink>
<div className={styles.menus}> <div className={styles.menus}>
<NavigationMenu items={navigationMenuItems} isMobile={false} /> {menuItems ? (
<NavigationMenu items={menuItems} isMobile={false} />
) : null}
{user ? ( {user ? (
<> <>
<MyPagesMenu <MyPagesMenu
@@ -70,12 +74,34 @@ export default async function MainMenu({ languageUrls }: MainMenuProps) {
</span> </span>
</Link> </Link>
)} )}
<MobileMenu {menuItems ? (
languageUrls={languageUrls} <MobileMenu languageUrls={languageUrls} menuItems={menuItems} />
mainNavigation={navigationMenuItems} ) : null}
/>
</div> </div>
</nav> </nav>
</div> </div>
) )
} }
const error = {
query: { lang: "sv" },
error: {
issues: [
{
code: "invalid_type",
expected: "string",
received: "null",
path: ["all_header", "items", 0, "top_link", "title"],
message: "Expected string, received null",
},
{
code: "invalid_type",
expected: "array",
received: "null",
path: ["all_header", "items", 0, "menu_items"],
message: "Expected array, received null",
},
],
name: "ZodError",
},
}

View File

@@ -8,17 +8,19 @@ import styles from "./topMenu.module.css"
import type { TopMenuProps } from "@/types/components/header/topMenu" import type { TopMenuProps } from "@/types/components/header/topMenu"
export default async function TopMenu({ languageUrls }: TopMenuProps) { export default async function TopMenu({ languageUrls, topLink }: TopMenuProps) {
const intl = await getIntl() const intl = await getIntl()
return ( return (
<div className={styles.topMenu}> <div className={styles.topMenu}>
<div className={styles.content}> <div className={styles.content}>
<HeaderLink href="#"> {topLink ? (
<GiftIcon width={20} height={20} color="burgundy" /> <HeaderLink className={styles.topLink} href={topLink.href}>
{intl.formatMessage({ id: "Join Scandic Friends" })} <GiftIcon width={20} height={20} color="burgundy" />
</HeaderLink> {topLink.title}
<div className={styles.right}> </HeaderLink>
) : null}
<div className={styles.options}>
<LanguageSwitcher type="desktopHeader" urls={languageUrls} /> <LanguageSwitcher type="desktopHeader" urls={languageUrls} />
<HeaderLink href="#"> <HeaderLink href="#">
<SearchIcon width={20} height={20} color="burgundy" /> <SearchIcon width={20} height={20} color="burgundy" />

View File

@@ -8,12 +8,12 @@
.content { .content {
max-width: var(--max-width-navigation); max-width: var(--max-width-navigation);
margin: 0 auto; margin: 0 auto;
display: flex; display: grid;
justify-content: space-between; justify-content: space-between;
gap: var(--Spacing-x3); gap: var(--Spacing-x3);
} }
.right { .options {
display: flex; display: flex;
gap: var(--Spacing-x2); gap: var(--Spacing-x2);
align-items: center; align-items: center;
@@ -23,4 +23,15 @@
.topMenu { .topMenu {
display: block; display: block;
} }
.content {
grid-template-areas: "topLink options";
}
.topLink {
grid-area: topLink;
}
.options {
grid-area: options;
}
} }

View File

@@ -2,6 +2,5 @@
position: relative; position: relative;
font-family: var(--typography-Body-Regular-fontFamily); font-family: var(--typography-Body-Regular-fontFamily);
color: var(--Base-Text-High-contrast); color: var(--Base-Text-High-contrast);
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.08);
z-index: var(--header-z-index); z-index: var(--header-z-index);
} }

View File

@@ -7,15 +7,19 @@ import styles from "./header.module.css"
export default async function Header() { export default async function Header() {
const languages = await serverClient().contentstack.languageSwitcher.get() const languages = await serverClient().contentstack.languageSwitcher.get()
const headerData = await serverClient().contentstack.base.header()
if (!languages) { if (!languages || !headerData) {
return null return null
} }
return ( return (
<header className={styles.header}> <header className={styles.header}>
<TopMenu languageUrls={languages.urls} /> <TopMenu languageUrls={languages.urls} topLink={headerData.topLink} />
<MainMenu languageUrls={languages.urls} /> <MainMenu
languageUrls={languages.urls}
menuItems={headerData.menuItems}
/>
</header> </header>
) )
} }

View File

@@ -1,75 +0,0 @@
import type { MainNavigationItem } from "@/types/components/header/mainNavigationItem"
export const navigationMenuItems: MainNavigationItem[] = [
{
id: "hotels",
title: "Hotels",
href: "/hotels",
children: [],
},
{
id: "business",
title: "Business",
href: "/business",
children: [
{
groupTitle: "Top conference venues",
children: [
{
id: "stockholm",
title: "Stockholm",
href: "/stockholm",
},
{
id: "bergen",
title: "Bergen",
href: "/bergen",
},
{
id: "copenhagen",
title: "Copenhagen",
href: "/copenhagen",
},
],
},
{
groupTitle: "Scandic for business",
children: [
{
id: "book-a-venue",
title: "Book a venue",
href: "/book-a-venue",
},
{
id: "conference-packages",
title: "Conference packages",
href: "/conference-packages",
},
{
id: "co-working",
title: "Co-working",
href: "/co-working",
},
],
},
],
seeAllLinkText: "See all conference & meeting venues",
infoCard: {
scriptedTitle: "Stockholm",
title: "Meeting venues in Stockholm",
description:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed et felis metus. Sed et felis metus.",
ctaLink: "/stockholm",
},
},
{
id: "offers",
title: "Offers",
href: "/offers",
},
{
id: "restaurants",
title: "Restaurants",
href: "/restaurants",
},
]

View File

@@ -74,7 +74,6 @@
"Hotel facilities": "Hotel faciliteter", "Hotel facilities": "Hotel faciliteter",
"Hotel surroundings": "Hotel omgivelser", "Hotel surroundings": "Hotel omgivelser",
"How it works": "Hvordan det virker", "How it works": "Hvordan det virker",
"Join Scandic Friends": "Tilmeld dig Scandic Friends",
"km to city center": "km til byens centrum", "km to city center": "km til byens centrum",
"Language": "Sprog", "Language": "Sprog",
"Level": "Niveau", "Level": "Niveau",

View File

@@ -73,7 +73,6 @@
"Hotel facilities": "Hotel-Infos", "Hotel facilities": "Hotel-Infos",
"Hotel surroundings": "Umgebung des Hotels", "Hotel surroundings": "Umgebung des Hotels",
"How it works": "Wie es funktioniert", "How it works": "Wie es funktioniert",
"Join Scandic Friends": "Treten Sie Scandic Friends bei",
"km to city center": "km bis zum Stadtzentrum", "km to city center": "km bis zum Stadtzentrum",
"Language": "Sprache", "Language": "Sprache",
"Level": "Level", "Level": "Level",

View File

@@ -79,7 +79,6 @@
"hotelPages.rooms.roomCard.persons": "persons", "hotelPages.rooms.roomCard.persons": "persons",
"hotelPages.rooms.roomCard.seeRoomDetails": "See room details", "hotelPages.rooms.roomCard.seeRoomDetails": "See room details",
"How it works": "How it works", "How it works": "How it works",
"Join Scandic Friends": "Join Scandic Friends",
"km to city center": "km to city center", "km to city center": "km to city center",
"Language": "Language", "Language": "Language",
"Level": "Level", "Level": "Level",

View File

@@ -74,7 +74,6 @@
"Hotel facilities": "Hotellin palvelut", "Hotel facilities": "Hotellin palvelut",
"Hotel surroundings": "Hotellin ympäristö", "Hotel surroundings": "Hotellin ympäristö",
"How it works": "Kuinka se toimii", "How it works": "Kuinka se toimii",
"Join Scandic Friends": "Liity jäseneksi",
"km to city center": "km keskustaan", "km to city center": "km keskustaan",
"Language": "Kieli", "Language": "Kieli",
"Level": "Level", "Level": "Level",

View File

@@ -74,7 +74,6 @@
"Hotel facilities": "Hotelfaciliteter", "Hotel facilities": "Hotelfaciliteter",
"Hotel surroundings": "Hotellomgivelser", "Hotel surroundings": "Hotellomgivelser",
"How it works": "Hvordan det fungerer", "How it works": "Hvordan det fungerer",
"Join Scandic Friends": "Bli med i Scandic Friends",
"km to city center": "km til sentrum", "km to city center": "km til sentrum",
"Language": "Språk", "Language": "Språk",
"Level": "Nivå", "Level": "Nivå",

View File

@@ -76,7 +76,6 @@
"hotelPages.rooms.roomCard.person": "person", "hotelPages.rooms.roomCard.person": "person",
"hotelPages.rooms.roomCard.persons": "personer", "hotelPages.rooms.roomCard.persons": "personer",
"How it works": "Hur det fungerar", "How it works": "Hur det fungerar",
"Join Scandic Friends": "Gå med i Scandic Friends",
"km to city center": "km till stadens centrum", "km to city center": "km till stadens centrum",
"Language": "Språk", "Language": "Språk",
"Level": "Nivå", "Level": "Nivå",

View File

@@ -301,11 +301,11 @@ const linkConnectionNodeSchema = z
const linkWithTitleSchema = z const linkWithTitleSchema = z
.object({ .object({
title: z.string(), title: z.string().nullable(),
linkConnection: linkConnectionNodeSchema, linkConnection: linkConnectionNodeSchema,
}) })
.transform((rawData) => { .transform((rawData) => {
return rawData.linkConnection return rawData.linkConnection && rawData.title
? { ? {
title: rawData.title, title: rawData.title,
href: rawData.linkConnection.href, href: rawData.linkConnection.href,
@@ -378,7 +378,7 @@ const cardConnectionSchema = z
} }
}) })
const menuItemSchema = z export const menuItemSchema = z
.object({ .object({
title: z.string(), title: z.string(),
linkConnection: linkConnectionNodeSchema, linkConnection: linkConnectionNodeSchema,
@@ -391,22 +391,25 @@ const menuItemSchema = z
see_all_link: linkWithTitleSchema, see_all_link: linkWithTitleSchema,
cardConnection: cardConnectionSchema, cardConnection: cardConnectionSchema,
}) })
.transform(({ submenu, linkConnection, cardConnection, see_all_link }) => { .transform(
return { ({ submenu, linkConnection, cardConnection, see_all_link, title }) => {
link: submenu.length ? null : linkConnection, return {
seeAllLink: submenu.length ? see_all_link : null, title,
submenu, link: submenu.length ? null : linkConnection,
card: cardConnection, seeAllLink: submenu.length ? see_all_link : null,
submenu,
card: cardConnection,
}
} }
}) )
export const getHeaderSchema = z export const getHeaderSchema = z
.object({ .object({
all_header: z.object({ all_header: z.object({
items: z.array( items: z.array(
z.object({ z.object({
top_link: linkWithTitleSchema, top_link: linkWithTitleSchema.nullable(),
menu_items: z.array(menuItemSchema), menu_items: z.array(menuItemSchema).nullable(),
}) })
), ),
}), }),

View File

@@ -183,12 +183,12 @@ export const baseQueryRouter = router({
const response = await request<HeaderResponse>( const response = await request<HeaderResponse>(
GetHeader, GetHeader,
{ locale: lang }, { locale: lang }
{ // {
tags: [ // tags: [
generateTag(lang, responseRef.data.all_header.items[0].system.uid), // generateTag(lang, responseRef.data.all_header.items[0].system.uid),
], // ],
} // }
) )
if (!response.data) { if (!response.data) {

View File

@@ -1,5 +1,7 @@
import { MenuItem } from "@/types/header"
import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher" import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher"
export interface MainMenuProps { export interface MainMenuProps {
languageUrls: LanguageSwitcherData languageUrls: LanguageSwitcherData
menuItems: MenuItem[] | null
} }

View File

@@ -1,20 +0,0 @@
export interface MainNavigationItem {
id: string
title: string
href: string
children?: {
groupTitle: string
children: {
id: string
title: string
href: string
}[]
}[]
seeAllLinkText?: string
infoCard?: {
scriptedTitle: string
title: string
description: string
ctaLink: string
}
}

View File

@@ -1,8 +1,7 @@
import { MainNavigationItem } from "./mainNavigationItem" import { MenuItem } from "@/types/header"
import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher" import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher"
export interface MobileMenuProps { export interface MobileMenuProps {
languageUrls: LanguageSwitcherData languageUrls: LanguageSwitcherData
mainNavigation: MainNavigationItem[] menuItems: MenuItem[]
} }

View File

@@ -1,6 +1,6 @@
import type { MainNavigationItem } from "@/types/components/header/mainNavigationItem" import { MenuItem } from "@/types/header"
export interface NavigationMenuProps { export interface NavigationMenuProps {
items: MainNavigationItem[] items: MenuItem[]
isMobile: boolean isMobile: boolean
} }

View File

@@ -1,6 +1,6 @@
import type { MainNavigationItem } from "@/types/components/header/mainNavigationItem" import { MenuItem } from "@/types/header"
export interface NavigationMenuItemProps { export interface NavigationMenuItemProps {
item: MainNavigationItem item: MenuItem
isMobile: boolean isMobile: boolean
} }

View File

@@ -1,5 +1,7 @@
import { Header } from "@/types/header"
import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher" import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher"
export interface TopMenuProps { export interface TopMenuProps {
languageUrls: LanguageSwitcherData languageUrls: LanguageSwitcherData
topLink: Header["topLink"]
} }

View File

@@ -3,8 +3,10 @@ import { z } from "zod"
import { import {
getHeaderRefSchema, getHeaderRefSchema,
getHeaderSchema, getHeaderSchema,
menuItemSchema,
} from "@/server/routers/contentstack/base/output" } from "@/server/routers/contentstack/base/output"
export type HeaderRefResponse = z.input<typeof getHeaderRefSchema> export type HeaderRefResponse = z.input<typeof getHeaderRefSchema>
export type HeaderResponse = z.input<typeof getHeaderSchema> export type HeaderResponse = z.input<typeof getHeaderSchema>
export type Header = z.output<typeof getHeaderSchema> export type Header = z.output<typeof getHeaderSchema>
export type MenuItem = z.output<typeof menuItemSchema>