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

View File

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

View File

@@ -40,9 +40,19 @@
.main {
display: grid;
width: 100%;
gap: var(--Spacing-x6);
margin: 0 auto;
max-width: var(--max-width-content);
padding: var(--Spacing-x4) 0;
padding: calc(var(--Spacing-x5) * 2) 0 calc(var(--Spacing-x5) * 4);
}
@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);
}
.baseText {
color: var(--Base-Text-Inverted);
}
.center {
text-align: center;
}

View File

@@ -9,6 +9,7 @@ const config = {
burgundy: styles.burgundy,
pale: styles.pale,
textMediumContrast: styles.textMediumContrast,
baseText: styles.baseText,
},
textAlign: {
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/Blocks/CardsGrid.graphql"
#import "../../Fragments/Blocks/FullWidthCampaign.graphql"
query GetStartPage($locale: String!, $uid: String!) {
start_page(uid: $uid, locale: $locale) {
start_page(locale: $locale, uid: $uid) {
title
url
header {
heading
hero_image
}
blocks {
__typename
... on StartPageBlocksFullWidthCampaign {
__typename
full_width_campaign {
full_width_campaignConnection {
edges {
node {
...FullWidthCampaign
}
}
}
}
}
}
system {
...System
created_at
@@ -29,6 +45,17 @@ query GetStartPageRefs($locale: String!, $uid: String!) {
blocks {
__typename
...CardsGrid_StartPageRefs
... on StartPageBlocksFullWidthCampaign {
full_width_campaign {
full_width_campaignConnection {
edges {
node {
...FullWidthCampaignRefs
}
}
}
}
}
}
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,
cardsGridSchema,
} from "../schemas/blocks/cardsGrid"
import {
fullWidthCampaignBlockRefsSchema,
fullWidthCampaignBlockSchema,
} from "../schemas/blocks/fullWidthCampaign"
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
import { systemSchema } from "../schemas/system"
import { StartPageEnum } from "@/types/enums/startPage"
export const startPageCards = z
const startPageCards = z
.object({
__typename: z.literal(StartPageEnum.ContentStack.blocks.CardsGrid),
})
.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({
start_page: z.object({
blocks: discriminatedUnionArray(blocksSchema.options).nullable(),
title: z.string(),
header: z.object({
heading: z.string(),
hero_image: tempImageVaultAssetSchema,
}),
blocks: discriminatedUnionArray(blocksSchema.options).nullable(),
system: systemSchema.merge(
z.object({
created_at: z.string(),
@@ -46,8 +59,15 @@ const startPageCardsRefs = z
})
.merge(cardGridRefsSchema)
const startPageFullWidthCampaignRef = z
.object({
__typename: z.literal(StartPageEnum.ContentStack.blocks.FullWidthCampaign),
})
.merge(fullWidthCampaignBlockRefsSchema)
const startPageBlockRefsItem = z.discriminatedUnion("__typename", [
startPageCardsRefs,
startPageFullWidthCampaignRef,
])
export const startPageRefsSchema = z.object({

View File

@@ -6,7 +6,7 @@ import { request } from "@/lib/graphql/request"
import { notFound } from "@/server/errors/trpc"
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
import { generateTag } from "@/utils/generateTag"
import { generateTag, generateTagsFromSystem } from "@/utils/generateTag"
import { startPageRefsSchema, startPageSchema } from "./output"
import {
@@ -17,6 +17,7 @@ import {
getStartPageRefsSuccessCounter,
getStartPageSuccessCounter,
} from "./telemetry"
import { getConnections } from "./utils"
import {
TrackingChannelEnum,
@@ -103,6 +104,13 @@ export const startPageQueryRouter = router({
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>(
GetStartPage,
{
@@ -112,10 +120,11 @@ export const startPageQueryRouter = router({
{
cache: "force-cache",
next: {
tags: [generateTag(lang, uid)],
tags,
},
}
)
if (!response.data) {
const notFoundError = notFound(response)
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",
SasTierComparison = "SasTierComparison",
HotelListing = "HotelListing",
FullWidthCampaign = "FullWidthCampaign",
}
}

View File

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

View File

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