From e88e4d92bf11fd5094d85fc49f9396f140049b87 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Mon, 2 Sep 2024 10:23:21 +0200 Subject: [PATCH] feat(SW-285): Ship support for ContentPageBlocksCardsGrid --- components/Content/Blocks/CardsGrid/index.tsx | 52 +++++++ components/Content/Blocks/index.tsx | 11 +- lib/graphql/Query/ContentPage.graphql | 36 +++++ .../contentstack/contentPage/output.ts | 146 +++++++++++++++++- .../routers/contentstack/contentPage/query.ts | 38 ++++- .../routers/contentstack/contentPage/utils.ts | 27 ++++ types/components/content/blocks.ts | 9 +- types/components/content/enums.ts | 6 + 8 files changed, 320 insertions(+), 5 deletions(-) create mode 100644 components/Content/Blocks/CardsGrid/index.tsx diff --git a/components/Content/Blocks/CardsGrid/index.tsx b/components/Content/Blocks/CardsGrid/index.tsx new file mode 100644 index 000000000..2eb4b9b4e --- /dev/null +++ b/components/Content/Blocks/CardsGrid/index.tsx @@ -0,0 +1,52 @@ +import SectionContainer from "@/components/Section/Container" +import SectionHeader from "@/components/Section/Header" +import Card from "@/components/TempDesignSystem/Card" +import Grids from "@/components/TempDesignSystem/Grids" +import LoyaltyCard from "@/components/TempDesignSystem/LoyaltyCard" + +import { CardsGridProps } from "@/types/components/content/blocks" +import { CardsGridEnum } from "@/types/components/content/enums" + +export default function CardsGrid({ + cards_grid, + firstItem = false, +}: CardsGridProps) { + return ( + + + + {cards_grid.cards.map((card) => { + switch (card.__typename) { + case CardsGridEnum.Card: { + return ( + + ) + } + case CardsGridEnum.LoyaltyCard: + return ( + + ) + } + })} + + + ) +} diff --git a/components/Content/Blocks/index.tsx b/components/Content/Blocks/index.tsx index 3638d7af6..9c396ed11 100644 --- a/components/Content/Blocks/index.tsx +++ b/components/Content/Blocks/index.tsx @@ -2,7 +2,8 @@ import JsonToHtml from "@/components/JsonToHtml" // import DynamicContentBlock from "@/components/Loyalty/Blocks/DynamicContent" import Shortcuts from "@/components/MyPages/Blocks/Shortcuts" -// import CardsGrid from "./CardsGrid" +import CardsGrid from "./CardsGrid" + import type { BlocksProps } from "@/types/components/content/blocks" import { ContentBlocksTypenameEnum } from "@/types/components/content/enums" @@ -29,6 +30,14 @@ export function Blocks({ blocks }: BlocksProps) { title={block.shortcuts.title} /> ) + case ContentBlocksTypenameEnum.ContentPageBlocksCardsGrid: + return ( + + ) default: return null } diff --git a/lib/graphql/Query/ContentPage.graphql b/lib/graphql/Query/ContentPage.graphql index 8202e89e1..6dedd3057 100644 --- a/lib/graphql/Query/ContentPage.graphql +++ b/lib/graphql/Query/ContentPage.graphql @@ -1,3 +1,8 @@ +#import "../Fragments/Blocks/Card.graphql" +#import "../Fragments/Blocks/LoyaltyCard.graphql" +#import "../Fragments/Blocks/Refs/Card.graphql" +#import "../Fragments/Blocks/Refs/LoyaltyCard.graphql" + #import "../Fragments/PageLink/AccountPageLink.graphql" #import "../Fragments/PageLink/ContentPageLink.graphql" #import "../Fragments/PageLink/LoyaltyPageLink.graphql" @@ -60,6 +65,24 @@ query GetContentPage($locale: String!, $uid: String!) { } } } + ... on ContentPageBlocksCardsGrid { + __typename + cards_grid { + title + preamble + layout + theme + cardConnection(limit: 10) { + edges { + node { + __typename + ...CardBlock + ...LoyaltyCardBlock + } + } + } + } + } } title header { @@ -128,6 +151,19 @@ query GetContentPageRefs($locale: String!, $uid: String!) { } } } + ... on ContentPageBlocksCardsGrid { + __typename + cards_grid { + cardConnection(limit: 10) { + edges { + node { + ...CardBlockRef + ...LoyaltyCardBlockRef + } + } + } + } + } } system { ...System diff --git a/server/routers/contentstack/contentPage/output.ts b/server/routers/contentstack/contentPage/output.ts index b99a873ff..c15d18555 100644 --- a/server/routers/contentstack/contentPage/output.ts +++ b/server/routers/contentstack/contentPage/output.ts @@ -4,9 +4,13 @@ import { Lang } from "@/constants/languages" import { imageVaultAssetSchema } from "../schemas/imageVault" -import { ContentBlocksTypenameEnum } from "@/types/components/content/enums" +import { + CardsGridEnum, + ContentBlocksTypenameEnum, +} from "@/types/components/content/enums" import { ImageVaultAsset } from "@/types/components/imageVault" import { Embeds } from "@/types/requests/embeds" +import { PageLinkEnum } from "@/types/requests/pageLinks" import { RTEEmbedsEnum } from "@/types/requests/rte" import { EdgesWithTotalCount } from "@/types/requests/utils/edges" import { RTEDocument } from "@/types/rte/node" @@ -41,9 +45,74 @@ const contentPageShortcuts = z.object({ }), }) +// TODO: this is a separate entity, should be in a separate file. +const cardBlock = z.object({ + __typename: z.literal(CardsGridEnum.Card), + heading: z.string().nullable(), + body_text: z.string().nullable(), + background_image: z.any(), + scripted_top_title: z.string().nullable(), + primaryButton: z + .object({ + openInNewTab: z.boolean(), + title: z.string(), + href: z.string(), + isExternal: z.boolean(), + }) + .optional(), + secondaryButton: z + .object({ + openInNewTab: z.boolean(), + title: z.string(), + href: z.string(), + isExternal: z.boolean(), + }) + .optional(), + system: z.object({ + locale: z.nativeEnum(Lang), + uid: z.string(), + }), +}) + +const loyaltyCardBlock = z.object({ + __typename: z.literal(CardsGridEnum.LoyaltyCard), + heading: z.string().nullable(), + body_text: z.string().nullable(), + image: z.any(), + link: z + .object({ + openInNewTab: z.boolean(), + title: z.string(), + href: z.string(), + isExternal: z.boolean(), + }) + .optional(), + system: z.object({ + locale: z.nativeEnum(Lang), + uid: z.string(), + }), +}) + +const contentPageCardsItems = z.discriminatedUnion("__typename", [ + loyaltyCardBlock, + cardBlock, +]) + +const contentPageCards = z.object({ + __typename: z.literal(ContentBlocksTypenameEnum.ContentPageBlocksCardsGrid), + cards_grid: z.object({ + title: z.string().nullable(), + preamble: z.string().nullable(), + layout: z.enum(["twoColumnGrid", "threeColumnGrid", "twoPlusOne"]), + theme: z.enum(["one", "two", "three"]).nullable(), + cards: z.array(contentPageCardsItems), + }), +}) + const contentPageBlockItem = z.discriminatedUnion("__typename", [ contentPageBlockTextContent, contentPageShortcuts, + contentPageCards, ]) type BlockContentRaw = z.infer @@ -57,7 +126,22 @@ export interface RteBlockContent extends BlockContentRaw { } export type Shortcuts = z.infer -export type Block = RteBlockContent | Shortcuts + +type LoyaltyCardRaw = z.infer +type LoyaltyCard = Omit & { + image?: ImageVaultAsset +} +type CardRaw = z.infer +type Card = Omit & { + backgroundImage?: ImageVaultAsset +} +type CardsGridRaw = z.infer +export type CardsGrid = Omit & { + cards: (LoyaltyCard | Card)[] +} +export type CardsRaw = CardsGrid["cards_grid"]["cards"][number] + +export type Block = RteBlockContent | Shortcuts | CardsGrid // Content Page Schema and types export const validateContentPageSchema = z.object({ @@ -86,6 +170,20 @@ export type ContentPage = Omit & { blocks: Block[] } +const pageConnectionRefs = z.object({ + edges: z.array( + z.object({ + node: z.object({ + __typename: z.nativeEnum(PageLinkEnum), + system: z.object({ + content_type_uid: z.string(), + uid: z.string(), + }), + }), + }) + ), +}) + const rteConnectionRefs = z.object({ edges: z.array( z.object({ @@ -100,6 +198,42 @@ const rteConnectionRefs = z.object({ ), }) +const cardBlockRefs = z.object({ + __typename: z.literal(CardsGridEnum.Card), + primary_button: z + .object({ + linkConnection: pageConnectionRefs, + }) + .nullable(), + secondary_button: z + .object({ + linkConnection: pageConnectionRefs, + }) + .nullable(), + system: z.object({ + content_type_uid: z.string(), + uid: z.string(), + }), +}) + +const loyaltyCardBlockRefs = z.object({ + __typename: z.literal(CardsGridEnum.LoyaltyCard), + link: z + .object({ + linkConnection: pageConnectionRefs, + }) + .nullable(), + system: z.object({ + content_type_uid: z.string(), + uid: z.string(), + }), +}) + +const cardGridCardsRef = z.discriminatedUnion("__typename", [ + loyaltyCardBlockRefs, + cardBlockRefs, +]) + const contentPageBlockTextContentRefs = z.object({ __typename: z.literal(ContentBlocksTypenameEnum.ContentPageBlocksContent), content: z.object({ @@ -109,6 +243,13 @@ const contentPageBlockTextContentRefs = z.object({ }), }) +const contentPageCardsRefs = z.object({ + __typename: z.literal(ContentBlocksTypenameEnum.ContentPageBlocksCardsGrid), + cards_grid: z.object({ + cardConnection: cardGridCardsRef, + }), +}) + const contentPageShortcutsRefs = z.object({ __typename: z.literal(ContentBlocksTypenameEnum.ContentPageBlocksShortcuts), shortcuts: z.object({ @@ -123,6 +264,7 @@ const contentPageShortcutsRefs = z.object({ const contentPageBlockRefsItem = z.discriminatedUnion("__typename", [ contentPageBlockTextContentRefs, contentPageShortcutsRefs, + contentPageCardsRefs, ]) export const validateContentPageRefsSchema = z.object({ diff --git a/server/routers/contentstack/contentPage/query.ts b/server/routers/contentstack/contentPage/query.ts index b473bb16c..7611ea410 100644 --- a/server/routers/contentstack/contentPage/query.ts +++ b/server/routers/contentstack/contentPage/query.ts @@ -18,10 +18,14 @@ import { fetchContentPageRefs, generatePageTags, getContentPageCounter, + makeButtonObject, validateContentPageRefs, } from "./utils" -import { ContentBlocksTypenameEnum } from "@/types/components/content/enums" +import { + CardsGridEnum, + ContentBlocksTypenameEnum, +} from "@/types/components/content/enums" import { TrackingChannelEnum, TrackingSDKPageData, @@ -85,6 +89,38 @@ export const contentPageQueryRouter = router({ })), }, } + case ContentBlocksTypenameEnum.ContentPageBlocksCardsGrid: + return { + ...block, + cards_grid: { + ...block.cards_grid, + cards: block.cards_grid.cardConnection.edges.map( + ({ node: card }: { node: any }) => { + switch (card.__typename) { + case CardsGridEnum.Card: + return { + ...card, + backgroundImage: makeImageVaultImage( + card.background_image + ), + primaryButton: card.has_primary_button + ? makeButtonObject(card.primary_button) + : undefined, + secondaryButton: card.has_secondary_button + ? makeButtonObject(card.secondary_button) + : undefined, + } + case CardsGridEnum.LoyaltyCard: + return { + ...card, + image: makeImageVaultImage(card.image), + link: makeButtonObject(card.link), + } + } + } + ), + }, + } default: return block } diff --git a/server/routers/contentstack/contentPage/utils.ts b/server/routers/contentstack/contentPage/utils.ts index 50c49e5e2..3f2689fc0 100644 --- a/server/routers/contentstack/contentPage/utils.ts +++ b/server/routers/contentstack/contentPage/utils.ts @@ -6,6 +6,7 @@ import { request } from "@/lib/graphql/request" import { notFound } from "@/server/errors/trpc" import { generateTag, generateTags } from "@/utils/generateTag" +import { removeMultipleSlashes } from "@/utils/url" import { removeEmptyObjects } from "../../utils" import { ContentPageRefsDataRaw, validateContentPageRefsSchema } from "./output" @@ -130,3 +131,29 @@ export function getConnections(refs: ContentPageRefsDataRaw) { } return connections } + +export function makeButtonObject(button: any) { + if (!button) return null + + const isContenstackLink = + button?.is_contentstack_link || button.linkConnection?.edges?.length + const linkConnnectionNode = isContenstackLink + ? button.linkConnection.edges[0]?.node + : null + + return { + openInNewTab: button?.open_in_new_tab, + title: + button.cta_text || + (linkConnnectionNode + ? linkConnnectionNode.title + : button.external_link.title), + href: linkConnnectionNode + ? linkConnnectionNode.web?.original_url || + removeMultipleSlashes( + `/${linkConnnectionNode.system.locale}/${linkConnnectionNode.url}` + ) + : button.external_link.href, + isExternal: !isContenstackLink, + } +} diff --git a/types/components/content/blocks.ts b/types/components/content/blocks.ts index af4815f69..702a086d4 100644 --- a/types/components/content/blocks.ts +++ b/types/components/content/blocks.ts @@ -1,5 +1,12 @@ -import { Block } from "@/server/routers/contentstack/contentPage/output" +import { + Block, + CardsGrid, +} from "@/server/routers/contentstack/contentPage/output" export type BlocksProps = { blocks: Block[] } + +export type CardsGridProps = Pick & { + firstItem?: boolean +} diff --git a/types/components/content/enums.ts b/types/components/content/enums.ts index 77fad4d68..a629a32c6 100644 --- a/types/components/content/enums.ts +++ b/types/components/content/enums.ts @@ -1,4 +1,10 @@ export enum ContentBlocksTypenameEnum { ContentPageBlocksContent = "ContentPageBlocksContent", ContentPageBlocksShortcuts = "ContentPageBlocksShortcuts", + ContentPageBlocksCardsGrid = "ContentPageBlocksCardsGrid", +} + +export enum CardsGridEnum { + LoyaltyCard = "LoyaltyCard", + Card = "Card", }