feat(SW-190): added hero to static content pages

This commit is contained in:
Erik Tiekstra
2024-08-14 09:29:00 +02:00
parent f1ca9a0704
commit 8220a39a8f
23 changed files with 351 additions and 64 deletions

View File

@@ -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 <ContentPage />
return <ContentPage lang={params.lang} />
case "loyalty-page":
return <LoyaltyPage />
case "hotel-page":

View File

@@ -1,3 +0,0 @@
export default async function ContentPage() {
return null
}

View File

@@ -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 (
<>
<section>
<header className={styles.header}>
<Intro>
<Title as="h2">{contentPage.header.heading}</Title>
<Preamble>{contentPage.header.preamble}</Preamble>
</Intro>
</header>
{heroImage ? (
<div className={styles.hero}>
<Hero
alt={heroImage.meta.alt || heroImage.meta.caption || ""}
src={heroImage.url}
/>
</div>
) : null}
</section>
<TrackingSDK pageData={tracking} />
</>
)
}

View File

@@ -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 (
<div className={styles.intro}>
<MaxWidth variant="content" tag="div">
<MaxWidth
className={styles.content}
variant="text"
align="left"
tag="div"
>
{children}
</MaxWidth>
</MaxWidth>
</div>
)
}

View File

@@ -0,0 +1,8 @@
.intro {
padding: var(--Spacing-x4) var(--Spacing-x2);
}
.content {
display: grid;
gap: var(--Spacing-x3);
}

View File

@@ -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);
}

View File

@@ -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 (
<>
<section className={styles.content}>

View File

@@ -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 <Cmp className={maxWidthVariants({ className })} {...props} />
return (
<Cmp
className={maxWidthVariants({ className, variant, align })}
{...props}
/>
)
}

View File

@@ -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;
}

View File

@@ -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",
},
})

View File

@@ -0,0 +1,25 @@
fragment ContentPageBreadcrumbs on ContentPage {
web {
breadcrumbs {
title
parentsConnection {
edges {
node {
... on ContentPage {
web {
breadcrumbs {
title
}
}
system {
locale
uid
}
url
}
}
}
}
}
}
}

View File

@@ -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
}
}
}

View File

@@ -107,9 +107,8 @@ query GetLoyaltyPage($locale: String!, $uid: String!) {
}
title
heading
header {
hero_image
}
preamble
hero_image
sidebar {
__typename
... on LoyaltyPageSidebarDynamicContent {

View File

@@ -0,0 +1,5 @@
import { mergeRouters } from "@/server/trpc"
import { contentPageQueryRouter } from "./query"
export const contentPageRouter = mergeRouters(contentPageQueryRouter)

View File

@@ -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<typeof validateContentPageSchema>
type ContentPageRaw = ContentPageDataRaw["content_page"]
export type ContentPage = Omit<ContentPageRaw, "hero_image"> & {
hero_image?: ImageVaultAsset
}

View File

@@ -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<ContentPageDataRaw>(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,
}
}),
})

View File

@@ -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
}

View File

@@ -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,
})

View File

@@ -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<typeof validateLoyaltyPageSchema>
export type LoyaltyPage = Omit<LoyaltyPageDataRaw, "blocks" | "sidebar"> & {
export type LoyaltyPage = Omit<
LoyaltyPageDataRaw,
"blocks" | "sidebar" | "hero_image"
> & {
hero_image?: ImageVaultAsset
blocks: Block[]
sidebar: Sidebar[]
}

View File

@@ -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,

View File

@@ -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,
}
}

View File

@@ -1,3 +1,9 @@
export interface MaxWidthProps extends React.HTMLAttributes<HTMLElement> {
import { VariantProps } from "class-variance-authority"
import { maxWidthVariants } from "@/components/MaxWidth/variants"
export interface MaxWidthProps
extends React.HTMLAttributes<HTMLElement>,
VariantProps<typeof maxWidthVariants> {
tag?: "article" | "div" | "main" | "section"
}

View File

@@ -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