Merged in feat/SW-1386-startpage-bap (pull request #1219)

feat(SW-1386): add full width campaign to start page

Approved-by: Erik Tiekstra
Approved-by: Chuma Mcphoy (We Ahead)
This commit is contained in:
Christian Andolf
2025-01-29 13:05:34 +00:00
16 changed files with 436 additions and 17 deletions

View File

@@ -0,0 +1,41 @@
.container {
position: relative;
overflow: hidden;
height: 640px;
}
@media screen and (min-width: 768px) {
.container {
height: 880px;
}
}
.content {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
max-width: 800px;
margin: 0 auto;
gap: var(--Spacing-x1);
padding: var(--Spacing-x4) var(--Spacing-x3);
}
.mainContent {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--Spacing-x2);
}
.buttons {
display: flex;
gap: var(--Spacing-x1);
}
.image {
max-width: 100%;
height: 100%;
}

View File

@@ -0,0 +1,76 @@
import Image from "@/components/Image"
import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link"
import BiroScript from "@/components/TempDesignSystem/Text/BiroScript"
import Preamble from "@/components/TempDesignSystem/Text/Preamble"
import Title from "@/components/TempDesignSystem/Text/Title"
import styles from "./fullWidthCampaign.module.css"
import type { FullWidthCampaign } from "@/types/trpc/routers/contentstack/startPage"
interface FullWidthCampaignProps {
content: FullWidthCampaign
}
export default function FullWidthCampaign({ content }: FullWidthCampaignProps) {
const { background_image, primary_button, secondary_button } = content
return (
<div className={styles.container}>
{background_image ? (
<Image
className={styles.image}
alt={background_image.meta.alt || background_image.meta.caption || ""}
src={background_image.url}
focalPoint={background_image.focalPoint}
width={1512}
height={880}
sizes="(min-width: 1512px) 1512px, 100vw"
/>
) : null}
<div className={styles.content}>
<BiroScript color="baseText" type="two">
{content.scripted_top_title}
</BiroScript>
<div className={styles.mainContent}>
<Title color="baseText" textAlign="center" level="h3">
{content.heading}
</Title>
<Preamble color="baseText" textAlign="center">
{content.body_text}
</Preamble>
<div className={styles.buttons}>
{content.has_primary_button ? (
<Button intent="inverted" size="small" theme="base" asChild>
<Link
href={primary_button.href}
target={primary_button.openInNewTab ? "_blank" : undefined}
color="none"
>
{primary_button.title}
</Link>
</Button>
) : null}
{content.has_secondary_button ? (
<Button
intent="secondary"
size="small"
theme="primaryStrong"
asChild
>
<Link
href={secondary_button.href}
target={secondary_button.openInNewTab ? "_blank" : undefined}
color="none"
>
{secondary_button.title}
</Link>
</Button>
) : null}
</div>
</div>
</div>
</div>
)
}

View File

@@ -7,6 +7,7 @@ import JsonToHtml from "@/components/JsonToHtml"
import { SasTierComparison } from "@/components/SasTierComparison" import { SasTierComparison } from "@/components/SasTierComparison"
import AccordionSection from "./Accordion" import AccordionSection from "./Accordion"
import FullWidthCampaign from "./FullWidthCampaign"
import HotelListing from "./HotelListing" import HotelListing from "./HotelListing"
import Table from "./Table" import Table from "./Table"
@@ -98,6 +99,8 @@ export default function Blocks({ blocks }: BlocksProps) {
firstItem={firstItem} firstItem={firstItem}
/> />
) )
case BlocksEnums.block.FullWidthCampaign:
return <FullWidthCampaign content={block.full_width_campaign} />
default: default:
return null return null
} }

View File

