From 8220a39a8f7a503ad73e7b35e370bb4f13ee3f72 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Wed, 14 Aug 2024 09:29:00 +0200 Subject: [PATCH] feat(SW-190): added hero to static content pages --- .../(public)/[contentType]/[uid]/page.tsx | 4 +- components/ContentType/ContentPage.tsx | 3 - .../ContentType/ContentPage/ContentPage.tsx | 47 ++++++++++++++ .../ContentType/ContentPage/Intro/index.tsx | 22 +++++++ .../ContentPage/Intro/intro.module.css | 8 +++ .../ContentPage/contentPage.module.css | 17 +++++ .../ContentType/LoyaltyPage/LoyaltyPage.tsx | 3 +- components/MaxWidth/index.tsx | 14 ++-- components/MaxWidth/maxWidth.module.css | 18 +++++- components/MaxWidth/variants.ts | 21 ++++++ .../Fragments/ContentPage/Breadcrumbs.graphql | 25 ++++++++ lib/graphql/Query/ContentPage.graphql | 19 ++++++ lib/graphql/Query/LoyaltyPage.graphql | 5 +- .../routers/contentstack/contentPage/index.ts | 5 ++ .../contentstack/contentPage/output.ts | 30 +++++++++ .../routers/contentstack/contentPage/query.ts | 64 +++++++++++++++++++ .../routers/contentstack/contentPage/utils.ts | 9 +++ server/routers/contentstack/index.ts | 2 + .../contentstack/loyaltyPage/output.ts | 13 ++-- .../routers/contentstack/loyaltyPage/query.ts | 44 +------------ .../routers/contentstack/loyaltyPage/utils.ts | 33 ++++++++++ types/components/max-width.ts | 8 ++- types/components/tracking.ts | 1 + 23 files changed, 351 insertions(+), 64 deletions(-) delete mode 100644 components/ContentType/ContentPage.tsx create mode 100644 components/ContentType/ContentPage/ContentPage.tsx create mode 100644 components/ContentType/ContentPage/Intro/index.tsx create mode 100644 components/ContentType/ContentPage/Intro/intro.module.css create mode 100644 components/ContentType/ContentPage/contentPage.module.css create mode 100644 components/MaxWidth/variants.ts create mode 100644 lib/graphql/Fragments/ContentPage/Breadcrumbs.graphql create mode 100644 lib/graphql/Query/ContentPage.graphql create mode 100644 server/routers/contentstack/contentPage/index.ts create mode 100644 server/routers/contentstack/contentPage/output.ts create mode 100644 server/routers/contentstack/contentPage/query.ts create mode 100644 server/routers/contentstack/contentPage/utils.ts diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx index 0e8e98a05..378d52bbf 100644 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx @@ -1,6 +1,6 @@ import { notFound } from "next/navigation" -import ContentPage from "@/components/ContentType/ContentPage" +import ContentPage from "@/components/ContentType/ContentPage/ContentPage" import HotelPage from "@/components/ContentType/HotelPage/HotelPage" import LoyaltyPage from "@/components/ContentType/LoyaltyPage/LoyaltyPage" import { setLang } from "@/i18n/serverContext" @@ -19,7 +19,7 @@ export default async function ContentTypePage({ switch (params.contentType) { case "content-page": - return + return case "loyalty-page": return case "hotel-page": diff --git a/components/ContentType/ContentPage.tsx b/components/ContentType/ContentPage.tsx deleted file mode 100644 index bf2443621..000000000 --- a/components/ContentType/ContentPage.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default async function ContentPage() { - return null -} diff --git a/components/ContentType/ContentPage/ContentPage.tsx b/components/ContentType/ContentPage/ContentPage.tsx new file mode 100644 index 000000000..546bcec0d --- /dev/null +++ b/components/ContentType/ContentPage/ContentPage.tsx @@ -0,0 +1,47 @@ +import { serverClient } from "@/lib/trpc/server" + +import Hero from "@/components/TempDesignSystem/Hero" +import Preamble from "@/components/TempDesignSystem/Text/Preamble" +import Title from "@/components/TempDesignSystem/Text/Title" +import TrackingSDK from "@/components/TrackingSDK" + +import Intro from "./Intro" + +import styles from "./contentPage.module.css" + +import type { LangParams } from "@/types/params" + +export default async function ContentPage({ lang }: LangParams) { + const contentPageRes = await serverClient().contentstack.contentPage.get() + + if (!contentPageRes) { + return null + } + + const { tracking, contentPage } = contentPageRes + const heroImage = contentPage.hero_image + + return ( + <> +
+
+ + {contentPage.header.heading} + {contentPage.header.preamble} + +
+ + {heroImage ? ( +
+ +
+ ) : null} +
+ + + + ) +} diff --git a/components/ContentType/ContentPage/Intro/index.tsx b/components/ContentType/ContentPage/Intro/index.tsx new file mode 100644 index 000000000..a5401164d --- /dev/null +++ b/components/ContentType/ContentPage/Intro/index.tsx @@ -0,0 +1,22 @@ +import { PropsWithChildren } from "react" + +import MaxWidth from "@/components/MaxWidth" + +import styles from "./intro.module.css" + +export default async function Intro({ children }: PropsWithChildren) { + return ( +
+ + + {children} + + +
+ ) +} diff --git a/components/ContentType/ContentPage/Intro/intro.module.css b/components/ContentType/ContentPage/Intro/intro.module.css new file mode 100644 index 000000000..8ae34a4ad --- /dev/null +++ b/components/ContentType/ContentPage/Intro/intro.module.css @@ -0,0 +1,8 @@ +.intro { + padding: var(--Spacing-x4) var(--Spacing-x2); +} + +.content { + display: grid; + gap: var(--Spacing-x3); +} diff --git a/components/ContentType/ContentPage/contentPage.module.css b/components/ContentType/ContentPage/contentPage.module.css new file mode 100644 index 000000000..df5938084 --- /dev/null +++ b/components/ContentType/ContentPage/contentPage.module.css @@ -0,0 +1,17 @@ +.content { + display: grid; + padding-bottom: var(--Spacing-x9); + position: relative; + justify-content: center; + align-items: flex-start; +} + +.header { + background-color: var(--Base-Surface-Subtle-Normal); +} + +.hero { + display: grid; + justify-content: center; + padding: var(--Spacing-x4) var(--Spacing-x2); +} diff --git a/components/ContentType/LoyaltyPage/LoyaltyPage.tsx b/components/ContentType/LoyaltyPage/LoyaltyPage.tsx index 484117c47..b6b90bf47 100644 --- a/components/ContentType/LoyaltyPage/LoyaltyPage.tsx +++ b/components/ContentType/LoyaltyPage/LoyaltyPage.tsx @@ -9,7 +9,6 @@ import TrackingSDK from "@/components/TrackingSDK" import styles from "./loyaltyPage.module.css" -import { ImageVaultAsset } from "@/types/components/imageVaultImage" import type { LangParams } from "@/types/params" export default async function LoyaltyPage({ lang }: LangParams) { @@ -20,7 +19,7 @@ export default async function LoyaltyPage({ lang }: LangParams) { } const { tracking, loyaltyPage } = loyaltyPageRes - const heroImage: ImageVaultAsset = loyaltyPage.header?.hero_image + const heroImage = loyaltyPage.hero_image return ( <>
diff --git a/components/MaxWidth/index.tsx b/components/MaxWidth/index.tsx index 495447a76..0047d0dd2 100644 --- a/components/MaxWidth/index.tsx +++ b/components/MaxWidth/index.tsx @@ -1,17 +1,21 @@ "use client" -import { cva } from "class-variance-authority" -import styles from "./maxWidth.module.css" +import { maxWidthVariants } from "./variants" import type { MaxWidthProps } from "@/types/components/max-width" -const maxWidthVariants = cva(styles.container) - export default function MaxWidth({ className, tag = "section", + variant, + align, ...props }: MaxWidthProps) { const Cmp = tag - return + return ( + + ) } diff --git a/components/MaxWidth/maxWidth.module.css b/components/MaxWidth/maxWidth.module.css index 563ae5721..9197d79dc 100644 --- a/components/MaxWidth/maxWidth.module.css +++ b/components/MaxWidth/maxWidth.module.css @@ -1,4 +1,20 @@ .container { - max-width: var(--max-width, 1140px); position: relative; } + +.container.default { + max-width: var(--max-width, 1140px); +} + +.container.text { + max-width: 49.5rem; +} + +.container.content { + max-width: 74.75rem; +} + +.container.center { + margin: 0 auto; + width: 100vh; +} diff --git a/components/MaxWidth/variants.ts b/components/MaxWidth/variants.ts new file mode 100644 index 000000000..f7c917b5f --- /dev/null +++ b/components/MaxWidth/variants.ts @@ -0,0 +1,21 @@ +import { cva } from "class-variance-authority" + +import styles from "./maxWidth.module.css" + +export const maxWidthVariants = cva(styles.container, { + variants: { + variant: { + text: styles.text, + content: styles.content, + default: styles.default, + }, + align: { + center: styles.center, + left: styles.left, + }, + }, + defaultVariants: { + variant: "default", + align: "center", + }, +}) diff --git a/lib/graphql/Fragments/ContentPage/Breadcrumbs.graphql b/lib/graphql/Fragments/ContentPage/Breadcrumbs.graphql new file mode 100644 index 000000000..0e9dc795c --- /dev/null +++ b/lib/graphql/Fragments/ContentPage/Breadcrumbs.graphql @@ -0,0 +1,25 @@ +fragment ContentPageBreadcrumbs on ContentPage { + web { + breadcrumbs { + title + parentsConnection { + edges { + node { + ... on ContentPage { + web { + breadcrumbs { + title + } + } + system { + locale + uid + } + url + } + } + } + } + } + } +} diff --git a/lib/graphql/Query/ContentPage.graphql b/lib/graphql/Query/ContentPage.graphql new file mode 100644 index 000000000..ff7c0d24d --- /dev/null +++ b/lib/graphql/Query/ContentPage.graphql @@ -0,0 +1,19 @@ +#import "../Fragments/ContentPage/Breadcrumbs.graphql" + +query GetContentPage($locale: String!, $uid: String!) { + content_page(uid: $uid, locale: $locale) { + title + header { + heading + preamble + } + hero_image + ...ContentPageBreadcrumbs + system { + uid + created_at + updated_at + locale + } + } +} diff --git a/lib/graphql/Query/LoyaltyPage.graphql b/lib/graphql/Query/LoyaltyPage.graphql index 6ed01ef83..7026fe288 100644 --- a/lib/graphql/Query/LoyaltyPage.graphql +++ b/lib/graphql/Query/LoyaltyPage.graphql @@ -107,9 +107,8 @@ query GetLoyaltyPage($locale: String!, $uid: String!) { } title heading - header { - hero_image - } + preamble + hero_image sidebar { __typename ... on LoyaltyPageSidebarDynamicContent { diff --git a/server/routers/contentstack/contentPage/index.ts b/server/routers/contentstack/contentPage/index.ts new file mode 100644 index 000000000..52130d7b4 --- /dev/null +++ b/server/routers/contentstack/contentPage/index.ts @@ -0,0 +1,5 @@ +import { mergeRouters } from "@/server/trpc" + +import { contentPageQueryRouter } from "./query" + +export const contentPageRouter = mergeRouters(contentPageQueryRouter) diff --git a/server/routers/contentstack/contentPage/output.ts b/server/routers/contentstack/contentPage/output.ts new file mode 100644 index 000000000..32fe3ec08 --- /dev/null +++ b/server/routers/contentstack/contentPage/output.ts @@ -0,0 +1,30 @@ +import { z } from "zod" + +import { Lang } from "@/constants/languages" + +import { ImageVaultAsset } from "@/types/components/imageVaultImage" + +export const validateContentPageSchema = z.object({ + content_page: z.object({ + title: z.string(), + header: z.object({ + heading: z.string(), + preamble: z.string(), + }), + hero_image: z.any().nullable(), + system: z.object({ + uid: z.string(), + locale: z.nativeEnum(Lang), + created_at: z.string(), + updated_at: z.string(), + }), + }), +}) + +export type ContentPageDataRaw = z.infer + +type ContentPageRaw = ContentPageDataRaw["content_page"] + +export type ContentPage = Omit & { + hero_image?: ImageVaultAsset +} diff --git a/server/routers/contentstack/contentPage/query.ts b/server/routers/contentstack/contentPage/query.ts new file mode 100644 index 000000000..845dd89e3 --- /dev/null +++ b/server/routers/contentstack/contentPage/query.ts @@ -0,0 +1,64 @@ +import { Lang } from "@/constants/languages" +import { GetContentPage } from "@/lib/graphql/Query/ContentPage.graphql" +import { request } from "@/lib/graphql/request" +import { notFound } from "@/server/errors/trpc" +import { contentstackExtendedProcedureUID, router } from "@/server/trpc" + +import { + ContentPage, + ContentPageDataRaw, + validateContentPageSchema, +} from "./output" +import { makeImageVaultImage } from "./utils" + +import { + TrackingChannelEnum, + TrackingSDKPageData, +} from "@/types/components/tracking" + +export const contentPageQueryRouter = router({ + get: contentstackExtendedProcedureUID.query(async ({ ctx }) => { + const { lang, uid } = ctx + + const response = await request(GetContentPage, { + locale: lang, + uid, + }) + + if (!response.data) { + throw notFound(response) + } + + const validatedContentPage = validateContentPageSchema.safeParse( + response.data + ) + + if (!validatedContentPage.success) { + console.error( + `Failed to validate Contentpage Data - (lang: ${lang}, uid: ${uid})` + ) + console.error(validatedContentPage.error) + return null + } + + const contentPageData = validatedContentPage.data.content_page + const contentPage: ContentPage = { + ...contentPageData, + hero_image: makeImageVaultImage(contentPageData.hero_image), + } + + const tracking: TrackingSDKPageData = { + pageId: contentPageData.system.uid, + lang: contentPageData.system.locale as Lang, + publishedDate: contentPageData.system.updated_at, + createdDate: contentPageData.system.created_at, + channel: TrackingChannelEnum["static-content-page"], + pageType: "staticcontentpage", + } + + return { + contentPage, + tracking, + } + }), +}) diff --git a/server/routers/contentstack/contentPage/utils.ts b/server/routers/contentstack/contentPage/utils.ts new file mode 100644 index 000000000..d2f748d93 --- /dev/null +++ b/server/routers/contentstack/contentPage/utils.ts @@ -0,0 +1,9 @@ +import { insertResponseToImageVaultAsset } from "@/utils/imageVault" + +import { InsertResponse } from "@/types/components/imageVaultImage" + +export function makeImageVaultImage(image: any) { + return image && !!Object.keys(image).length + ? insertResponseToImageVaultAsset(image as InsertResponse) + : undefined +} diff --git a/server/routers/contentstack/index.ts b/server/routers/contentstack/index.ts index be1ef1ed3..127673eb0 100644 --- a/server/routers/contentstack/index.ts +++ b/server/routers/contentstack/index.ts @@ -3,6 +3,7 @@ import { router } from "@/server/trpc" import { accountPageRouter } from "./accountPage" import { baseRouter } from "./base" import { breadcrumbsRouter } from "./breadcrumbs" +import { contentPageRouter } from "./contentPage" import { hotelPageRouter } from "./hotelPage" import { languageSwitcherRouter } from "./languageSwitcher" import { loyaltyPageRouter } from "./loyaltyPage" @@ -15,5 +16,6 @@ export const contentstackRouter = router({ hotelPage: hotelPageRouter, languageSwitcher: languageSwitcherRouter, loyaltyPage: loyaltyPageRouter, + contentPage: contentPageRouter, myPages: myPagesRouter, }) diff --git a/server/routers/contentstack/loyaltyPage/output.ts b/server/routers/contentstack/loyaltyPage/output.ts index 20a8e3179..f89796ad9 100644 --- a/server/routers/contentstack/loyaltyPage/output.ts +++ b/server/routers/contentstack/loyaltyPage/output.ts @@ -191,11 +191,8 @@ const loyaltyPageSidebarItem = z.discriminatedUnion("__typename", [ export const validateLoyaltyPageSchema = z.object({ heading: z.string().nullable(), - header: z - .object({ - hero_image: z.any(), - }) - .nullable(), + preamble: z.string().nullable(), + hero_image: z.any().nullable(), blocks: z.array(loyaltyPageBlockItem).nullable(), sidebar: z.array(loyaltyPageSidebarItem).nullable(), system: z.object({ @@ -263,7 +260,11 @@ export type Sidebar = | SideBarDynamicContent type LoyaltyPageDataRaw = z.infer -export type LoyaltyPage = Omit & { +export type LoyaltyPage = Omit< + LoyaltyPageDataRaw, + "blocks" | "sidebar" | "hero_image" +> & { + hero_image?: ImageVaultAsset blocks: Block[] sidebar: Sidebar[] } diff --git a/server/routers/contentstack/loyaltyPage/query.ts b/server/routers/contentstack/loyaltyPage/query.ts index 0a0b99142..5fea2a8dd 100644 --- a/server/routers/contentstack/loyaltyPage/query.ts +++ b/server/routers/contentstack/loyaltyPage/query.ts @@ -12,7 +12,6 @@ import { generateTag, generateTags, } from "@/utils/generateTag" -import { insertResponseToImageVaultAsset } from "@/utils/imageVault" import { removeMultipleSlashes } from "@/utils/url" import { removeEmptyObjects } from "../../utils" @@ -22,9 +21,8 @@ import { validateLoyaltyPageRefsSchema, validateLoyaltyPageSchema, } from "./output" -import { getConnections } from "./utils" +import { getConnections, makeButtonObject, makeImageVaultImage } from "./utils" -import { InsertResponse } from "@/types/components/imageVaultImage" import { LoyaltyBlocksTypenameEnum, LoyaltyCardsGridEnum, @@ -35,35 +33,6 @@ import { TrackingSDKPageData, } from "@/types/components/tracking" -function makeImageVaultImage(image: any) { - return image && !!Object.keys(image).length - ? insertResponseToImageVaultAsset(image as InsertResponse) - : undefined -} - -function makeButtonObject(button: any) { - if (!button) return null - - const isContenstackLink = - button?.is_contentstack_link || button.linkConnection?.edges?.length - - return { - openInNewTab: button?.open_in_new_tab, - title: - button.cta_text || - (isContenstackLink - ? button.linkConnection.edges[0].node.title - : button.external_link.title), - href: isContenstackLink - ? button.linkConnection.edges[0].node.web?.original_url || - removeMultipleSlashes( - `/${button.linkConnection.edges[0].node.system.locale}/${button.linkConnection.edges[0].node.url}` - ) - : button.external_link.href, - isExternal: !isContenstackLink, - } -} - export const loyaltyPageQueryRouter = router({ get: contentstackExtendedProcedureUID.query(async ({ ctx }) => { const { lang, uid } = ctx @@ -208,17 +177,10 @@ export const loyaltyPageQueryRouter = router({ }) : null - const header = response.data.loyalty_page.header - ? { - hero_image: makeImageVaultImage( - response.data.loyalty_page.header.hero_image - ), - } - : null - const loyaltyPage = { heading: response.data.loyalty_page.heading, - header, + preamble: response.data.loyalty_page.preamble, + hero_image: makeImageVaultImage(response.data.loyalty_page.hero_image), system: response.data.loyalty_page.system, blocks, sidebar, diff --git a/server/routers/contentstack/loyaltyPage/utils.ts b/server/routers/contentstack/loyaltyPage/utils.ts index 95e251075..99e66571d 100644 --- a/server/routers/contentstack/loyaltyPage/utils.ts +++ b/server/routers/contentstack/loyaltyPage/utils.ts @@ -1,5 +1,9 @@ +import { insertResponseToImageVaultAsset } from "@/utils/imageVault" +import { removeMultipleSlashes } from "@/utils/url" + import { LoyaltyPageRefsDataRaw } from "./output" +import { InsertResponse } from "@/types/components/imageVaultImage" import { LoyaltyBlocksTypenameEnum, LoyaltyCardsGridEnum, @@ -77,3 +81,32 @@ export function getConnections(refs: LoyaltyPageRefsDataRaw) { return connections } + +export function makeImageVaultImage(image: any) { + return image && !!Object.keys(image).length + ? insertResponseToImageVaultAsset(image as InsertResponse) + : undefined +} + +export function makeButtonObject(button: any) { + if (!button) return null + + const isContenstackLink = + button?.is_contentstack_link || button.linkConnection?.edges?.length + + return { + openInNewTab: button?.open_in_new_tab, + title: + button.cta_text || + (isContenstackLink + ? button.linkConnection.edges[0].node.title + : button.external_link.title), + href: isContenstackLink + ? button.linkConnection.edges[0].node.web?.original_url || + removeMultipleSlashes( + `/${button.linkConnection.edges[0].node.system.locale}/${button.linkConnection.edges[0].node.url}` + ) + : button.external_link.href, + isExternal: !isContenstackLink, + } +} diff --git a/types/components/max-width.ts b/types/components/max-width.ts index e1327b581..4a99b2d99 100644 --- a/types/components/max-width.ts +++ b/types/components/max-width.ts @@ -1,3 +1,9 @@ -export interface MaxWidthProps extends React.HTMLAttributes { +import { VariantProps } from "class-variance-authority" + +import { maxWidthVariants } from "@/components/MaxWidth/variants" + +export interface MaxWidthProps + extends React.HTMLAttributes, + VariantProps { tag?: "article" | "div" | "main" | "section" } diff --git a/types/components/tracking.ts b/types/components/tracking.ts index 354212d8d..92128d939 100644 --- a/types/components/tracking.ts +++ b/types/components/tracking.ts @@ -4,6 +4,7 @@ import type { Lang } from "@/constants/languages" export enum TrackingChannelEnum { "scandic-friends" = "scandic-friends", + "static-content-page" = "static-content-page", } export type TrackingChannel = keyof typeof TrackingChannelEnum