diff --git a/components/ContentType/ContentPage/contentPage.module.css b/components/ContentType/ContentPage/contentPage.module.css index 013b1863a..9ba97d7f9 100644 --- a/components/ContentType/ContentPage/contentPage.module.css +++ b/components/ContentType/ContentPage/contentPage.module.css @@ -11,6 +11,18 @@ padding: var(--Spacing-x4) var(--Spacing-x2); } +.headerContent { + display: grid; + gap: var(--Spacing-x3); + max-width: var(--max-width-content); + margin: 0 auto; +} +.headerIntro { + display: grid; + max-width: var(--max-width-text-block); + gap: var(--Spacing-x3); +} + .content { padding: var(--Spacing-x4) var(--Spacing-x2); display: grid; @@ -21,3 +33,9 @@ width: 100%; max-width: var(--max-width-content); } + +@media (min-width: 768px) { + .headerIntro { + gap: var(--Spacing-x3); + } +} diff --git a/components/ContentType/ContentPage/index.tsx b/components/ContentType/ContentPage/index.tsx index 8e53cafd7..3e7c59abb 100644 --- a/components/ContentType/ContentPage/index.tsx +++ b/components/ContentType/ContentPage/index.tsx @@ -2,8 +2,8 @@ import { serverClient } from "@/lib/trpc/server" import Blocks from "@/components/Blocks" import Hero from "@/components/Hero" -import Intro from "@/components/Intro" import Sidebar from "@/components/Sidebar" +import LinkChips from "@/components/TempDesignSystem/LinkChips" import Preamble from "@/components/TempDesignSystem/Text/Preamble" import Title from "@/components/TempDesignSystem/Text/Title" import TrackingSDK from "@/components/TrackingSDK" @@ -18,31 +18,34 @@ export default async function ContentPage() { } const { tracking, contentPage } = contentPageRes - const heroImage = contentPage.hero_image + const { blocks, hero_image, header, sidebar } = contentPage return ( <> - {contentPage.sidebar?.length ? ( - - ) : null} + {sidebar?.length ? : null} - - {contentPage.header.heading} - {contentPage.header.preamble} - + + + {header.heading} + {header.preamble} + + {header.navigation_links ? ( + + ) : null} + - {heroImage ? ( + {hero_image ? ( ) : null} - {contentPage.blocks ? : null} + {blocks ? : null} diff --git a/components/Icons/ChevronRightSmall.tsx b/components/Icons/ChevronRightSmall.tsx new file mode 100644 index 000000000..25fb29002 --- /dev/null +++ b/components/Icons/ChevronRightSmall.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function ChevronRightSmallIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/get-icon-by-icon-name.ts b/components/Icons/get-icon-by-icon-name.ts index 92d674299..20bf8d9a8 100644 --- a/components/Icons/get-icon-by-icon-name.ts +++ b/components/Icons/get-icon-by-icon-name.ts @@ -17,6 +17,7 @@ import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, + ChevronRightSmallIcon, CloseIcon, CloseLarge, CoffeeIcon, @@ -89,6 +90,8 @@ export function getIconByIconName(icon?: IconName): FC | null { return ChevronLeftIcon case IconName.ChevronRight: return ChevronRightIcon + case IconName.ChevronRightSmall: + return ChevronRightSmallIcon case IconName.Close: return CloseIcon case IconName.CloseLarge: diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx index 433ef0c89..9d7c0bd2c 100644 --- a/components/Icons/index.tsx +++ b/components/Icons/index.tsx @@ -11,6 +11,7 @@ export { default as CheckCircleIcon } from "./CheckCircle" export { default as ChevronDownIcon } from "./ChevronDown" export { default as ChevronLeftIcon } from "./ChevronLeft" export { default as ChevronRightIcon } from "./ChevronRight" +export { default as ChevronRightSmallIcon } from "./ChevronRightSmall" export { default as CloseIcon } from "./Close" export { default as CloseLarge } from "./CloseLarge" export { default as CoffeeIcon } from "./Coffee" diff --git a/components/Intro/index.tsx b/components/Intro/index.tsx deleted file mode 100644 index 6c7a8a30d..000000000 --- a/components/Intro/index.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { PropsWithChildren } from "react" - -import styles from "./intro.module.css" - -export default async function Intro({ children }: PropsWithChildren) { - return ( - - {children} - - ) -} diff --git a/components/Intro/intro.module.css b/components/Intro/intro.module.css deleted file mode 100644 index b2290fb9f..000000000 --- a/components/Intro/intro.module.css +++ /dev/null @@ -1,16 +0,0 @@ -.intro { - max-width: var(--max-width-content); - margin: 0 auto; -} - -.content { - display: grid; - max-width: var(--max-width-text-block); - gap: var(--Spacing-x2); -} - -@media (min-width: 768px) { - .content { - gap: var(--Spacing-x3); - } -} diff --git a/components/TempDesignSystem/LinkChips/Chip/chip.module.css b/components/TempDesignSystem/LinkChips/Chip/chip.module.css new file mode 100644 index 000000000..311892c58 --- /dev/null +++ b/components/TempDesignSystem/LinkChips/Chip/chip.module.css @@ -0,0 +1,14 @@ +.linkChip { + display: flex; + align-items: center; + gap: var(--Spacing-x-half); + padding: var(--Spacing-x1) var(--Spacing-x-one-and-half); + border-radius: var(--Corner-radius-Small); + background-color: var(--Base-Button-Inverted-Fill-Normal); + transition: background-color 0.3s; + text-decoration: none; +} + +.linkChip:hover { + background-color: var(--Base-Button-Inverted-Fill-Hover-alt); +} diff --git a/components/TempDesignSystem/LinkChips/Chip/chip.ts b/components/TempDesignSystem/LinkChips/Chip/chip.ts new file mode 100644 index 000000000..0a4116686 --- /dev/null +++ b/components/TempDesignSystem/LinkChips/Chip/chip.ts @@ -0,0 +1,4 @@ +export interface LinkChipProps { + url: string + title: string +} diff --git a/components/TempDesignSystem/LinkChips/Chip/index.tsx b/components/TempDesignSystem/LinkChips/Chip/index.tsx new file mode 100644 index 000000000..791037906 --- /dev/null +++ b/components/TempDesignSystem/LinkChips/Chip/index.tsx @@ -0,0 +1,19 @@ +import Link from "next/link" + +import { ChevronRightSmallIcon } from "@/components/Icons" +import Caption from "@/components/TempDesignSystem/Text/Caption" + +import { LinkChipProps } from "./chip" + +import styles from "./chip.module.css" + +export default function LinkChip({ url, title }: LinkChipProps) { + return ( + + + {title} + + + + ) +} diff --git a/components/TempDesignSystem/LinkChips/index.tsx b/components/TempDesignSystem/LinkChips/index.tsx new file mode 100644 index 000000000..2d6e12881 --- /dev/null +++ b/components/TempDesignSystem/LinkChips/index.tsx @@ -0,0 +1,20 @@ +import LinkChip from "./Chip" +import { LinkChipsProps } from "./linkChips" + +import styles from "./linkChips.module.css" + +export default function LinkChips({ chips }: LinkChipsProps) { + if (!chips.length) { + return null + } + + return ( + + {chips.map(({ url, title }) => ( + + + + ))} + + ) +} diff --git a/components/TempDesignSystem/LinkChips/linkChips.module.css b/components/TempDesignSystem/LinkChips/linkChips.module.css new file mode 100644 index 000000000..9d7361824 --- /dev/null +++ b/components/TempDesignSystem/LinkChips/linkChips.module.css @@ -0,0 +1,8 @@ +.linkChips { + list-style: none; + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + align-items: center; + gap: var(--Spacing-x1); +} diff --git a/components/TempDesignSystem/LinkChips/linkChips.ts b/components/TempDesignSystem/LinkChips/linkChips.ts new file mode 100644 index 000000000..361cd07ed --- /dev/null +++ b/components/TempDesignSystem/LinkChips/linkChips.ts @@ -0,0 +1,5 @@ +import { LinkChipProps } from "./Chip/chip" + +export interface LinkChipsProps { + chips: LinkChipProps[] +} diff --git a/lib/graphql/Fragments/ContentPage/NavigationLinks.graphql b/lib/graphql/Fragments/ContentPage/NavigationLinks.graphql new file mode 100644 index 000000000..b82d68627 --- /dev/null +++ b/lib/graphql/Fragments/ContentPage/NavigationLinks.graphql @@ -0,0 +1,19 @@ +#import "../PageLink/ContentPageLink.graphql" +#import "../PageLink/HotelPageLink.graphql" +#import "../PageLink/LoyaltyPageLink.graphql" + +fragment NavigationLinks on ContentPageHeader { + navigation_links { + title + linkConnection { + edges { + node { + __typename + ...HotelPageLink + ...ContentPageLink + ...LoyaltyPageLink + } + } + } + } +} diff --git a/lib/graphql/Query/ContentPage/ContentPage.graphql b/lib/graphql/Query/ContentPage/ContentPage.graphql index 7b330003b..c1e4decc5 100644 --- a/lib/graphql/Query/ContentPage/ContentPage.graphql +++ b/lib/graphql/Query/ContentPage/ContentPage.graphql @@ -7,6 +7,8 @@ #import "../../Fragments/Blocks/TextCols.graphql" #import "../../Fragments/Blocks/UspGrid.graphql" +#import "../../Fragments/ContentPage/NavigationLinks.graphql" + #import "../../Fragments/Sidebar/Content.graphql" #import "../../Fragments/Sidebar/DynamicContent.graphql" #import "../../Fragments/Sidebar/JoinLoyaltyContact.graphql" @@ -18,6 +20,7 @@ query GetContentPage($locale: String!, $uid: String!) { header { heading preamble + ...NavigationLinks } blocks { __typename @@ -44,6 +47,20 @@ query GetContentPage($locale: String!, $uid: String!) { query GetContentPageRefs($locale: String!, $uid: String!) { content_page(locale: $locale, uid: $uid) { + header { + navigation_links { + linkConnection { + edges { + node { + __typename + ...ContentPageRef + ...HotelPageRef + ...LoyaltyPageRef + } + } + } + } + } blocks { __typename ...CardsGrid_ContentPageRefs diff --git a/server/routers/contentstack/contentPage/output.ts b/server/routers/contentstack/contentPage/output.ts index 8048faee6..398abb372 100644 --- a/server/routers/contentstack/contentPage/output.ts +++ b/server/routers/contentstack/contentPage/output.ts @@ -11,8 +11,8 @@ import { contentSchema as blockContentSchema, } from "../schemas/blocks/content" import { - dynamicContentRefsSchema, dynamicContentSchema as blockDynamicContentSchema, + dynamicContentRefsSchema, } from "../schemas/blocks/dynamicContent" import { shortcutsRefsSchema, @@ -32,7 +32,18 @@ import { } from "../schemas/sidebar/joinLoyaltyContact" import { systemSchema } from "../schemas/system" +import * as pageLinks from "@/server/routers/contentstack/schemas/pageLinks" import { ContentPageEnum } from "@/types/enums/contentPage" +import { + linkAndTitleSchema, + linkConnectionRefs, +} from "../schemas/linkConnection" + +const linkUnionSchema = z.discriminatedUnion("__typename", [ + pageLinks.contentPageSchema, + pageLinks.hotelPageSchema, + pageLinks.loyaltyPageSchema, +]) // Block schemas export const contentPageCards = z @@ -106,6 +117,19 @@ export const sidebarSchema = z.discriminatedUnion("__typename", [ contentPageJoinLoyaltyContact, ]) +const navigationLinksSchema = z + .array(linkAndTitleSchema) + .nullable() + .transform((data) => { + if (!data) { + return null + } + + return data + .filter((item) => !!item.link) + .map((item) => ({ url: item.link.url, title: item.title })) + }) + // Content Page Schema and types export const contentPageSchema = z.object({ content_page: z.object({ @@ -116,6 +140,7 @@ export const contentPageSchema = z.object({ header: z.object({ heading: z.string(), preamble: z.string(), + navigation_links: navigationLinksSchema, }), system: systemSchema.merge( z.object({ @@ -191,8 +216,13 @@ const contentPageSidebarRefsItem = z.discriminatedUnion("__typename", [ contentPageSidebarJoinLoyaltyContactRef, ]) +const contentPageHeaderRefs = z.object({ + navigation_links: z.array(linkConnectionRefs), +}) + export const contentPageRefsSchema = z.object({ content_page: z.object({ + header: contentPageHeaderRefs, blocks: discriminatedUnionArray( contentPageBlockRefsItem.options ).nullable(), diff --git a/server/routers/contentstack/schemas/linkConnection.ts b/server/routers/contentstack/schemas/linkConnection.ts new file mode 100644 index 000000000..aff9b980c --- /dev/null +++ b/server/routers/contentstack/schemas/linkConnection.ts @@ -0,0 +1,74 @@ +import { z } from "zod" + + + +import { discriminatedUnion } from "@/lib/discriminatedUnion" +import * as pageLinks from "@/server/routers/contentstack/schemas/pageLinks" + +const linkUnionSchema = z.discriminatedUnion("__typename", [ + pageLinks.contentPageSchema, + pageLinks.hotelPageSchema, + pageLinks.loyaltyPageSchema, +]) + +const titleSchema = z.object({ + title: z.string().optional().default(""), +}) + +export const linkConnectionSchema = z + .object({ + linkConnection: z.object({ + edges: z.array( + z.object({ + node: discriminatedUnion(linkUnionSchema.options), + }) + ), + }), + }) + .transform((data) => { + if (data.linkConnection.edges.length) { + const link = pageLinks.transform(data.linkConnection.edges[0].node) + if (link) { + return { + link, + } + } + } + + return { + link: null, + } + }) + +export const linkAndTitleSchema = z.intersection( + linkConnectionSchema, + titleSchema +) + +const linkRefsUnionSchema = z.discriminatedUnion("__typename", [ + pageLinks.contentPageRefSchema, + pageLinks.hotelPageRefSchema, + pageLinks.loyaltyPageRefSchema, +]) + +export const linkConnectionRefs = z + .object({ + linkConnection: z.object({ + edges: z.array( + z.object({ + node: linkRefsUnionSchema, + }) + ), + }), + }) + .transform((data) => { + if (data.linkConnection.edges.length) { + const link = pageLinks.transformRef(data.linkConnection.edges[0].node) + if (link) { + return { + link, + } + } + } + return { link: null } + }) diff --git a/types/components/icon.ts b/types/components/icon.ts index f9f61a525..6b11649a2 100644 --- a/types/components/icon.ts +++ b/types/components/icon.ts @@ -21,6 +21,7 @@ export enum IconName { ChevronDown = "ChevronDown", ChevronLeft = "ChevronLeft", ChevronRight = "ChevronRight", + ChevronRightSmall = "ChevronRightSmall", Close = "Close", CloseLarge = "CloseLarge", Coffee = "Coffee",