From 11201e238dd248a71569d48f49be3cd9db2c93f4 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Tue, 24 Jun 2025 10:22:07 +0000 Subject: [PATCH] feat(SW-2975): Added top campaign to campaign overview page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Approved-by: Matilda Landström --- .../Blocks/CampaignHotelListing/Client.tsx | 16 ++-- .../campaignHotelListing.module.css | 5 -- .../Blocks/CampaignHotelListing/index.tsx | 13 ++- .../TopCampaign/index.tsx | 43 ++++++++++ .../TopCampaign/topCampaign.module.css | 8 ++ .../CampaignOverviewPage/index.tsx | 6 +- .../ContentType/CampaignPage/Hero/index.tsx | 6 +- .../CampaignOverviewPage/TopCampaign.graphql | 24 ++++++ .../CampaignOverviewPage.graphql | 15 ++++ .../Query/CampaignPage/CampaignPage.graphql | 3 - .../campaignOverviewPage/output.ts | 86 ++++++++++++++++--- .../campaignOverviewPage/utils.ts | 5 ++ .../contentstack/campaignPage/output.ts | 19 +++- 13 files changed, 212 insertions(+), 37 deletions(-) create mode 100644 apps/scandic-web/components/ContentType/CampaignOverviewPage/TopCampaign/index.tsx create mode 100644 apps/scandic-web/components/ContentType/CampaignOverviewPage/TopCampaign/topCampaign.module.css create mode 100644 apps/scandic-web/lib/graphql/Fragments/CampaignOverviewPage/TopCampaign.graphql diff --git a/apps/scandic-web/components/Blocks/CampaignHotelListing/Client.tsx b/apps/scandic-web/components/Blocks/CampaignHotelListing/Client.tsx index 4aecf32fc..87b7ea581 100644 --- a/apps/scandic-web/components/Blocks/CampaignHotelListing/Client.tsx +++ b/apps/scandic-web/components/Blocks/CampaignHotelListing/Client.tsx @@ -21,20 +21,24 @@ import type { HotelDataWithUrl } from "@/types/hotel" interface CampaignHotelListingClientProps { heading: string hotels: HotelDataWithUrl[] + visibleCountMobile?: 3 | 6 + visibleCountDesktop?: 3 | 6 } export default function CampaignHotelListingClient({ heading, hotels, + visibleCountMobile = 3, + visibleCountDesktop = 6, }: CampaignHotelListingClientProps) { const intl = useIntl() const isMobile = useMediaQuery("(max-width: 767px)") const scrollRef = useRef(null) - const initialCount = isMobile ? 3 : 6 // Initial number of hotels to show - const thresholdCount = isMobile ? 6 : 9 // This is the threshold at which we start showing the "Show More" button - const showAllThreshold = isMobile ? 9 : 18 // This is the threshold at which we show the "Show All" button - const incrementCount = isMobile ? 3 : 6 // Number of hotels to increment when the button is clicked + const initialCount = isMobile ? visibleCountMobile : visibleCountDesktop // Initial number of hotels to show + const thresholdCount = initialCount + 3 // This is the threshold at which we start showing the "Show More" button + const showAllThreshold = initialCount * 3 // This is the threshold at which we show the "Show All" button + const incrementCount = initialCount // Number of hotels to increment when the button is clicked const [visibleCount, setVisibleCount] = useState(() => // Set initial visible count based on the number of hotels and the threshold @@ -86,8 +90,8 @@ export default function CampaignHotelListingClient({ return (
- -

{heading}

+ +

{heading}

    diff --git a/apps/scandic-web/components/Blocks/CampaignHotelListing/campaignHotelListing.module.css b/apps/scandic-web/components/Blocks/CampaignHotelListing/campaignHotelListing.module.css index 75cd06075..e90fc592e 100644 --- a/apps/scandic-web/components/Blocks/CampaignHotelListing/campaignHotelListing.module.css +++ b/apps/scandic-web/components/Blocks/CampaignHotelListing/campaignHotelListing.module.css @@ -8,10 +8,6 @@ scroll-margin-top: var(--scroll-margin-top); } -.heading { - color: var(--Text-Heading); -} - .list { list-style: none; display: grid; @@ -27,7 +23,6 @@ --scroll-margin-top: calc( var(--booking-widget-tablet-height) + var(--Spacing-x2) ); - gap: var(--Space-x5); } .list { row-gap: var(--Space-x5); diff --git a/apps/scandic-web/components/Blocks/CampaignHotelListing/index.tsx b/apps/scandic-web/components/Blocks/CampaignHotelListing/index.tsx index fc6cc1be2..d0e98f982 100644 --- a/apps/scandic-web/components/Blocks/CampaignHotelListing/index.tsx +++ b/apps/scandic-web/components/Blocks/CampaignHotelListing/index.tsx @@ -5,11 +5,15 @@ import CampaignHotelListingClient from "./Client" interface CampaignHotelListingProps { heading: string hotelIds: string[] + visibleCountMobile?: 3 | 6 + visibleCountDesktop?: 3 | 6 } export default async function CampaignHotelListing({ heading, hotelIds, + visibleCountMobile, + visibleCountDesktop, }: CampaignHotelListingProps) { const hotels = await getHotelsByCSFilter({ hotelsToInclude: hotelIds }) @@ -17,5 +21,12 @@ export default async function CampaignHotelListing({ return null } - return + return ( + + ) } diff --git a/apps/scandic-web/components/ContentType/CampaignOverviewPage/TopCampaign/index.tsx b/apps/scandic-web/components/ContentType/CampaignOverviewPage/TopCampaign/index.tsx new file mode 100644 index 000000000..cb69218e3 --- /dev/null +++ b/apps/scandic-web/components/ContentType/CampaignOverviewPage/TopCampaign/index.tsx @@ -0,0 +1,43 @@ +import { Typography } from "@scandic-hotels/design-system/Typography" + +import CampaignHotelListing from "@/components/Blocks/CampaignHotelListing" +import CampaignHero from "@/components/ContentType/CampaignPage/Hero" +import { getIntl } from "@/i18n" +import { getLang } from "@/i18n/serverContext" + +import styles from "./topCampaign.module.css" + +import { type CampaignOverviewPageData } from "@/types/trpc/routers/contentstack/campaignOverviewPage" + +interface TopCampaignProps { + topCampaign: CampaignOverviewPageData["topCampaign"] +} + +export default async function TopCampaign({ topCampaign }: TopCampaignProps) { + const lang = await getLang() + const intl = await getIntl() + const buttonData = { + cta: intl.formatMessage({ defaultMessage: "Explore the offer" }), + url: `/${lang}${topCampaign.url}`, + } + + return ( +
    + +

    {topCampaign.heading}

    +
    + + +
    + ) +} diff --git a/apps/scandic-web/components/ContentType/CampaignOverviewPage/TopCampaign/topCampaign.module.css b/apps/scandic-web/components/ContentType/CampaignOverviewPage/TopCampaign/topCampaign.module.css new file mode 100644 index 000000000..e0bafbdcf --- /dev/null +++ b/apps/scandic-web/components/ContentType/CampaignOverviewPage/TopCampaign/topCampaign.module.css @@ -0,0 +1,8 @@ +.topCampaign { + display: grid; + gap: var(--Space-x3); +} + +.heading { + color: var(--Text-Heading); +} diff --git a/apps/scandic-web/components/ContentType/CampaignOverviewPage/index.tsx b/apps/scandic-web/components/ContentType/CampaignOverviewPage/index.tsx index 859aa4f9e..8ab819481 100644 --- a/apps/scandic-web/components/ContentType/CampaignOverviewPage/index.tsx +++ b/apps/scandic-web/components/ContentType/CampaignOverviewPage/index.tsx @@ -5,6 +5,7 @@ import { Typography } from "@scandic-hotels/design-system/Typography" import { getCampaignOverviewPage } from "@/lib/trpc/memoizedRequests" +import TopCampaign from "@/components/ContentType/CampaignOverviewPage/TopCampaign" import LinkChips from "@/components/TempDesignSystem/LinkChips" import CampaignOverviewPageSkeleton from "./CampaignOverviewPageSkeleton" @@ -19,7 +20,7 @@ export default async function CampaignOverviewPage() { } const { campaignOverviewPage } = pageData - const { header } = campaignOverviewPage + const { header, topCampaign } = campaignOverviewPage return ( <> @@ -46,8 +47,7 @@ export default async function CampaignOverviewPage() { ) : null}
    - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {">>> MAIN CONTENT <<<"} +
    diff --git a/apps/scandic-web/components/ContentType/CampaignPage/Hero/index.tsx b/apps/scandic-web/components/ContentType/CampaignPage/Hero/index.tsx index be3e9f9ef..6b22a2cb2 100644 --- a/apps/scandic-web/components/ContentType/CampaignPage/Hero/index.tsx +++ b/apps/scandic-web/components/ContentType/CampaignPage/Hero/index.tsx @@ -10,7 +10,7 @@ import { variants } from "./variants" import styles from "./hero.module.css" -import type { HeroProps } from "./types" +import type { HeroProps } from "@/components/ContentType/CampaignPage/Hero/types" export default async function CampaignHero({ image, @@ -82,11 +82,11 @@ export default async function CampaignHero({ ) : null} ) : null} - {button.link ? ( + {button ? ( diff --git a/apps/scandic-web/lib/graphql/Fragments/CampaignOverviewPage/TopCampaign.graphql b/apps/scandic-web/lib/graphql/Fragments/CampaignOverviewPage/TopCampaign.graphql new file mode 100644 index 000000000..5e205e58b --- /dev/null +++ b/apps/scandic-web/lib/graphql/Fragments/CampaignOverviewPage/TopCampaign.graphql @@ -0,0 +1,24 @@ +#import "../System.graphql" + +#import "../Blocks/HotelListing.graphql" +#import "../CampaignPage/IncludedHotels.graphql" +#import "../CampaignPage/Hero.graphql" + +fragment TopCampaign on CampaignPage { + heading + included_hotels { + ...CampaignPageIncludedHotels + } + blocks { + __typename + ...HotelListing_CampaignPage + } + url + ...Hero_CampaignPage +} + +fragment TopCampaignRef on CampaignPage { + system { + ...System + } +} diff --git a/apps/scandic-web/lib/graphql/Query/CampaignOverviewPage/CampaignOverviewPage.graphql b/apps/scandic-web/lib/graphql/Query/CampaignOverviewPage/CampaignOverviewPage.graphql index ae888feec..be30b77af 100644 --- a/apps/scandic-web/lib/graphql/Query/CampaignOverviewPage/CampaignOverviewPage.graphql +++ b/apps/scandic-web/lib/graphql/Query/CampaignOverviewPage/CampaignOverviewPage.graphql @@ -1,6 +1,7 @@ #import "../../Fragments/System.graphql" #import "../../Fragments/CampaignOverviewPage/NavigationLinks.graphql" +#import "../../Fragments/CampaignOverviewPage/TopCampaign.graphql" query GetCampaignOverviewPage($locale: String!, $uid: String!) { campaign_overview_page(uid: $uid, locale: $locale) { @@ -10,6 +11,13 @@ query GetCampaignOverviewPage($locale: String!, $uid: String!) { preamble ...NavigationLinks_CampaignOverviewPage } + top_campaignConnection { + edges { + node { + ...TopCampaign + } + } + } system { ...System created_at @@ -26,6 +34,13 @@ query GetCampaignOverviewPageRefs($locale: String!, $uid: String!) { header { ...NavigationLinksRef_CampaignOverviewPage } + top_campaignConnection { + edges { + node { + ...TopCampaignRef + } + } + } system { ...System } diff --git a/apps/scandic-web/lib/graphql/Query/CampaignPage/CampaignPage.graphql b/apps/scandic-web/lib/graphql/Query/CampaignPage/CampaignPage.graphql index 0430111a4..1ee76426f 100644 --- a/apps/scandic-web/lib/graphql/Query/CampaignPage/CampaignPage.graphql +++ b/apps/scandic-web/lib/graphql/Query/CampaignPage/CampaignPage.graphql @@ -42,9 +42,6 @@ query GetCampaignPage($locale: String!, $uid: String!) { query GetCampaignPageRefs($locale: String!, $uid: String!) { campaign_page(locale: $locale, uid: $uid) { - included_hotels { - ...CampaignPageIncludedHotelsRef - } blocks { __typename ...CarouselCards_CampaignPageRefs diff --git a/apps/scandic-web/server/routers/contentstack/campaignOverviewPage/output.ts b/apps/scandic-web/server/routers/contentstack/campaignOverviewPage/output.ts index b71ea6dd6..cf7e9bff8 100644 --- a/apps/scandic-web/server/routers/contentstack/campaignOverviewPage/output.ts +++ b/apps/scandic-web/server/routers/contentstack/campaignOverviewPage/output.ts @@ -1,5 +1,11 @@ import { z } from "zod" +import { discriminatedUnionArray } from "@/lib/discriminatedUnion" +import { + campaignPageHotelListing, + heroSchema, + includedHotelsSchema, +} from "@/server/routers/contentstack/campaignPage/output" import { linkAndTitleSchema, linkConnectionRefs, @@ -7,6 +13,8 @@ import { import { systemSchema } from "../schemas/system" +import { CampaignPageEnum } from "@/types/enums/campaignPage" + const navigationLinksSchema = z .array(linkAndTitleSchema) .nullable() @@ -23,21 +31,64 @@ const navigationLinksSchema = z })) }) +export const blocksSchema = z.discriminatedUnion("__typename", [ + campaignPageHotelListing, +]) + +const topCampaignSchema = z + .object({ + heading: z.string(), + hero: heroSchema, + included_hotels: includedHotelsSchema, + blocks: discriminatedUnionArray(blocksSchema.options), + url: z.string(), + }) + .transform((data) => { + const { blocks, included_hotels, ...rest } = data + const hotelListingBlock = blocks.find( + (block) => + block.__typename === CampaignPageEnum.ContentStack.blocks.HotelListing + ) + + return { + ...rest, + hotel_listing: { + heading: hotelListingBlock?.hotel_listing.heading || "", + hotelIds: included_hotels, + }, + } + }) + export const campaignOverviewPageSchema = z.object({ - campaign_overview_page: z.object({ - title: z.string(), - header: z.object({ - heading: z.string(), - preamble: z.string(), - navigation_links: navigationLinksSchema, + campaign_overview_page: z + .object({ + title: z.string(), + header: z.object({ + heading: z.string(), + preamble: z.string(), + navigation_links: navigationLinksSchema, + }), + top_campaignConnection: z.object({ + edges: z.array( + z.object({ + node: topCampaignSchema, + }) + ), + }), + system: systemSchema.merge( + z.object({ + created_at: z.string(), + updated_at: z.string(), + }) + ), + }) + .transform((data) => { + const { top_campaignConnection, ...rest } = data + return { + ...rest, + topCampaign: top_campaignConnection.edges.map(({ node }) => node)[0], + } }), - system: systemSchema.merge( - z.object({ - created_at: z.string(), - updated_at: z.string(), - }) - ), - }), trackingProps: z.object({ url: z.string(), }), @@ -52,6 +103,15 @@ const campaignOverviewPageHeaderRefs = z.object({ export const campaignOverviewPageRefsSchema = z.object({ campaign_overview_page: z.object({ header: campaignOverviewPageHeaderRefs, + top_campaignConnection: z.object({ + edges: z.array( + z.object({ + node: z.object({ + system: systemSchema, + }), + }) + ), + }), system: systemSchema, }), }) diff --git a/apps/scandic-web/server/routers/contentstack/campaignOverviewPage/utils.ts b/apps/scandic-web/server/routers/contentstack/campaignOverviewPage/utils.ts index 63ffe830f..9fd9f20b7 100644 --- a/apps/scandic-web/server/routers/contentstack/campaignOverviewPage/utils.ts +++ b/apps/scandic-web/server/routers/contentstack/campaignOverviewPage/utils.ts @@ -28,6 +28,11 @@ export function getConnections({ } }) } + if (campaign_overview_page.top_campaignConnection) { + campaign_overview_page.top_campaignConnection.edges.forEach(({ node }) => { + connections.push(node.system) + }) + } return connections } diff --git a/apps/scandic-web/server/routers/contentstack/campaignPage/output.ts b/apps/scandic-web/server/routers/contentstack/campaignPage/output.ts index 270bbd448..25581fb5a 100644 --- a/apps/scandic-web/server/routers/contentstack/campaignPage/output.ts +++ b/apps/scandic-web/server/routers/contentstack/campaignPage/output.ts @@ -56,17 +56,30 @@ export const heroSchema = z.object({ image: tempImageVaultAssetSchema, heading: z.string(), theme: z.enum(["Peach", "Burgundy"]).default("Peach"), - benefits: z.array(z.string()).nullish(), + benefits: z + .array(z.string()) + .nullish() + .transform((data) => data || []), rate_text: z .object({ bold_text: z.string().nullish(), text: z.string().nullish(), }) .nullish(), - button: z.intersection(z.object({ cta: z.string() }), linkConnectionSchema), + button: z + .intersection(z.object({ cta: z.string() }), linkConnectionSchema) + .transform((data) => { + if (!data.link) { + return null + } + return { + cta: data.cta, + url: data.link?.url || "", + } + }), }) -const includedHotelsSchema = z +export const includedHotelsSchema = z .object({ list_1Connection: z.object({ edges: z.array(