From 2781a41110cb1fc61a041e62d724a34c7f0dd9f1 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Tue, 18 Feb 2025 14:42:36 +0000 Subject: [PATCH] Merged in feat/SW-1443-card-gallery-destination-overview (pull request #1362) feat(SW-1443): added cardGallery block to destination overview page instead of carousel functionality * feat(SW-1443): added cardGallery block to destination overview page instead of carousel functionality Approved-by: Fredrik Thorsson --- .../Blocks/CardGallery/cardGallery.module.css | 25 +++++ components/Blocks/CardGallery/index.tsx | 67 +++++++++++++ components/Blocks/CarouselCards/index.tsx | 5 +- components/Blocks/index.tsx | 8 ++ .../Filters => TabFilters}/index.tsx | 20 ++-- .../tabFilters.module.css} | 0 .../Fragments/Blocks/CardGallery.graphql | 92 ++++++++++++++++++ .../Fragments/Blocks/CarouselCards.graphql | 73 -------------- .../DestinationOverviewPage.graphql | 6 +- .../destinationOverviewPage/output.ts | 22 ++--- .../schemas/blocks/cardGallery.ts | 95 +++++++++++++++++++ types/components/blocks/cardGallery.ts | 3 + types/enums/blocks.ts | 9 +- types/enums/cardGallery.ts | 10 ++ types/enums/destinationOverviewPage.ts | 2 +- types/trpc/routers/contentstack/blocks.ts | 2 + 16 files changed, 337 insertions(+), 102 deletions(-) create mode 100644 components/Blocks/CardGallery/cardGallery.module.css create mode 100644 components/Blocks/CardGallery/index.tsx rename components/{Blocks/CarouselCards/Filters => TabFilters}/index.tsx (59%) rename components/{Blocks/CarouselCards/Filters/filters.module.css => TabFilters/tabFilters.module.css} (100%) create mode 100644 lib/graphql/Fragments/Blocks/CardGallery.graphql create mode 100644 server/routers/contentstack/schemas/blocks/cardGallery.ts create mode 100644 types/components/blocks/cardGallery.ts create mode 100644 types/enums/cardGallery.ts diff --git a/components/Blocks/CardGallery/cardGallery.module.css b/components/Blocks/CardGallery/cardGallery.module.css new file mode 100644 index 000000000..950abadbd --- /dev/null +++ b/components/Blocks/CardGallery/cardGallery.module.css @@ -0,0 +1,25 @@ +.cardsList { + list-style: none; + display: none; + gap: var(--Spacing-x4) var(--Spacing-x1); +} + +.navigationButton { + top: 30%; +} + +@media screen and (min-width: 768px) { + .carousel { + display: none; + } + .cardsList { + display: grid; + grid-template-columns: repeat(2, 1fr); + } +} + +@media screen and (min-width: 1024px) { + .cardsList { + grid-template-columns: repeat(3, 1fr); + } +} diff --git a/components/Blocks/CardGallery/index.tsx b/components/Blocks/CardGallery/index.tsx new file mode 100644 index 000000000..0719ff200 --- /dev/null +++ b/components/Blocks/CardGallery/index.tsx @@ -0,0 +1,67 @@ +"use client" + +import { useState } from "react" + +import { Carousel } from "@/components/Carousel" +import ContentCard from "@/components/ContentCard" +import SectionContainer from "@/components/Section/Container" +import SectionHeader from "@/components/Section/Header" +import SectionLink from "@/components/Section/Link" +import TabFilters from "@/components/TabFilters" + +import styles from "./cardGallery.module.css" + +import type { CardGalleryProps } from "@/types/components/blocks/cardGallery" + +export default function CardGallery({ card_gallery }: CardGalleryProps) { + const { heading, defaultFilter, filterCategories, cards, link } = card_gallery + + const [activeFilter, setActiveFilter] = useState(defaultFilter) + + const filteredCards = cards.filter((card) => card.filterId === activeFilter) + + return ( + + + {filterCategories.length > 0 && activeFilter && ( + + )} +
    + {filteredCards.map((card, index) => ( +
  • + +
  • + ))} +
+ + + {filteredCards.map((card, index) => ( + + + + ))} + + + + + + +
+ ) +} diff --git a/components/Blocks/CarouselCards/index.tsx b/components/Blocks/CarouselCards/index.tsx index 2e1e8456b..51eb9de3d 100644 --- a/components/Blocks/CarouselCards/index.tsx +++ b/components/Blocks/CarouselCards/index.tsx @@ -7,8 +7,7 @@ import ContentCard from "@/components/ContentCard" import SectionContainer from "@/components/Section/Container" import SectionHeader from "@/components/Section/Header" import SectionLink from "@/components/Section/Link" - -import Filters from "./Filters" +import TabFilters from "@/components/TabFilters" import styles from "./carouselCards.module.css" @@ -43,7 +42,7 @@ export default function CarouselCards({ carousel_cards }: CarouselCardsProps) { link={link} /> {filterCategories.length > 0 && activeFilter && ( - ) + case BlocksEnums.block.CardGallery: + return ( + + ) case BlocksEnums.block.HotelListing: const { heading, contentType, locationFilter, hotelsToInclude } = block.hotel_listing diff --git a/components/Blocks/CarouselCards/Filters/index.tsx b/components/TabFilters/index.tsx similarity index 59% rename from components/Blocks/CarouselCards/Filters/index.tsx rename to components/TabFilters/index.tsx index 42b8e0cc1..ae3b6fe68 100644 --- a/components/Blocks/CarouselCards/Filters/index.tsx +++ b/components/TabFilters/index.tsx @@ -1,20 +1,26 @@ "use client" -import styles from "./filters.module.css" +import styles from "./tabFilters.module.css" +import type { CardGalleryFilter } from "@/types/enums/cardGallery" import type { CarouselCardFilter } from "@/types/enums/carouselCards" -interface FiltersProps { - categories: Array<{ identifier: CarouselCardFilter; label: string }> - selectedFilter: CarouselCardFilter - onFilterSelect: (filter: CarouselCardFilter) => void +interface Filter { + identifier: CarouselCardFilter | CardGalleryFilter + label: string } -export default function Filters({ +interface TabFiltersProps { + categories: Array + selectedFilter: Filter["identifier"] + onFilterSelect: (filter: Filter["identifier"]) => void +} + +export default function TabFilters({ categories, selectedFilter, onFilterSelect, -}: FiltersProps) { +}: TabFiltersProps) { return (
{categories.map((category) => ( diff --git a/components/Blocks/CarouselCards/Filters/filters.module.css b/components/TabFilters/tabFilters.module.css similarity index 100% rename from components/Blocks/CarouselCards/Filters/filters.module.css rename to components/TabFilters/tabFilters.module.css diff --git a/lib/graphql/Fragments/Blocks/CardGallery.graphql b/lib/graphql/Fragments/Blocks/CardGallery.graphql new file mode 100644 index 000000000..6540695c5 --- /dev/null +++ b/lib/graphql/Fragments/Blocks/CardGallery.graphql @@ -0,0 +1,92 @@ +#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 CardGallery_DestinationOverviewPage on DestinationOverviewPageBlocksCardGallery { + card_gallery { + heading + card_groups { + 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 CardGallery_DestinationOverviewPageRefs on DestinationOverviewPageBlocksCardGallery { + card_gallery { + 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/CarouselCards.graphql b/lib/graphql/Fragments/Blocks/CarouselCards.graphql index f2ee33806..0030653bd 100644 --- a/lib/graphql/Fragments/Blocks/CarouselCards.graphql +++ b/lib/graphql/Fragments/Blocks/CarouselCards.graphql @@ -91,76 +91,3 @@ fragment CarouselCards_StartPageRefs on StartPageBlocksCarouselCards { } } } - -fragment CarouselCards_DestinationOverviewPage on DestinationOverviewPageBlocksCarouselCards { - carousel_cards { - heading - enable_filters - card_groups { - 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_DestinationOverviewPageRefs on DestinationOverviewPageBlocksCarouselCards { - 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/Query/DestinationOverviewPage/DestinationOverviewPage.graphql b/lib/graphql/Query/DestinationOverviewPage/DestinationOverviewPage.graphql index f81b58f1d..8d55e2ebb 100644 --- a/lib/graphql/Query/DestinationOverviewPage/DestinationOverviewPage.graphql +++ b/lib/graphql/Query/DestinationOverviewPage/DestinationOverviewPage.graphql @@ -1,6 +1,6 @@ #import "../../Fragments/System.graphql" -#import "../../Fragments/Blocks/CarouselCards.graphql" +#import "../../Fragments/Blocks/CardGallery.graphql" query GetDestinationOverviewPage($locale: String!, $uid: String!) { destination_overview_page(uid: $uid, locale: $locale) { @@ -8,7 +8,7 @@ query GetDestinationOverviewPage($locale: String!, $uid: String!) { url blocks { __typename - ...CarouselCards_DestinationOverviewPage + ...CardGallery_DestinationOverviewPage } system { ...System @@ -25,7 +25,7 @@ query GetDestinationOverviewPageRefs($locale: String!, $uid: String!) { destination_overview_page(locale: $locale, uid: $uid) { blocks { __typename - ...CarouselCards_DestinationOverviewPageRefs + ...CardGallery_DestinationOverviewPageRefs } system { ...System diff --git a/server/routers/contentstack/destinationOverviewPage/output.ts b/server/routers/contentstack/destinationOverviewPage/output.ts index 8d4c6e9a4..7defa4017 100644 --- a/server/routers/contentstack/destinationOverviewPage/output.ts +++ b/server/routers/contentstack/destinationOverviewPage/output.ts @@ -5,23 +5,23 @@ import { discriminatedUnionArray } from "@/lib/discriminatedUnion" import { removeMultipleSlashes } from "@/utils/url" import { - carouselCardsRefsSchema, - carouselCardsSchema, -} from "../schemas/blocks/carouselCards" + cardGalleryRefsSchema, + cardGallerySchema, +} from "../schemas/blocks/cardGallery" import { systemSchema } from "../schemas/system" import { DestinationOverviewPageEnum } from "@/types/enums/destinationOverviewPage" -const destinationOverviewPageCarouselCards = z +const destinationOverviewPageCardGallery = z .object({ __typename: z.literal( - DestinationOverviewPageEnum.ContentStack.blocks.CarouselCards + DestinationOverviewPageEnum.ContentStack.blocks.CardGallery ), }) - .merge(carouselCardsSchema) + .merge(cardGallerySchema) export const blocksSchema = z.discriminatedUnion("__typename", [ - destinationOverviewPageCarouselCards, + destinationOverviewPageCardGallery, ]) export const destinationOverviewPageSchema = z.object({ @@ -62,16 +62,16 @@ export const countryPageUrlSchema = z ) /** REFS */ -const destinationOverviewPageCarouselCardsRef = z +const destinationOverviewPageCardGalleryRef = z .object({ __typename: z.literal( - DestinationOverviewPageEnum.ContentStack.blocks.CarouselCards + DestinationOverviewPageEnum.ContentStack.blocks.CardGallery ), }) - .merge(carouselCardsRefsSchema) + .merge(cardGalleryRefsSchema) const blocksRefsSchema = z.discriminatedUnion("__typename", [ - destinationOverviewPageCarouselCardsRef, + destinationOverviewPageCardGalleryRef, ]) export const destinationOverviewPageRefsSchema = z.object({ diff --git a/server/routers/contentstack/schemas/blocks/cardGallery.ts b/server/routers/contentstack/schemas/blocks/cardGallery.ts new file mode 100644 index 000000000..469c7b47b --- /dev/null +++ b/server/routers/contentstack/schemas/blocks/cardGallery.ts @@ -0,0 +1,95 @@ +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 CardGalleryFilter, + CardGalleryFilterEnum, +} from "@/types/enums/cardGallery" + +export const cardGallerySchema = z.object({ + typename: z + .literal(BlocksEnums.block.CardGallery) + .optional() + .default(BlocksEnums.block.CardGallery), + card_gallery: z + .object({ + heading: z.string().optional(), + link: buttonSchema.optional(), + card_groups: z.array( + z.object({ + filter_identifier: z.nativeEnum(CardGalleryFilterEnum), + filter_label: z.string(), + cardConnection: z.object({ + edges: z.array(z.object({ node: contentCardSchema })), + }), + }) + ), + }) + .transform((data) => { + const filterCategories = data.card_groups.reduce< + Array<{ + identifier: CardGalleryFilter + label: string + }> + >((acc, group) => { + const identifier = group.filter_identifier + if (!acc.some((category) => category.identifier === identifier)) { + acc.push({ + identifier, + label: group.filter_label, + }) + } + return acc + }, []) + + return { + heading: data.heading, + filterCategories, + cards: data.card_groups.flatMap((group) => + group.cardConnection.edges + .map((edge) => transformContentCard(edge.node)) + .filter((card): card is NonNullable => card !== null) + .map((card) => ({ + ...card, + filterId: group.filter_identifier, + })) + ), + defaultFilter: + data.card_groups[0]?.filter_identifier ?? + filterCategories[0]?.identifier, + link: + data.link?.href && data.link.title + ? { href: data.link.href, text: data.link.title } + : undefined, + } + }), +}) + +export const cardGalleryRefsSchema = z.object({ + typename: z + .literal(BlocksEnums.block.CardGallery) + .optional() + .default(BlocksEnums.block.CardGallery), + card_gallery: z.object({ + card_groups: z.array( + z.object({ + cardConnection: z.object({ + edges: z.array( + z.object({ + node: contentCardRefSchema, + }) + ), + }), + }) + ), + link: linkConnectionRefsSchema.optional(), + }), +}) diff --git a/types/components/blocks/cardGallery.ts b/types/components/blocks/cardGallery.ts new file mode 100644 index 000000000..9733c443d --- /dev/null +++ b/types/components/blocks/cardGallery.ts @@ -0,0 +1,3 @@ +import type { CardGallery } from "@/types/trpc/routers/contentstack/blocks" + +export interface CardGalleryProps extends Pick {} diff --git a/types/enums/blocks.ts b/types/enums/blocks.ts index 4bb7559fd..05f25d680 100644 --- a/types/enums/blocks.ts +++ b/types/enums/blocks.ts @@ -1,17 +1,18 @@ export namespace BlocksEnums { export const enum block { Accordion = "Accordion", + CardGallery = "CardGallery", CardsGrid = "CardsGrid", + CarouselCards = "CarouselCards", Content = "Content", DynamicContent = "DynamicContent", + FullWidthCampaign = "FullWidthCampaign", + HotelListing = "HotelListing", + JoinScandicFriends = "JoinScandicFriends", Shortcuts = "Shortcuts", Table = "Table", TextCols = "TextCols", TextContent = "TextContent", UspGrid = "UspGrid", - HotelListing = "HotelListing", - FullWidthCampaign = "FullWidthCampaign", - CarouselCards = "CarouselCards", - JoinScandicFriends = "JoinScandicFriends", } } diff --git a/types/enums/cardGallery.ts b/types/enums/cardGallery.ts new file mode 100644 index 000000000..6b7767004 --- /dev/null +++ b/types/enums/cardGallery.ts @@ -0,0 +1,10 @@ +export const CardGalleryFilterEnum = { + offers: "offers", + popular_destinations: "popular_destinations", + popular_hotels: "popular_hotels", + popular_cities: "popular_cities", + spa_wellness: "spa_wellness", +} as const + +export type CardGalleryFilter = + (typeof CardGalleryFilterEnum)[keyof typeof CardGalleryFilterEnum] diff --git a/types/enums/destinationOverviewPage.ts b/types/enums/destinationOverviewPage.ts index 304028a88..274f7a606 100644 --- a/types/enums/destinationOverviewPage.ts +++ b/types/enums/destinationOverviewPage.ts @@ -1,7 +1,7 @@ export namespace DestinationOverviewPageEnum { export namespace ContentStack { export const enum blocks { - CarouselCards = "DestinationOverviewPageBlocksCarouselCards", + CardGallery = "DestinationOverviewPageBlocksCardGallery", } } } diff --git a/types/trpc/routers/contentstack/blocks.ts b/types/trpc/routers/contentstack/blocks.ts index b22595325..22a88e1d9 100644 --- a/types/trpc/routers/contentstack/blocks.ts +++ b/types/trpc/routers/contentstack/blocks.ts @@ -1,5 +1,6 @@ import type { z } from "zod" +import type { cardGallerySchema } from "@/server/routers/contentstack/schemas/blocks/cardGallery" 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" @@ -24,3 +25,4 @@ export interface UspGrid extends z.output {} interface GetHotelListing extends z.output {} export type HotelListing = GetHotelListing["hotel_listing"] export interface CarouselCards extends z.output {} +export interface CardGallery extends z.output {}