diff --git a/components/Blocks/CarouselCards.tsx b/components/Blocks/CarouselCards.tsx index a7dd207f9..a02a9c555 100644 --- a/components/Blocks/CarouselCards.tsx +++ b/components/Blocks/CarouselCards.tsx @@ -1,6 +1,9 @@ +import ContentCard from "@/components/ContentCard" import SectionContainer from "@/components/Section/Container" import SectionHeader from "@/components/Section/Header" +import styles from "./carouselCards.module.css" + import type { CarouselCardsProps } from "@/types/components/blocks/carouselCards" export default function CarouselCards({ carousel_cards }: CarouselCardsProps) { @@ -19,17 +22,27 @@ export default function CarouselCards({ carousel_cards }: CarouselCardsProps) { {enableFilters && (
Filter data -

Todo: Add filter component here

-
-            {JSON.stringify({ filterCategories, defaultFilter }, null, 2)}
-          
+
+ {/* Filter component will go here */} +
+              {JSON.stringify({ filterCategories, defaultFilter }, null, 2)}
+            
+
)} -
- Carousel cards -

Todo: Add carousel cards component here

-
{JSON.stringify({ cards }, null, 2)}
-
+ {/* Carousel functionality will go here */} +
+ {cards.map((card) => ( + + ))} +
) } diff --git a/components/Blocks/carouselCards.module.css b/components/Blocks/carouselCards.module.css new file mode 100644 index 000000000..38fcc7653 --- /dev/null +++ b/components/Blocks/carouselCards.module.css @@ -0,0 +1,36 @@ +.code { + padding: var(--Spacing-x2); + background: var(--Base-Surface-Secondary-light-Normal); +} + +/* + Mock styles for the carousel cards. Will be removed/replaced + when the carousel functionality is implemented (SW-1542). +*/ +.cardsContainer { + display: grid; + gap: var(--Spacing-x3); + grid-auto-flow: column; + grid-auto-columns: 100%; + overflow-x: auto; + overscroll-behavior-x: contain; + scroll-snap-type: x mandatory; +} + +.cardsContainer > * { + scroll-snap-align: start; +} + +/* Show 2 cards on tablet */ +@media (min-width: 768px) { + .cardsContainer { + grid-auto-columns: calc((100% - var(--Spacing-x3)) / 2); + } +} + +/* Show 3 cards on desktop */ +@media (min-width: 1024px) { + .cardsContainer { + grid-auto-columns: calc((100% - var(--Spacing-x3) * 2) / 3); + } +} diff --git a/components/ContentCard/contentCard.module.css b/components/ContentCard/contentCard.module.css new file mode 100644 index 000000000..0a82585af --- /dev/null +++ b/components/ContentCard/contentCard.module.css @@ -0,0 +1,60 @@ +.card { + display: grid; +} + +.imageContainer { + position: relative; + aspect-ratio: 16/9; + border-radius: var(--Corner-radius-Medium); + overflow: hidden; + transition: border-radius 0.3s ease-in-out; +} + +.image { + border-radius: var(--Corner-radius-Medium); + transition: + transform 0.3s ease-in-out, + border-radius 0.3s ease-in-out; +} + +.card:hover, +.card:hover .imageContainer, +.card:hover .image { + border-radius: var(--Corner-radius-Large); +} + +.card:hover .image { + transform: scale(1.05); +} + +.promoTag { + position: absolute; + top: 14px; + left: 14px; + text-transform: uppercase; +} + +.content { + display: flex; + padding: var(--Spacing-x-one-and-half); + flex-direction: column; + align-items: flex-start; + gap: var(--Spacing-x-one-and-half); + align-self: stretch; +} + +@media (min-width: 768px) { + .card { + max-width: 413px; + } + + .content { + padding: var(--Spacing-x2) var(--Spacing-x2) var(--Spacing-x2) 0; + } +} + +@media (max-width: 767px) { + .card { + min-width: 300px; + } +} diff --git a/components/ContentCard/contentCard.ts b/components/ContentCard/contentCard.ts new file mode 100644 index 000000000..b18ebf0e4 --- /dev/null +++ b/components/ContentCard/contentCard.ts @@ -0,0 +1,21 @@ +import type { ImageVaultAsset } from "@/types/components/imageVault" + +export interface ContentCardProps { + link?: { + href: string + openInNewTab?: boolean + isExternal?: boolean + } + heading: string + image: ImageVaultAsset + bodyText: string + promoText?: string + className?: string +} + +export interface ContentCardLinkProps { + href: string + openInNewTab?: boolean + isExternal?: boolean + children: React.ReactNode +} diff --git a/components/ContentCard/index.tsx b/components/ContentCard/index.tsx new file mode 100644 index 000000000..727b1afc4 --- /dev/null +++ b/components/ContentCard/index.tsx @@ -0,0 +1,70 @@ +import Image from "@/components/Image" +import Chip from "@/components/TempDesignSystem/Chip" +import Link from "@/components/TempDesignSystem/Link" +import Body from "@/components/TempDesignSystem/Text/Body" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" + +import styles from "./contentCard.module.css" + +import type { ContentCardLinkProps,ContentCardProps } from "./contentCard" + +export default function ContentCard({ + heading, + image, + bodyText, + promoText, + className = "", + link, +}: ContentCardProps) { + const card = ( +
+
+ {image.meta.alt + {promoText ? ( + {promoText} + ) : null} +
+
+ {heading} + {bodyText} +
+
+ ) + + if (!link) return card + + return ( + + {card} + + ) +} + +function ContentCardLink({ + children, + href, + openInNewTab, + isExternal, +}: ContentCardLinkProps) { + const Component = isExternal ? "a" : Link + const linkProps = { + href, + ...(openInNewTab && { + target: "_blank", + rel: "noopener noreferrer", + }), + } + + return {children} +} diff --git a/components/ContentType/StartPage/startPage.module.css b/components/ContentType/StartPage/startPage.module.css index d69fef887..2c9b1f6b3 100644 --- a/components/ContentType/StartPage/startPage.module.css +++ b/components/ContentType/StartPage/startPage.module.css @@ -42,6 +42,7 @@ display: grid; gap: var(--Spacing-x6); padding: calc(var(--Spacing-x5) * 2) 0 calc(var(--Spacing-x5) * 4); + background-color: var(--Base-Surface-Primary-light-Normal); } @media screen and (min-width: 768px) { diff --git a/lib/graphql/Fragments/Blocks/ContentCard.graphql b/lib/graphql/Fragments/Blocks/ContentCard.graphql index 34a1c5241..3601f6b96 100644 --- a/lib/graphql/Fragments/Blocks/ContentCard.graphql +++ b/lib/graphql/Fragments/Blocks/ContentCard.graphql @@ -25,36 +25,9 @@ fragment ContentCardBlock on ContentCard { heading image body_text - has_primary_button - has_secondary_button + has_card_link 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 + card_link { is_contentstack_link open_in_new_tab external_link { @@ -85,25 +58,7 @@ fragment ContentCardBlock on ContentCard { fragment ContentCardBlockRef on ContentCard { __typename - primary_button { - linkConnection { - edges { - node { - __typename - ...AccountPageRef - ...CollectionPageRef - ...ContentPageRef - ...DestinationCityPageRef - ...DestinationCountryPageRef - ...DestinationOverviewPageRef - ...HotelPageRef - ...LoyaltyPageRef - ...StartPageRef - } - } - } - } - secondary_button { + card_link { linkConnection { edges { node { diff --git a/lib/graphql/Fragments/Blocks/Refs/ContentCard.graphql b/lib/graphql/Fragments/Blocks/Refs/ContentCard.graphql deleted file mode 100644 index 2d6441ab0..000000000 --- a/lib/graphql/Fragments/Blocks/Refs/ContentCard.graphql +++ /dev/null @@ -1,52 +0,0 @@ -#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/server/routers/contentstack/schemas/blocks/cards/contentCard.ts b/server/routers/contentstack/schemas/blocks/cards/contentCard.ts index 2d1f5ba8b..0a244998e 100644 --- a/server/routers/contentstack/schemas/blocks/cards/contentCard.ts +++ b/server/routers/contentstack/schemas/blocks/cards/contentCard.ts @@ -14,21 +14,21 @@ export const contentCardSchema = z.object({ 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, + has_card_link: z.boolean(), + card_link: buttonSchema, system: systemSchema, }) export const contentCardRefSchema = z.object({ __typename: z.literal(CardsEnum.ContentCard), - primary_button: linkConnectionRefsSchema, - secondary_button: linkConnectionRefsSchema, + card_link: linkConnectionRefsSchema, system: systemSchema, }) export function transformContentCard(card: typeof contentCardSchema._type) { + // Return null if image or image URL is missing + if (!card.image?.url) return null + return { __typename: card.__typename, title: card.title, @@ -36,9 +36,12 @@ export function transformContentCard(card: typeof contentCardSchema._type) { 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 + link: card.has_card_link + ? { + href: card.card_link.href, + openInNewTab: card.card_link.openInNewTab, + isExternal: card.card_link.isExternal, + } : undefined, } } diff --git a/server/routers/contentstack/schemas/blocks/carouselCards.ts b/server/routers/contentstack/schemas/blocks/carouselCards.ts index 00f34c952..4f4205441 100644 --- a/server/routers/contentstack/schemas/blocks/carouselCards.ts +++ b/server/routers/contentstack/schemas/blocks/carouselCards.ts @@ -69,11 +69,13 @@ export const carouselCardsSchema = z.object({ heading: data.heading, enableFilters: false, filterCategories: [], - cards: data.card_groups.flatMap((group) => - group.cardConnection.edges.map((edge) => - transformContentCard(edge.node) + cards: data.card_groups + .flatMap((group) => + group.cardConnection.edges.map((edge) => + transformContentCard(edge.node) + ) ) - ), + .filter((card): card is NonNullable => card !== null), defaultFilter: null, link: data.link ? { href: data.link.href, text: data.link.title } @@ -102,10 +104,13 @@ export const carouselCardsSchema = z.object({ enableFilters: true, filterCategories, cards: data.card_groups.flatMap((group) => - group.cardConnection.edges.map((edge) => ({ - ...transformContentCard(edge.node), - filterId: group.filter_category.filter_identifier, - })) + group.cardConnection.edges + .map((edge) => transformContentCard(edge.node)) + .filter((card): card is NonNullable => card !== null) + .map((card) => ({ + ...card, + filterId: group.filter_category.filter_identifier, + })) ), defaultFilter: data.default_filter, link: data.link