diff --git a/components/Blocks/CarouselCards.tsx b/components/Blocks/CarouselCards.tsx new file mode 100644 index 000000000..a7dd207f9 --- /dev/null +++ b/components/Blocks/CarouselCards.tsx @@ -0,0 +1,35 @@ +import SectionContainer from "@/components/Section/Container" +import SectionHeader from "@/components/Section/Header" + +import type { CarouselCardsProps } from "@/types/components/blocks/carouselCards" + +export default function CarouselCards({ carousel_cards }: CarouselCardsProps) { + const { + heading, + enableFilters, + filterCategories, + cards, + defaultFilter, + link, + } = carousel_cards + + return ( + + + {enableFilters && ( +
+ Filter data +

Todo: Add filter component here

+
+            {JSON.stringify({ filterCategories, defaultFilter }, null, 2)}
+          
+
+ )} +
+ Carousel cards +

Todo: Add carousel cards component here

+
{JSON.stringify({ cards }, null, 2)}
+
+
+ ) +} diff --git a/components/Blocks/index.tsx b/components/Blocks/index.tsx index 36a31daaf..6b4995b1a 100644 --- a/components/Blocks/index.tsx +++ b/components/Blocks/index.tsx @@ -1,4 +1,5 @@ import CardsGrid from "@/components/Blocks/CardsGrid" +import CarouselCards from "@/components/Blocks/CarouselCards" import DynamicContent from "@/components/Blocks/DynamicContent" import ShortcutsList from "@/components/Blocks/ShortcutsList" import TextCols from "@/components/Blocks/TextCols" @@ -51,6 +52,13 @@ export default function Blocks({ blocks }: BlocksProps) { key={`${block.dynamic_content.title}-${idx}`} /> ) + case BlocksEnums.block.CarouselCards: + return ( + + ) case BlocksEnums.block.HotelListing: const { heading, contentType, locationFilter, hotelsToInclude } = block.hotel_listing diff --git a/lib/graphql/Fragments/Blocks/CarouselCards.graphql b/lib/graphql/Fragments/Blocks/CarouselCards.graphql new file mode 100644 index 000000000..1b2cbaa99 --- /dev/null +++ b/lib/graphql/Fragments/Blocks/CarouselCards.graphql @@ -0,0 +1,96 @@ +#import "../System.graphql" +#import "./ContentCard.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" + +#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 CarouselCards_StartPage on StartPageBlocksCarouselCards { + carousel_cards { + heading + enable_filters + default_filter + card_groups { + filter_category { + filter_identifier + filter_label + } + cardConnection { + edges { + node { + ...ContentCardBlock + } + } + } + } + link { + cta_text + is_contentstack_link + open_in_new_tab + external_link { + href + title + } + linkConnection { + edges { + node { + __typename + ...AccountPageLink + ...CollectionPageLink + ...ContentPageLink + ...DestinationCityPageLink + ...DestinationCountryPageLink + ...DestinationOverviewPageLink + ...HotelPageLink + ...LoyaltyPageLink + } + } + } + } + } +} + +fragment CarouselCards_StartPageRefs on StartPageBlocksCarouselCards { + carousel_cards { + card_groups { + cardConnection { + edges { + node { + ...ContentCardBlockRef + } + } + } + } + link { + linkConnection { + edges { + node { + __typename + ...AccountPageRef + ...CollectionPageRef + ...ContentPageRef + ...DestinationCityPageRef + ...DestinationCountryPageRef + ...DestinationOverviewPageRef + ...HotelPageRef + ...LoyaltyPageRef + } + } + } + } + } +} diff --git a/lib/graphql/Fragments/Blocks/ContentCard.graphql b/lib/graphql/Fragments/Blocks/ContentCard.graphql new file mode 100644 index 000000000..34a1c5241 --- /dev/null +++ b/lib/graphql/Fragments/Blocks/ContentCard.graphql @@ -0,0 +1,127 @@ +#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" + +#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" +#import "../StartPage/Ref.graphql" + +fragment ContentCardBlock on ContentCard { + __typename + title + heading + image + body_text + has_primary_button + has_secondary_button + promo_text + primary_button { + cta_text + is_contentstack_link + open_in_new_tab + external_link { + href + title + } + linkConnection { + edges { + node { + __typename + ...AccountPageLink + ...CollectionPageLink + ...ContentPageLink + ...DestinationCityPageLink + ...DestinationCountryPageLink + ...DestinationOverviewPageLink + ...HotelPageLink + ...LoyaltyPageLink + ...StartPageLink + } + } + } + } + secondary_button { + cta_text + is_contentstack_link + open_in_new_tab + external_link { + href + title + } + linkConnection { + edges { + node { + __typename + ...AccountPageLink + ...CollectionPageLink + ...ContentPageLink + ...DestinationCityPageLink + ...DestinationCountryPageLink + ...DestinationOverviewPageLink + ...HotelPageLink + ...LoyaltyPageLink + ...StartPageLink + } + } + } + } + system { + ...System + } +} + +fragment ContentCardBlockRef on ContentCard { + __typename + primary_button { + linkConnection { + edges { + node { + __typename + ...AccountPageRef + ...CollectionPageRef + ...ContentPageRef + ...DestinationCityPageRef + ...DestinationCountryPageRef + ...DestinationOverviewPageRef + ...HotelPageRef + ...LoyaltyPageRef + ...StartPageRef + } + } + } + } + secondary_button { + linkConnection { + edges { + node { + __typename + ...AccountPageRef + ...CollectionPageRef + ...ContentPageRef + ...DestinationCityPageRef + ...DestinationCountryPageRef + ...DestinationOverviewPageRef + ...HotelPageRef + ...LoyaltyPageRef + ...StartPageRef + } + } + } + } + system { + ...System + } +} diff --git a/lib/graphql/Fragments/Blocks/Refs/ContentCard.graphql b/lib/graphql/Fragments/Blocks/Refs/ContentCard.graphql new file mode 100644 index 000000000..2d6441ab0 --- /dev/null +++ b/lib/graphql/Fragments/Blocks/Refs/ContentCard.graphql @@ -0,0 +1,52 @@ +#import "../../System.graphql" +#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 ContentCardBlockRef on ContentCard { + 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 + } +} diff --git a/lib/graphql/Query/StartPage/StartPage.graphql b/lib/graphql/Query/StartPage/StartPage.graphql index a158e3d3f..03c113478 100644 --- a/lib/graphql/Query/StartPage/StartPage.graphql +++ b/lib/graphql/Query/StartPage/StartPage.graphql @@ -1,6 +1,7 @@ #import "../../Fragments/System.graphql" #import "../../Fragments/Blocks/CardsGrid.graphql" #import "../../Fragments/Blocks/FullWidthCampaign.graphql" +#import "../../Fragments/Blocks/CarouselCards.graphql" query GetStartPage($locale: String!, $uid: String!) { start_page(locale: $locale, uid: $uid) { @@ -13,6 +14,7 @@ query GetStartPage($locale: String!, $uid: String!) { blocks { __typename ...CardsGrid_StartPage + ...CarouselCards_StartPage ... on StartPageBlocksFullWidthCampaign { __typename full_width_campaign { @@ -42,6 +44,7 @@ query GetStartPageRefs($locale: String!, $uid: String!) { blocks { __typename ...CardsGrid_StartPageRefs + ...CarouselCards_StartPageRefs ... on StartPageBlocksFullWidthCampaign { full_width_campaign { full_width_campaignConnection { diff --git a/server/routers/contentstack/schemas/blocks/cards/contentCard.ts b/server/routers/contentstack/schemas/blocks/cards/contentCard.ts new file mode 100644 index 000000000..2d1f5ba8b --- /dev/null +++ b/server/routers/contentstack/schemas/blocks/cards/contentCard.ts @@ -0,0 +1,44 @@ +import { z } from "zod" + +import { tempImageVaultAssetSchema } from "../../imageVault" +import { systemSchema } from "../../system" +import { buttonSchema } from "../utils/buttonLinkSchema" +import { linkConnectionRefsSchema } from "../utils/linkConnection" + +import { CardsEnum } from "@/types/enums/cards" + +export const contentCardSchema = z.object({ + __typename: z.literal(CardsEnum.ContentCard), + title: z.string(), + heading: z.string(), + image: tempImageVaultAssetSchema, + body_text: z.string(), + promo_text: z.string().optional(), + has_primary_button: z.boolean(), + has_secondary_button: z.boolean(), + primary_button: buttonSchema, + secondary_button: buttonSchema, + system: systemSchema, +}) + +export const contentCardRefSchema = z.object({ + __typename: z.literal(CardsEnum.ContentCard), + primary_button: linkConnectionRefsSchema, + secondary_button: linkConnectionRefsSchema, + system: systemSchema, +}) + +export function transformContentCard(card: typeof contentCardSchema._type) { + return { + __typename: card.__typename, + title: card.title, + heading: card.heading, + image: card.image, + bodyText: card.body_text, + promoText: card.promo_text, + primaryButton: card.has_primary_button ? card.primary_button : undefined, + secondaryButton: card.has_secondary_button + ? card.secondary_button + : undefined, + } +} diff --git a/server/routers/contentstack/schemas/blocks/cards/infoCard.ts b/server/routers/contentstack/schemas/blocks/cards/infoCard.ts new file mode 100644 index 000000000..14c916806 --- /dev/null +++ b/server/routers/contentstack/schemas/blocks/cards/infoCard.ts @@ -0,0 +1,43 @@ +import { z } from "zod" + +import { tempImageVaultAssetSchema } from "../../imageVault" +import { systemSchema } from "../../system" +import { buttonSchema } from "../utils/buttonLinkSchema" +import { linkConnectionRefsSchema } from "../utils/linkConnection" + +import { CardsEnum } from "@/types/enums/cards" + +export const infoCardBlockSchema = z.object({ + __typename: z.literal(CardsEnum.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 infoCardBlockRefsSchema = z.object({ + __typename: z.literal(CardsEnum.InfoCard), + primary_button: linkConnectionRefsSchema, + secondary_button: linkConnectionRefsSchema, + system: systemSchema, +}) diff --git a/server/routers/contentstack/schemas/blocks/cards/loyaltyCard.ts b/server/routers/contentstack/schemas/blocks/cards/loyaltyCard.ts new file mode 100644 index 000000000..584107d23 --- /dev/null +++ b/server/routers/contentstack/schemas/blocks/cards/loyaltyCard.ts @@ -0,0 +1,25 @@ +import { z } from "zod" + +import { tempImageVaultAssetSchema } from "../../imageVault" +import { systemSchema } from "../../system" +import { buttonSchema } from "../utils/buttonLinkSchema" +import { linkConnectionRefsSchema } from "../utils/linkConnection" + +import { CardsEnum } from "@/types/enums/cards" + +export const loyaltyCardBlockSchema = z.object({ + __typename: z.literal(CardsEnum.LoyaltyCard), + body_text: z.string().optional(), + heading: z.string().optional().default(""), + // JSON - ImageVault Image + image: tempImageVaultAssetSchema, + link: buttonSchema, + system: systemSchema, + title: z.string().optional(), +}) + +export const loyaltyCardBlockRefsSchema = z.object({ + __typename: z.literal(CardsEnum.LoyaltyCard), + link: linkConnectionRefsSchema, + system: systemSchema, +}) diff --git a/server/routers/contentstack/schemas/blocks/cards/teaserCard.ts b/server/routers/contentstack/schemas/blocks/cards/teaserCard.ts new file mode 100644 index 000000000..2519e9a2f --- /dev/null +++ b/server/routers/contentstack/schemas/blocks/cards/teaserCard.ts @@ -0,0 +1,121 @@ +import { z } from "zod" + +import { tempImageVaultAssetSchema } from "../../imageVault" +import { + accountPageSchema, + collectionPageSchema, + contentPageSchema, + destinationCityPageSchema, + destinationCountryPageSchema, + destinationOverviewPageSchema, + hotelPageSchema, + loyaltyPageSchema, + startPageSchema, + transformPageLink, +} from "../../pageLinks" +import { systemSchema } from "../../system" +import { imageSchema } from "../image" +import { imageContainerSchema } from "../imageContainer" +import { buttonSchema } from "../utils/buttonLinkSchema" +import { linkConnectionRefsSchema } from "../utils/linkConnection" + +import { CardsEnum } from "@/types/enums/cards" + +export const teaserCardBlockSchema = z.object({ + __typename: z.literal(CardsEnum.TeaserCard), + heading: z.string().default(""), + body_text: z.string().default(""), + image: tempImageVaultAssetSchema, + primary_button: buttonSchema, + secondary_button: buttonSchema, + has_primary_button: z.boolean().default(false), + has_secondary_button: z.boolean().default(false), + has_sidepeek_button: z.boolean().default(false), + sidepeek_button: z + .object({ + call_to_action_text: z.string().optional().default(""), + }) + .optional(), + sidepeek_content: z + .object({ + heading: z.string(), + content: z.object({ + json: z.any(), + embedded_itemsConnection: z.object({ + edges: z.array( + z.object({ + node: z + .discriminatedUnion("__typename", [ + imageContainerSchema, + imageSchema, + accountPageSchema, + collectionPageSchema, + contentPageSchema, + destinationCityPageSchema, + destinationCountryPageSchema, + destinationOverviewPageSchema, + hotelPageSchema, + loyaltyPageSchema, + startPageSchema, + ]) + .transform((data) => { + const link = transformPageLink(data) + if (link) { + return link + } + return data + }), + }) + ), + }), + }), + has_primary_button: z.boolean().default(false), + primary_button: buttonSchema, + has_secondary_button: z.boolean().default(false), + secondary_button: buttonSchema, + }) + .optional() + .transform((data) => { + if (!data) { + return + } + + return { + ...data, + primary_button: data.has_primary_button + ? data.primary_button + : undefined, + secondary_button: data.has_secondary_button + ? data.secondary_button + : undefined, + } + }), + system: systemSchema, +}) + +export function transformTeaserCardBlock( + card: typeof teaserCardBlockSchema._type +) { + return { + __typename: card.__typename, + body_text: card.body_text, + heading: card.heading, + primaryButton: card.has_primary_button ? card.primary_button : undefined, + secondaryButton: card.has_secondary_button + ? card.secondary_button + : undefined, + sidePeekButton: card.has_sidepeek_button ? card.sidepeek_button : undefined, + sidePeekContent: card.has_sidepeek_button + ? card.sidepeek_content + : undefined, + image: card.image, + system: card.system, + } +} + +export const teaserCardBlockRefsSchema = z.object({ + __typename: z.literal(CardsEnum.TeaserCard), + primary_button: linkConnectionRefsSchema, + secondary_button: linkConnectionRefsSchema, + system: systemSchema, +}) diff --git a/server/routers/contentstack/schemas/blocks/cardsGrid.ts b/server/routers/contentstack/schemas/blocks/cardsGrid.ts index be543d124..369b85f03 100644 --- a/server/routers/contentstack/schemas/blocks/cardsGrid.ts +++ b/server/routers/contentstack/schemas/blocks/cardsGrid.ts @@ -1,23 +1,23 @@ import { z } from "zod" import { tempImageVaultAssetSchema } from "../imageVault" -import { - accountPageSchema, - collectionPageSchema, - contentPageSchema, - destinationCityPageSchema, - destinationCountryPageSchema, - destinationOverviewPageSchema, - hotelPageSchema, - loyaltyPageSchema, - startPageSchema, - transformPageLink, -} from "../pageLinks" import { systemSchema } from "../system" +import { + infoCardBlockRefsSchema, + infoCardBlockSchema, + transformInfoCardBlock, +} from "./cards/infoCard" +import { + loyaltyCardBlockRefsSchema, + loyaltyCardBlockSchema, +} from "./cards/loyaltyCard" +import { + teaserCardBlockRefsSchema, + teaserCardBlockSchema, + transformTeaserCardBlock, +} from "./cards/teaserCard" import { buttonSchema } from "./utils/buttonLinkSchema" import { linkConnectionRefsSchema } from "./utils/linkConnection" -import { imageSchema } from "./image" -import { imageContainerSchema } from "./imageContainer" import { BlocksEnums } from "@/types/enums/blocks" import { CardsGridEnum, CardsGridLayoutEnum } from "@/types/enums/cardsGrid" @@ -54,137 +54,6 @@ export function transformCardBlock(card: typeof cardBlockSchema._type) { } } -export const teaserCardBlockSchema = z.object({ - __typename: z.literal(CardsGridEnum.cards.TeaserCard), - heading: z.string().default(""), - body_text: z.string().default(""), - image: tempImageVaultAssetSchema, - primary_button: buttonSchema, - secondary_button: buttonSchema, - has_primary_button: z.boolean().default(false), - has_secondary_button: z.boolean().default(false), - has_sidepeek_button: z.boolean().default(false), - sidepeek_button: z - .object({ - call_to_action_text: z.string().optional().default(""), - }) - .optional(), - sidepeek_content: z - .object({ - heading: z.string(), - content: z.object({ - json: z.any(), - embedded_itemsConnection: z.object({ - edges: z.array( - z.object({ - node: z - .discriminatedUnion("__typename", [ - imageContainerSchema, - imageSchema, - accountPageSchema, - collectionPageSchema, - contentPageSchema, - destinationCityPageSchema, - destinationCountryPageSchema, - destinationOverviewPageSchema, - hotelPageSchema, - loyaltyPageSchema, - startPageSchema, - ]) - .transform((data) => { - const link = transformPageLink(data) - if (link) { - return link - } - return data - }), - }) - ), - }), - }), - has_primary_button: z.boolean().default(false), - primary_button: buttonSchema, - has_secondary_button: z.boolean().default(false), - secondary_button: buttonSchema, - }) - .optional() - .transform((data) => { - if (!data) { - return undefined - } - - return { - ...data, - primary_button: data.has_primary_button - ? data.primary_button - : undefined, - secondary_button: data.has_secondary_button - ? data.secondary_button - : undefined, - } - }), - system: systemSchema, -}) - -export function transformTeaserCardBlock( - card: typeof teaserCardBlockSchema._type -) { - return { - __typename: card.__typename, - body_text: card.body_text, - heading: card.heading, - primaryButton: card.has_primary_button ? card.primary_button : undefined, - secondaryButton: card.has_secondary_button - ? card.secondary_button - : undefined, - sidePeekButton: card.has_sidepeek_button ? card.sidepeek_button : undefined, - sidePeekContent: card.has_sidepeek_button - ? card.sidepeek_content - : undefined, - image: card.image, - system: card.system, - } -} - -const loyaltyCardBlockSchema = z.object({ - __typename: z.literal(CardsGridEnum.cards.LoyaltyCard), - body_text: z.string().optional(), - heading: z.string().optional().default(""), - // JSON - ImageVault Image - image: tempImageVaultAssetSchema, - link: buttonSchema, - system: systemSchema, - 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) @@ -261,26 +130,6 @@ export function transformCardBlockRefs( return cards } -const loyaltyCardBlockRefsSchema = z.object({ - __typename: z.literal(CardsGridEnum.cards.LoyaltyCard), - link: linkConnectionRefsSchema, - system: systemSchema, -}) - -export const teaserCardBlockRefsSchema = z.object({ - __typename: z.literal(CardsGridEnum.cards.TeaserCard), - primary_button: linkConnectionRefsSchema, - secondary_button: linkConnectionRefsSchema, - 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({ diff --git a/server/routers/contentstack/schemas/blocks/carouselCards.ts b/server/routers/contentstack/schemas/blocks/carouselCards.ts new file mode 100644 index 000000000..00f34c952 --- /dev/null +++ b/server/routers/contentstack/schemas/blocks/carouselCards.ts @@ -0,0 +1,137 @@ +import { z } from "zod" + +import { + contentCardRefSchema, + contentCardSchema, + transformContentCard, +} from "./cards/contentCard" +import { buttonSchema } from "./utils/buttonLinkSchema" +import { linkConnectionRefsSchema } from "./utils/linkConnection" + +import { BlocksEnums } from "@/types/enums/blocks" +import { + type CarouselCardFilter, + CarouselCardFilterEnum, +} from "@/types/enums/carouselCards" + +const commonFields = { + heading: z.string().optional(), + link: buttonSchema.optional(), +} as const + +const carouselCardsWithFilters = z.object({ + ...commonFields, + enable_filters: z.literal(true), + card_groups: z.array( + z.object({ + filter_category: z.object({ + filter_identifier: z.nativeEnum(CarouselCardFilterEnum), + filter_label: z.string(), + }), + cardConnection: z.object({ + edges: z.array(z.object({ node: contentCardSchema })), + }), + }) + ), + default_filter: z.nativeEnum(CarouselCardFilterEnum), +}) + +const carouselCardsWithoutFilters = z.object({ + ...commonFields, + enable_filters: z.literal(false), + card_groups: z.array( + z.object({ + filter_category: z.object({ + filter_identifier: z.null(), + filter_label: z.string(), + }), + cardConnection: z.object({ + edges: z.array(z.object({ node: contentCardSchema })), + }), + }) + ), + default_filter: z.null(), +}) + +export const carouselCardsSchema = z.object({ + typename: z + .literal(BlocksEnums.block.CarouselCards) + .optional() + .default(BlocksEnums.block.CarouselCards), + carousel_cards: z + .discriminatedUnion("enable_filters", [ + carouselCardsWithFilters, + carouselCardsWithoutFilters, + ]) + .transform((data) => { + if (!data.enable_filters) { + return { + heading: data.heading, + enableFilters: false, + filterCategories: [], + cards: data.card_groups.flatMap((group) => + group.cardConnection.edges.map((edge) => + transformContentCard(edge.node) + ) + ), + defaultFilter: null, + link: data.link + ? { href: data.link.href, text: data.link.title } + : undefined, + } + } + + const filterCategories = data.card_groups.reduce< + Array<{ + identifier: CarouselCardFilter + label: string + }> + >((acc, group) => { + const identifier = group.filter_category.filter_identifier + if (!acc.some((category) => category.identifier === identifier)) { + acc.push({ + identifier, + label: group.filter_category.filter_label, + }) + } + return acc + }, []) + + return { + heading: data.heading, + enableFilters: true, + filterCategories, + cards: data.card_groups.flatMap((group) => + group.cardConnection.edges.map((edge) => ({ + ...transformContentCard(edge.node), + filterId: group.filter_category.filter_identifier, + })) + ), + defaultFilter: data.default_filter, + link: data.link + ? { href: data.link.href, text: data.link.title } + : undefined, + } + }), +}) + +export const carouselCardsRefsSchema = z.object({ + typename: z + .literal(BlocksEnums.block.CarouselCards) + .optional() + .default(BlocksEnums.block.CarouselCards), + carousel_cards: z.object({ + card_groups: z.array( + z.object({ + cardConnection: z.object({ + edges: z.array( + z.object({ + node: contentCardRefSchema, + }) + ), + }), + }) + ), + link: linkConnectionRefsSchema.optional(), + }), +}) diff --git a/server/routers/contentstack/schemas/sidebar/teaserCard.ts b/server/routers/contentstack/schemas/sidebar/teaserCard.ts index 94443cd75..c0cfde027 100644 --- a/server/routers/contentstack/schemas/sidebar/teaserCard.ts +++ b/server/routers/contentstack/schemas/sidebar/teaserCard.ts @@ -3,9 +3,9 @@ import { z } from "zod" import { teaserCardBlockRefsSchema, teaserCardBlockSchema, - transformCardBlockRefs, transformTeaserCardBlock, -} from "../blocks/cardsGrid" +} from "../blocks/cards/teaserCard" +import { transformCardBlockRefs } from "../blocks/cardsGrid" import { SidebarEnums } from "@/types/enums/sidebar" diff --git a/server/routers/contentstack/startPage/output.ts b/server/routers/contentstack/startPage/output.ts index 69c6dc02b..4ee6b6212 100644 --- a/server/routers/contentstack/startPage/output.ts +++ b/server/routers/contentstack/startPage/output.ts @@ -6,6 +6,10 @@ import { cardGridRefsSchema, cardsGridSchema, } from "../schemas/blocks/cardsGrid" +import { + carouselCardsRefsSchema, + carouselCardsSchema, +} from "../schemas/blocks/carouselCards" import { fullWidthCampaignBlockRefsSchema, fullWidthCampaignBlockSchema, @@ -21,6 +25,12 @@ const startPageCards = z }) .merge(cardsGridSchema) +const startPageCarouselCards = z + .object({ + __typename: z.literal(StartPageEnum.ContentStack.blocks.CarouselCards), + }) + .merge(carouselCardsSchema) + const startPageFullWidthCampaign = z .object({ __typename: z.literal(StartPageEnum.ContentStack.blocks.FullWidthCampaign), @@ -30,6 +40,7 @@ const startPageFullWidthCampaign = z export const blocksSchema = z.discriminatedUnion("__typename", [ startPageCards, startPageFullWidthCampaign, + startPageCarouselCards, ]) export const startPageSchema = z.object({ @@ -65,9 +76,16 @@ const startPageFullWidthCampaignRef = z }) .merge(fullWidthCampaignBlockRefsSchema) +const startPageCarouselCardsRef = z + .object({ + __typename: z.literal(StartPageEnum.ContentStack.blocks.CarouselCards), + }) + .merge(carouselCardsRefsSchema) + const startPageBlockRefsItem = z.discriminatedUnion("__typename", [ startPageCardsRefs, startPageFullWidthCampaignRef, + startPageCarouselCardsRef, ]) export const startPageRefsSchema = z.object({ diff --git a/types/components/blocks/carouselCards.ts b/types/components/blocks/carouselCards.ts new file mode 100644 index 000000000..934f38096 --- /dev/null +++ b/types/components/blocks/carouselCards.ts @@ -0,0 +1,4 @@ +import type { CarouselCards } from "@/types/trpc/routers/contentstack/blocks" + +export interface CarouselCardsProps + extends Pick {} diff --git a/types/enums/blocks.ts b/types/enums/blocks.ts index 4fd753abf..ad3532441 100644 --- a/types/enums/blocks.ts +++ b/types/enums/blocks.ts @@ -12,5 +12,6 @@ export namespace BlocksEnums { SasTierComparison = "SasTierComparison", HotelListing = "HotelListing", FullWidthCampaign = "FullWidthCampaign", + CarouselCards = "CarouselCards", } } diff --git a/types/enums/cards.ts b/types/enums/cards.ts new file mode 100644 index 000000000..5520208a2 --- /dev/null +++ b/types/enums/cards.ts @@ -0,0 +1,7 @@ +export const CardsEnum = { + Card: "Card", + TeaserCard: "TeaserCard", + LoyaltyCard: "LoyaltyCard", + InfoCard: "InfoCard", + ContentCard: "ContentCard", +} as const diff --git a/types/enums/cardsGrid.ts b/types/enums/cardsGrid.ts index b38a51e2f..2675d3488 100644 --- a/types/enums/cardsGrid.ts +++ b/types/enums/cardsGrid.ts @@ -1,10 +1,17 @@ +import { CardsEnum } from "./cards" + +/** + * Enums specific to the CardsGrid presentation context. + * CardsEnum defines the core card types, while CardsGridEnum defines + * which cards are supported in the grid layout. + */ export namespace CardsGridEnum { - export const enum cards { - Card = "Card", - LoyaltyCard = "LoyaltyCard", - TeaserCard = "TeaserCard", - InfoCard = "InfoCard", - } + export const cards = { + Card: CardsEnum.Card, + LoyaltyCard: CardsEnum.LoyaltyCard, + TeaserCard: CardsEnum.TeaserCard, + InfoCard: CardsEnum.InfoCard, + } as const satisfies Partial } export enum CardsGridLayoutEnum { diff --git a/types/enums/carouselCards.ts b/types/enums/carouselCards.ts new file mode 100644 index 000000000..23b123c87 --- /dev/null +++ b/types/enums/carouselCards.ts @@ -0,0 +1,9 @@ +export const CarouselCardFilterEnum = { + offers: "offers", + popular_hotels: "popular_hotels", + popular_cities: "popular_cities", + spa_wellness: "spa_wellness", +} as const + +export type CarouselCardFilter = + (typeof CarouselCardFilterEnum)[keyof typeof CarouselCardFilterEnum] diff --git a/types/enums/startPage.ts b/types/enums/startPage.ts index 339e5f26e..684cfd7de 100644 --- a/types/enums/startPage.ts +++ b/types/enums/startPage.ts @@ -2,6 +2,7 @@ export namespace StartPageEnum { export namespace ContentStack { export const enum blocks { CardsGrid = "StartPageBlocksCardsGrid", + CarouselCards = "StartPageBlocksCarouselCards", FullWidthCampaign = "StartPageBlocksFullWidthCampaign", } } diff --git a/types/trpc/routers/contentstack/blocks.ts b/types/trpc/routers/contentstack/blocks.ts index 0c8a84374..c7189c379 100644 --- a/types/trpc/routers/contentstack/blocks.ts +++ b/types/trpc/routers/contentstack/blocks.ts @@ -1,9 +1,8 @@ import type { z } from "zod" -import type { - cardsGridSchema, - teaserCardBlockSchema, -} from "@/server/routers/contentstack/schemas/blocks/cardsGrid" +import type { teaserCardBlockSchema } from "@/server/routers/contentstack/schemas/blocks/cards/teaserCard" +import type { cardsGridSchema } from "@/server/routers/contentstack/schemas/blocks/cardsGrid" +import type { carouselCardsSchema } from "@/server/routers/contentstack/schemas/blocks/carouselCards" import type { contentSchema } from "@/server/routers/contentstack/schemas/blocks/content" import type { dynamicContentSchema } from "@/server/routers/contentstack/schemas/blocks/dynamicContent" import type { hotelListingSchema } from "@/server/routers/contentstack/schemas/blocks/hotelListing" @@ -27,3 +26,4 @@ interface GetHotelListing extends z.output {} export type HotelListing = GetHotelListing["hotel_listing"] export interface SasTierComparison extends z.output {} +export interface CarouselCards extends z.output {}