@@ -1,8 +1,10 @@
import { assertNullableType } from "graphql"
import { Suspense } from "react" import { Suspense } from "react"
import { getStartPage } from "@/lib/trpc/memoizedRequests" import { getStartPage } from "@/lib/trpc/memoizedRequests"
import Blocks from "@/components/Blocks" import Blocks from "@/components/Blocks"
import FullWidthCampaign from "@/components/Blocks/FullWidthCampaign"
import Image from "@/components/Image" import Image from "@/components/Image"
import PageContainer from "@/components/PageContainer" import PageContainer from "@/components/PageContainer"
import Title from "@/components/TempDesignSystem/Text/Title" import Title from "@/components/TempDesignSystem/Text/Title"
@@ -10,6 +12,8 @@ import TrackingSDK from "@/components/TrackingSDK"
import styles from "./startPage.module.css" import styles from "./startPage.module.css"
import { BlocksEnums } from "@/types/enums/blocks"
export default async function StartPage() { export default async function StartPage() {
const content = await getStartPage() const content = await getStartPage()
if (!content) { if (!content) {
@@ -43,13 +47,24 @@ export default async function StartPage() {
) : null} ) : null}
</header> </header>
<main className={styles.main}> <main className={styles.main}>
<details {blocks
style={{ maxWidth: "100vw", overflow: "hidden", padding: "1rem 0" }} ? blocks.map((block) => {
> if (block.typename === BlocksEnums.block.FullWidthCampaign) {
<summary>JSON data</summary> return (
<pre>{JSON.stringify(content, null, 2)}</pre> <FullWidthCampaign
</details> key={block.typename}
{blocks ? <Blocks blocks={blocks} /> : null} content={block.full_width_campaign}
/>
)
}
return (
<div key={block.typename} className={styles.section}>
<Blocks blocks={[block]} />
</div>
)
})
: null}
</main> </main>
<Suspense fallback={null}> <Suspense fallback={null}>
<TrackingSDK pageData={content.tracking} /> <TrackingSDK pageData={content.tracking} />

View File

@@ -40,9 +40,19 @@
.main { .main {
display: grid; display: grid;
width: 100%;
gap: var(--Spacing-x6); gap: var(--Spacing-x6);
margin: 0 auto; padding: calc(var(--Spacing-x5) * 2) 0 calc(var(--Spacing-x5) * 4);
max-width: var(--max-width-content); }
padding: var(--Spacing-x4) 0;
@media screen and (min-width: 768px) {
.main {
gap: calc(var(--Spacing-x5) * 3);
}
}
.section {
margin-left: auto;
margin-right: auto;
max-width: var(--max-width-content);
width: 100%;
} }

View File

@@ -33,6 +33,10 @@
color: var(--Base-Text-UI-Medium-contrast); color: var(--Base-Text-UI-Medium-contrast);
} }
.baseText {
color: var(--Base-Text-Inverted);
}
.center { .center {
text-align: center; text-align: center;
} }

View File

