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 = (
+
+
+
+ {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