Merged in SW-1382-start-page-offers-section (pull request #1221)

SW-1382 start page offers section

* feat(SW-1382): Add InfoCard component to CardsGrid and StartPage

* feat(SW-1382): Add dynamic image positioning for InfoCard in CardsGrid

* refactor(SW-1382): Update InfoCard data transformation and prop naming

* fix(SW-1382): Add flex display to InfoCard image container


Approved-by: Christian Andolf
Approved-by: Erik Tiekstra
This commit is contained in:
Chuma Mcphoy (We Ahead)
2025-01-29 10:21:33 +00:00
parent ca42876eb8
commit b57174647f
17 changed files with 405 additions and 9 deletions

View File

@@ -1,3 +1,4 @@
import InfoCard from "@/components/ContentType/StartPage/InfoCard"
import SectionContainer from "@/components/Section/Container"
import SectionHeader from "@/components/Section/Header"
import Card from "@/components/TempDesignSystem/Card"
@@ -37,7 +38,7 @@ export default function CardsGrid({
topTitle={firstItem}
/>
<Grids.Stackable columns={columns}>
{cards_grid.cards.map((card) => {
{cards_grid.cards.map((card, index) => {
switch (card.__typename) {
case CardsGridEnum.cards.Card:
return (
@@ -55,6 +56,19 @@ export default function CardsGrid({
imageGradient
/>
)
case CardsGridEnum.cards.InfoCard:
return (
<InfoCard
key={card.system.uid}
scriptedTopTitle={card.scriptedTopTitle}
heading={card.heading}
bodyText={card.bodyText}
image={card.image}
primaryButton={card.primaryButton}
secondaryButton={card.secondaryButton}
imagePosition={index % 2 === 0 ? "right" : "left"}
/>
)
case CardsGridEnum.cards.TeaserCard:
return (
<TeaserCard

View File

@@ -0,0 +1,46 @@
import Image from "@/components/Image"
import Card from "@/components/TempDesignSystem/Card"
import styles from "./infoCard.module.css"
import type { InfoCardProps } from "@/types/components/blocks/infoCard"
export default function InfoCard({
scriptedTopTitle,
heading,
bodyText,
image,
primaryButton,
secondaryButton,
theme = "one",
imagePosition = "right",
}: InfoCardProps) {
return (
<article className={styles.container}>
{image ? (
<div
className={`${styles.imageContainer} ${styles[`image-${imagePosition}`]}`}
>
<Image
src={image.url}
alt={image.title}
sizes="(min-width: 768px) 724px, 358px"
width={358}
height={179}
focalPoint={image.focalPoint}
className={styles.image}
/>
</div>
) : null}
<Card
scriptedTopTitle={scriptedTopTitle}
heading={heading}
bodyText={bodyText}
primaryButton={primaryButton}
secondaryButton={secondaryButton}
className={styles.card}
theme={theme}
/>
</article>
)
}

View File

@@ -0,0 +1,47 @@
.container {
display: grid;
gap: var(--Spacing-x-quarter);
}
.image {
object-fit: cover;
overflow: hidden;
width: 100%;
height: 179px; /* Exact mobile height from Figma */
border-radius: var(--Corner-radius-Medium);
}
.imageContainer {
display: flex;
}
@media (min-width: 768px) {
.container:has(.imageContainer) {
gap: var(--Spacing-x2);
align-items: center;
}
.container:not(:has(.imageContainer)) {
grid-template-columns: 1fr;
}
.container:has(.image-right) {
grid-template-columns: 456px 1fr;
}
.container:has(.image-left) {
grid-template-columns: 1fr 456px;
}
.image-right {
order: 2;
}
.image-left {
order: 0;
}
.image {
height: 320px; /* Desktop height from Figma */
}
}

View File

@@ -2,6 +2,7 @@ import { Suspense } from "react"
import { getStartPage } from "@/lib/trpc/memoizedRequests"
import Blocks from "@/components/Blocks"
import Image from "@/components/Image"
import PageContainer from "@/components/PageContainer"
import Title from "@/components/TempDesignSystem/Text/Title"
@@ -15,7 +16,7 @@ export default async function StartPage() {
return null
}
const { header } = content.startPage
const { header, blocks } = content.startPage
return (
<PageContainer>
@@ -41,12 +42,15 @@ export default async function StartPage() {
/>
) : null}
</header>
<details
style={{ maxWidth: "100vw", overflow: "hidden", padding: "1rem 0" }}
>
<summary>JSON data</summary>
<pre>{JSON.stringify(content, null, 2)}</pre>
</details>
<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}
</main>
<Suspense fallback={null}>
<TrackingSDK pageData={content.tracking} />
</Suspense>

View File

@@ -37,3 +37,12 @@
.topImage {
max-width: 100%;
}
.main {
display: grid;
width: 100%;
gap: var(--Spacing-x6);
margin: 0 auto;
max-width: var(--max-width-content);
padding: var(--Spacing-x4) 0;
}

View File

@@ -1,8 +1,10 @@
#import "./Card.graphql"
#import "./InfoCard.graphql"
#import "./LoyaltyCard.graphql"
#import "./TeaserCard.graphql"
#import "./Refs/Card.graphql"
#import "./Refs/InfoCard.graphql"
#import "./Refs/LoyaltyCard.graphql"
#import "./Refs/TeaserCard.graphql"
@@ -17,6 +19,7 @@ fragment CardsGrid_ContentPage on ContentPageBlocksCardsGrid {
node {
__typename
...CardBlock
...InfoCardBlock
...LoyaltyCardBlock
...TeaserCardBlock
}
@@ -32,6 +35,7 @@ fragment CardsGrid_ContentPageRefs on ContentPageBlocksCardsGrid {
node {
__typename
...CardBlockRef
...InfoCardBlockRef
...LoyaltyCardBlockRef
...TeaserCardBlockRef
}
@@ -103,3 +107,37 @@ fragment CardsGrid_LoyaltyPageRefs on LoyaltyPageBlocksCardsGrid {
}
}
}
fragment CardsGrid_StartPage on StartPageBlocksCardsGrid {
cards_grid {
layout
preamble
theme
title
cardConnection(limit: 10) {
edges {
node {
__typename
...CardBlock
...TeaserCardBlock
...InfoCardBlock
}
}
}
}
}
fragment CardsGrid_StartPageRefs on StartPageBlocksCardsGrid {
cards_grid {
cardConnection(limit: 10) {
edges {
node {
__typename
...CardBlockRef
...TeaserCardBlockRef
...InfoCardBlockRef
}
}
}
}
}

View File

@@ -0,0 +1,73 @@
#import "../System.graphql"
#import "../PageLink/AccountPageLink.graphql"
#import "../PageLink/CollectionPageLink.graphql"
#import "../PageLink/ContentPageLink.graphql"
#import "../PageLink/DestinationCityPageLink.graphql"
#import "../PageLink/DestinationCountryPageLink.graphql"
#import "../PageLink/DestinationOverviewPageLink.graphql"
#import "../PageLink/HotelPageLink.graphql"
#import "../PageLink/LoyaltyPageLink.graphql"
#import "../PageLink/StartPageLink.graphql"
fragment InfoCardBlock on InfoCard {
scripted_top_title
heading
body_text
image
title
primary_button {
is_contentstack_link
cta_text
open_in_new_tab
external_link {
title
href
}
linkConnection {
edges {
node {
__typename
...AccountPageLink
...CollectionPageLink
...ContentPageLink
...DestinationCityPageLink
...DestinationCountryPageLink
...DestinationOverviewPageLink
...HotelPageLink
...LoyaltyPageLink
...StartPageLink
}
}
}
}
secondary_button {
is_contentstack_link
cta_text
open_in_new_tab
external_link {
title
href
}
linkConnection {
edges {
node {
__typename
...AccountPageLink
...CollectionPageLink
...ContentPageLink
...DestinationCityPageLink
...DestinationCountryPageLink
...DestinationOverviewPageLink
...HotelPageLink
...LoyaltyPageLink
...StartPageLink
}
}
}
}
system {
...System
}
}

View File

@@ -0,0 +1,51 @@
#import "../../AccountPage/Ref.graphql"
#import "../../CollectionPage/Ref.graphql"
#import "../../ContentPage/Ref.graphql"
#import "../../DestinationCityPage/Ref.graphql"
#import "../../DestinationCountryPage/Ref.graphql"
#import "../../DestinationOverviewPage/Ref.graphql"
#import "../../HotelPage/Ref.graphql"
#import "../../LoyaltyPage/Ref.graphql"
#import "../../StartPage/Ref.graphql"
fragment InfoCardBlockRef on InfoCard {
secondary_button {
linkConnection {
edges {
node {
__typename
...AccountPageRef
...CollectionPageRef
...ContentPageRef
...DestinationCityPageRef
...DestinationCountryPageRef
...DestinationOverviewPageRef
...HotelPageRef
...LoyaltyPageRef
...StartPageRef
}
}
}
}
primary_button {
linkConnection {
edges {
node {
__typename
...AccountPageRef
...CollectionPageRef
...ContentPageRef
...DestinationCityPageRef
...DestinationCountryPageRef
...DestinationOverviewPageRef
...HotelPageRef
...LoyaltyPageRef
...StartPageRef
}
}
}
}
system {
...System
}
}

View File

@@ -1,4 +1,5 @@
#import "../../Fragments/System.graphql"
#import "../../Fragments/Blocks/CardsGrid.graphql"
query GetStartPage($locale: String!, $uid: String!) {
start_page(uid: $uid, locale: $locale) {
@@ -13,6 +14,10 @@ query GetStartPage($locale: String!, $uid: String!) {
created_at
updated_at
}
blocks {
__typename
...CardsGrid_StartPage
}
}
trackingProps: start_page(locale: "en", uid: $uid) {
url
@@ -21,6 +26,10 @@ query GetStartPage($locale: String!, $uid: String!) {
query GetStartPageRefs($locale: String!, $uid: String!) {
start_page(locale: $locale, uid: $uid) {
blocks {
__typename
...CardsGrid_StartPageRefs
}
system {
...System
}

View File

@@ -157,6 +157,34 @@ const loyaltyCardBlockSchema = z.object({
title: z.string().optional(),
})
export const infoCardBlockSchema = z.object({
__typename: z.literal(CardsGridEnum.cards.InfoCard),
scripted_top_title: z.string().optional(),
heading: z.string().optional().default(""),
body_text: z.string().optional().default(""),
image: tempImageVaultAssetSchema,
title: z.string().optional(),
primary_button: buttonSchema.optional().nullable(),
secondary_button: buttonSchema.optional().nullable(),
system: systemSchema,
})
export function transformInfoCardBlock(card: typeof infoCardBlockSchema._type) {
return {
__typename: card.__typename,
scriptedTopTitle: card.scripted_top_title,
heading: card.heading,
bodyText: card.body_text,
image: card.image,
title: card.title,
primaryButton: card.primary_button?.href ? card.primary_button : undefined,
secondaryButton: card.secondary_button?.href
? card.secondary_button
: undefined,
system: card.system,
}
}
export const cardsGridSchema = z.object({
typename: z
.literal(BlocksEnums.block.CardsGrid)
@@ -171,6 +199,7 @@ export const cardsGridSchema = z.object({
cardBlockSchema,
loyaltyCardBlockSchema,
teaserCardBlockSchema,
infoCardBlockSchema,
]),
})
),
@@ -191,6 +220,8 @@ export const cardsGridSchema = z.object({
return transformCardBlock(card.node)
} else if (card.node.__typename === CardsGridEnum.cards.TeaserCard) {
return transformTeaserCardBlock(card.node)
} else if (card.node.__typename === CardsGridEnum.cards.InfoCard) {
return transformInfoCardBlock(card.node)
} else {
return {
__typename: card.node.__typename,
@@ -218,6 +249,7 @@ export function transformCardBlockRefs(
card:
| typeof cardBlockRefsSchema._type
| typeof teaserCardBlockRefsSchema._type
| typeof infoCardBlockRefsSchema._type
) {
const cards = [card.system]
if (card.primary_button) {
@@ -242,6 +274,13 @@ export const teaserCardBlockRefsSchema = z.object({
system: systemSchema,
})
export const infoCardBlockRefsSchema = z.object({
__typename: z.literal(CardsGridEnum.cards.InfoCard),
primary_button: linkConnectionRefsSchema,
secondary_button: linkConnectionRefsSchema,
system: systemSchema,
})
export const cardGridRefsSchema = z.object({
cards_grid: z
.object({
@@ -252,6 +291,7 @@ export const cardGridRefsSchema = z.object({
cardBlockRefsSchema,
loyaltyCardBlockRefsSchema,
teaserCardBlockRefsSchema,
infoCardBlockRefsSchema,
]),
})
),
@@ -262,7 +302,8 @@ export const cardGridRefsSchema = z.object({
.map(({ node }) => {
if (
node.__typename === CardsGridEnum.cards.Card ||
node.__typename === CardsGridEnum.cards.TeaserCard
node.__typename === CardsGridEnum.cards.TeaserCard ||
node.__typename === CardsGridEnum.cards.InfoCard
) {
return transformCardBlockRefs(node)
} else {

View File

@@ -136,6 +136,7 @@ export const imageVaultAssetTransformedSchema = imageVaultAssetSchema.transform(
)
export const tempImageVaultAssetSchema = imageVaultAssetSchema
.nullable()
.optional()
.or(
// Temp since there is a bug in Contentstack

View File

@@ -1,10 +1,27 @@
import { z } from "zod"
import { discriminatedUnionArray } from "@/lib/discriminatedUnion"
import {
cardGridRefsSchema,
cardsGridSchema,
} from "../schemas/blocks/cardsGrid"
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
import { systemSchema } from "../schemas/system"
import { StartPageEnum } from "@/types/enums/startPage"
export const startPageCards = z
.object({
__typename: z.literal(StartPageEnum.ContentStack.blocks.CardsGrid),
})
.merge(cardsGridSchema)
export const blocksSchema = z.discriminatedUnion("__typename", [startPageCards])
export const startPageSchema = z.object({
start_page: z.object({
blocks: discriminatedUnionArray(blocksSchema.options).nullable(),
title: z.string(),
header: z.object({
heading: z.string(),
@@ -23,8 +40,19 @@ export const startPageSchema = z.object({
})
/** REFS */
const startPageCardsRefs = z
.object({
__typename: z.literal(StartPageEnum.ContentStack.blocks.CardsGrid),
})
.merge(cardGridRefsSchema)
const startPageBlockRefsItem = z.discriminatedUnion("__typename", [
startPageCardsRefs,
])
export const startPageRefsSchema = z.object({
start_page: z.object({
blocks: discriminatedUnionArray(startPageBlockRefsItem.options).nullable(),
system: systemSchema,
}),
})

View File

@@ -2,12 +2,14 @@ import type { Block as AccountPageBlock } from "@/types/trpc/routers/contentstac
import type { Block as CollectionPageBlock } from "@/types/trpc/routers/contentstack/collectionPage"
import type { Block as ContentPageBlock } from "@/types/trpc/routers/contentstack/contentPage"
import type { Block as LoyaltyPageBlock } from "@/types/trpc/routers/contentstack/loyaltyPage"
import type { Block as StartPageBlock } from "@/types/trpc/routers/contentstack/startPage"
export type Blocks =
| AccountPageBlock
| CollectionPageBlock
| ContentPageBlock
| LoyaltyPageBlock
| StartPageBlock
export interface BlocksProps {
blocks: Blocks[]

View File

@@ -0,0 +1,22 @@
import type { VariantProps } from "class-variance-authority"
import type { ImageVaultAsset } from "@/types/components/imageVault"
import type { CardProps } from "@/components/TempDesignSystem/Card/card"
import type { cardVariants } from "@/components/TempDesignSystem/Card/variants"
type CardTheme = Exclude<
NonNullable<VariantProps<typeof cardVariants>["theme"]>,
"image"
>
export interface InfoCardProps {
scriptedTopTitle?: string
heading: string
bodyText: string
image?: ImageVaultAsset
imagePosition?: "left" | "right"
primaryButton?: CardProps["primaryButton"]
secondaryButton?: CardProps["secondaryButton"]
theme?: CardTheme
className?: string
}

View File

@@ -3,6 +3,7 @@ export namespace CardsGridEnum {
Card = "Card",
LoyaltyCard = "LoyaltyCard",
TeaserCard = "TeaserCard",
InfoCard = "InfoCard",
}
}

7
types/enums/startPage.ts Normal file
View File

@@ -0,0 +1,7 @@
export namespace StartPageEnum {
export namespace ContentStack {
export const enum blocks {
CardsGrid = "StartPageBlocksCardsGrid",
}
}
}

View File

@@ -1,6 +1,7 @@
import type { z } from "zod"
import type {
blocksSchema,
startPageRefsSchema,
startPageSchema,
} from "@/server/routers/contentstack/startPage/output"
@@ -12,3 +13,5 @@ export interface GetStartPageRefsSchema
extends z.input<typeof startPageRefsSchema> {}
export interface StartPageRefs extends z.output<typeof startPageRefsSchema> {}
export type Block = z.output<typeof blocksSchema>