@@ -9,6 +9,7 @@ const config = {
burgundy: styles.burgundy, burgundy: styles.burgundy,
pale: styles.pale, pale: styles.pale,
textMediumContrast: styles.textMediumContrast, textMediumContrast: styles.textMediumContrast,
baseText: styles.baseText,
}, },
textAlign: { textAlign: {
center: styles.center, center: styles.center,

View File

@@ -0,0 +1,117 @@
#import "../PageLink/AccountPageLink.graphql"
#import "../PageLink/ContentPageLink.graphql"
#import "../PageLink/LoyaltyPageLink.graphql"
#import "../PageLink/HotelPageLink.graphql"
#import "../PageLink/CollectionPageLink.graphql"
#import "../PageLink/DestinationCityPageLink.graphql"
#import "../PageLink/DestinationCountryPageLink.graphql"
#import "../PageLink/DestinationOverviewPageLink.graphql"
#import "../AccountPage/Ref.graphql"
#import "../ContentPage/Ref.graphql"
#import "../HotelPage/Ref.graphql"
#import "../LoyaltyPage/Ref.graphql"
#import "../CollectionPage/Ref.graphql"
#import "../DestinationCityPage/Ref.graphql"
#import "../DestinationCountryPage/Ref.graphql"
#import "../DestinationOverviewPage/Ref.graphql"
fragment FullWidthCampaign on FullWidthCampaign {
background_image
scripted_top_title
heading
body_text
has_primary_button
primary_button {
cta_text
open_in_new_tab
is_contentstack_link
external_link {
href
title
}
linkConnection {
edges {
node {
__typename
...AccountPageLink
...ContentPageLink
...LoyaltyPageLink
...HotelPageLink
...CollectionPageLink
...DestinationCityPageLink
...DestinationCountryPageLink
...DestinationOverviewPageLink
}
}
}
}
has_secondary_button
secondary_button {
cta_text
open_in_new_tab
is_contentstack_link
external_link {
href
title
}
linkConnection {
edges {
node {
__typename
...AccountPageLink
...ContentPageLink
...LoyaltyPageLink
...HotelPageLink
...CollectionPageLink
...DestinationCityPageLink
...DestinationCountryPageLink
...DestinationOverviewPageLink
}
}
}
}
system {
...System
}
}
fragment FullWidthCampaignRefs on FullWidthCampaign {
primary_button {
linkConnection {
edges {
node {
__typename
...AccountPageRef
...ContentPageRef
...HotelPageRef
...LoyaltyPageRef
...CollectionPageRef
...DestinationCityPageRef
...DestinationCountryPageRef
...DestinationOverviewPageRef
}
}
}
}
secondary_button {
linkConnection {
edges {
node {
__typename
...AccountPageRef
...ContentPageRef
...HotelPageRef
...LoyaltyPageRef
...CollectionPageRef
...DestinationCityPageRef
...DestinationCountryPageRef
...DestinationOverviewPageRef
}
}
}
}
system {
...System
}
}

View File

@@ -1,14 +1,30 @@
#import "../../Fragments/System.graphql" #import "../../Fragments/System.graphql"
#import "../../Fragments/Blocks/CardsGrid.graphql" #import "../../Fragments/Blocks/CardsGrid.graphql"
#import "../../Fragments/Blocks/FullWidthCampaign.graphql"
query GetStartPage($locale: String!, $uid: String!) { query GetStartPage($locale: String!, $uid: String!) {
start_page(uid: $uid, locale: $locale) { start_page(locale: $locale, uid: $uid) {
title title
url url
header { header {
heading heading
hero_image hero_image
} }
blocks {
__typename
... on StartPageBlocksFullWidthCampaign {
__typename
full_width_campaign {
full_width_campaignConnection {
edges {
node {
...FullWidthCampaign
}
}
}
}
}
}
system { system {
...System ...System
created_at created_at
@@ -29,6 +45,17 @@ query GetStartPageRefs($locale: String!, $uid: String!) {
blocks { blocks {
__typename __typename
...CardsGrid_StartPageRefs ...CardsGrid_StartPageRefs
... on StartPageBlocksFullWidthCampaign {
full_width_campaign {
full_width_campaignConnection {
edges {
node {
...FullWidthCampaignRefs
}
}
}
}
}
} }
system { system {
...System ...System

View File

@@ -0,0 +1,65 @@
import { z } from "zod"
import * as pageLinks from "@/server/routers/contentstack/schemas/pageLinks"
import { tempImageVaultAssetSchema } from "../imageVault"
import { systemSchema } from "../system"
import { buttonSchema } from "./utils/buttonLinkSchema"
import { BlocksEnums } from "@/types/enums/blocks"
export const fullWidthCampaignSchema = z.object({
full_width_campaign: z
.object({
full_width_campaignConnection: z.object({
edges: z.array(
z.object({
node: z.object({
background_image: tempImageVaultAssetSchema,
heading: z.string().optional(),
body_text: z.string().optional(),
scripted_top_title: z.string().optional(),
has_primary_button: z.boolean().default(false),
primary_button: buttonSchema,
has_secondary_button: z.boolean().default(false),
secondary_button: buttonSchema,
system: systemSchema,
}),
})
),
}),
})
.transform((data) => {
return data.full_width_campaignConnection.edges[0]?.node || null
}),
})
export const fullWidthCampaignBlockSchema = z
.object({
typename: z
.literal(BlocksEnums.block.FullWidthCampaign)
.optional()
.default(BlocksEnums.block.FullWidthCampaign),
})
.merge(fullWidthCampaignSchema)
export const fullWidthCampaignBlockRefsSchema = z.object({
full_width_campaign: z.object({
full_width_campaignConnection: z.object({
edges: z.array(
z.object({
node: z.discriminatedUnion("__typename", [
pageLinks.accountPageRefSchema,
pageLinks.contentPageRefSchema,
pageLinks.loyaltyPageRefSchema,
pageLinks.collectionPageRefSchema,
pageLinks.hotelPageRefSchema,
pageLinks.destinationCityPageRefSchema,
pageLinks.destinationCountryPageRefSchema,
pageLinks.destinationOverviewPageRefSchema,
]),
})
),
}),
}),
})

View File

@@ -6,27 +6,40 @@ import {
cardGridRefsSchema, cardGridRefsSchema,
cardsGridSchema, cardsGridSchema,
} from "../schemas/blocks/cardsGrid" } from "../schemas/blocks/cardsGrid"
import {
fullWidthCampaignBlockRefsSchema,
fullWidthCampaignBlockSchema,
} from "../schemas/blocks/fullWidthCampaign"
import { tempImageVaultAssetSchema } from "../schemas/imageVault" import { tempImageVaultAssetSchema } from "../schemas/imageVault"
import { systemSchema } from "../schemas/system" import { systemSchema } from "../schemas/system"
import { StartPageEnum } from "@/types/enums/startPage" import { StartPageEnum } from "@/types/enums/startPage"
export const startPageCards = z const startPageCards = z
.object({ .object({
__typename: z.literal(StartPageEnum.ContentStack.blocks.CardsGrid), __typename: z.literal(StartPageEnum.ContentStack.blocks.CardsGrid),
}) })
.merge(cardsGridSchema) .merge(cardsGridSchema)
export const blocksSchema = z.discriminatedUnion("__typename", [startPageCards]) const startPageFullWidthCampaign = z
.object({
__typename: z.literal(StartPageEnum.ContentStack.blocks.FullWidthCampaign),
})
.merge(fullWidthCampaignBlockSchema)
export const blocksSchema = z.discriminatedUnion("__typename", [
startPageCards,
startPageFullWidthCampaign,
])
export const startPageSchema = z.object({ export const startPageSchema = z.object({
start_page: z.object({ start_page: z.object({
blocks: discriminatedUnionArray(blocksSchema.options).nullable(),
title: z.string(), title: z.string(),
header: z.object({ header: z.object({
heading: z.string(), heading: z.string(),
hero_image: tempImageVaultAssetSchema, hero_image: tempImageVaultAssetSchema,
}), }),
blocks: discriminatedUnionArray(blocksSchema.options).nullable(),
system: systemSchema.merge( system: systemSchema.merge(
z.object({ z.object({
created_at: z.string(), created_at: z.string(),
@@ -46,8 +59,15 @@ const startPageCardsRefs = z
}) })
.merge(cardGridRefsSchema) .merge(cardGridRefsSchema)
const startPageFullWidthCampaignRef = z
.object({
__typename: z.literal(StartPageEnum.ContentStack.blocks.FullWidthCampaign),
})
.merge(fullWidthCampaignBlockRefsSchema)
const startPageBlockRefsItem = z.discriminatedUnion("__typename", [ const startPageBlockRefsItem = z.discriminatedUnion("__typename", [
startPageCardsRefs, startPageCardsRefs,
startPageFullWidthCampaignRef,
]) ])
export const startPageRefsSchema = z.object({ export const startPageRefsSchema = z.object({

View File

@@ -6,7 +6,7 @@ import { request } from "@/lib/graphql/request"
import { notFound } from "@/server/errors/trpc" import { notFound } from "@/server/errors/trpc"
import { contentstackExtendedProcedureUID, router } from "@/server/trpc" import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
import { generateTag } from "@/utils/generateTag" import { generateTag, generateTagsFromSystem } from "@/utils/generateTag"
import { startPageRefsSchema, startPageSchema } from "./output" import { startPageRefsSchema, startPageSchema } from "./output"
import { import {
@@ -17,6 +17,7 @@ import {
getStartPageRefsSuccessCounter, getStartPageRefsSuccessCounter,
getStartPageSuccessCounter, getStartPageSuccessCounter,
} from "./telemetry" } from "./telemetry"
import { getConnections } from "./utils"
import { import {
TrackingChannelEnum, TrackingChannelEnum,
@@ -103,6 +104,13 @@ export const startPageQueryRouter = router({
query: { lang, uid }, query: { lang, uid },
}) })
) )
const connections = getConnections(validatedRefsData.data)
const tags = [
generateTagsFromSystem(lang, connections),
generateTag(lang, validatedRefsData.data.start_page.system.uid),
].flat()
const response = await request<GetStartPageData>( const response = await request<GetStartPageData>(
GetStartPage, GetStartPage,
{ {
@@ -112,10 +120,11 @@ export const startPageQueryRouter = router({
{ {
cache: "force-cache", cache: "force-cache",
next: { next: {
tags: [generateTag(lang, uid)], tags,
}, },
} }
) )
if (!response.data) { if (!response.data) {
const notFoundError = notFound(response) const notFoundError = notFound(response)
getStartPageFailCounter.add(1, { getStartPageFailCounter.add(1, {

View File

@@ -0,0 +1,24 @@
import { StartPageEnum } from "@/types/enums/startPage"
import type { System } from "@/types/requests/system"
import type { StartPageRefs } from "@/types/trpc/routers/contentstack/startPage"
export function getConnections({ start_page }: StartPageRefs) {
const connections: System["system"][] = [start_page.system]
if (start_page.blocks) {
start_page.blocks.forEach((block) => {
switch (block.__typename) {
case StartPageEnum.ContentStack.blocks.FullWidthCampaign: {
block.full_width_campaign.full_width_campaignConnection.edges.forEach(
({ node }) => {
connections.push(node.system)
}
)
break
}
}
})
}
return connections
}

View File

@@ -11,5 +11,6 @@ export namespace BlocksEnums {
UspGrid = "UspGrid", UspGrid = "UspGrid",
SasTierComparison = "SasTierComparison", SasTierComparison = "SasTierComparison",
HotelListing = "HotelListing", HotelListing = "HotelListing",
FullWidthCampaign = "FullWidthCampaign",
} }
} }

View File

@@ -2,6 +2,7 @@ export namespace StartPageEnum {
export namespace ContentStack { export namespace ContentStack {
export const enum blocks { export const enum blocks {
CardsGrid = "StartPageBlocksCardsGrid", CardsGrid = "StartPageBlocksCardsGrid",
FullWidthCampaign = "StartPageBlocksFullWidthCampaign",
} }
} }
} }

View File

@@ -1,5 +1,6 @@
import type { z } from "zod" import type { z } from "zod"
import type { fullWidthCampaignSchema } from "@/server/routers/contentstack/schemas/blocks/fullWidthCampaign"
import type { import type {
blocksSchema, blocksSchema,
startPageRefsSchema, startPageRefsSchema,
@@ -15,3 +16,7 @@ export interface GetStartPageRefsSchema
export interface StartPageRefs extends z.output<typeof startPageRefsSchema> {} export interface StartPageRefs extends z.output<typeof startPageRefsSchema> {}
export type Block = z.output<typeof blocksSchema> export type Block = z.output<typeof blocksSchema>
export type FullWidthCampaign = z.output<
typeof fullWidthCampaignSchema
>["full_width_campaign"]