feat(SW-190): added hero to static content pages
This commit is contained in:
@@ -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":
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export default async function ContentPage() {
|
||||
return null
|
||||
}
|
||||
47
components/ContentType/ContentPage/ContentPage.tsx
Normal file
47
components/ContentType/ContentPage/ContentPage.tsx
Normal 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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
22
components/ContentType/ContentPage/Intro/index.tsx
Normal file
22
components/ContentType/ContentPage/Intro/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
.intro {
|
||||
padding: var(--Spacing-x4) var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
17
components/ContentType/ContentPage/contentPage.module.css
Normal file
17
components/ContentType/ContentPage/contentPage.module.css
Normal 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);
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
21
components/MaxWidth/variants.ts
Normal file
21
components/MaxWidth/variants.ts
Normal 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",
|
||||
},
|
||||
})
|
||||
25
lib/graphql/Fragments/ContentPage/Breadcrumbs.graphql
Normal file
25
lib/graphql/Fragments/ContentPage/Breadcrumbs.graphql
Normal file
@@ -0,0 +1,25 @@
|
||||
fragment ContentPageBreadcrumbs on ContentPage {
|
||||
web {
|
||||
breadcrumbs {
|
||||
title
|
||||
parentsConnection {
|
||||
edges {
|
||||
node {
|
||||
... on ContentPage {
|
||||
web {
|
||||
breadcrumbs {
|
||||
title
|
||||
}
|
||||
}
|
||||
system {
|
||||
locale
|
||||
uid
|
||||
}
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
lib/graphql/Query/ContentPage.graphql
Normal file
19
lib/graphql/Query/ContentPage.graphql
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,9 +107,8 @@ query GetLoyaltyPage($locale: String!, $uid: String!) {
|
||||
}
|
||||
title
|
||||
heading
|
||||
header {
|
||||
hero_image
|
||||
}
|
||||
preamble
|
||||
hero_image
|
||||
sidebar {
|
||||
__typename
|
||||
... on LoyaltyPageSidebarDynamicContent {
|
||||
|
||||
5
server/routers/contentstack/contentPage/index.ts
Normal file
5
server/routers/contentstack/contentPage/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { mergeRouters } from "@/server/trpc"
|
||||
|
||||
import { contentPageQueryRouter } from "./query"
|
||||
|
||||
export const contentPageRouter = mergeRouters(contentPageQueryRouter)
|
||||
30
server/routers/contentstack/contentPage/output.ts
Normal file
30
server/routers/contentstack/contentPage/output.ts
Normal 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
|
||||
}
|
||||
64
server/routers/contentstack/contentPage/query.ts
Normal file
64
server/routers/contentstack/contentPage/query.ts
Normal 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,
|
||||
}
|
||||
}),
|
||||
})
|
||||
9
server/routers/contentstack/contentPage/utils.ts
Normal file
9
server/routers/contentstack/contentPage/utils.ts
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user