From af92f7183c92ee81e00ae6d15668fd5635427d7a Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Thu, 19 Jun 2025 09:36:28 +0000 Subject: [PATCH] feat(SW-2278): Added hotel listing to campaign page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Approved-by: Matilda Landström --- .../CampaignHotelListingSkeleton.tsx | 28 ++++ .../Blocks/CampaignHotelListing/Client.tsx | 120 ++++++++++++++++ .../HotelListingItemSkeleton.tsx | 59 ++++++++ .../hotelListingItem.module.css | 64 +++++++++ .../HotelListingItem/index.tsx | 129 ++++++++++++++++++ .../campaignHotelListing.module.css | 56 ++++++++ .../Blocks/CampaignHotelListing/index.tsx | 21 +++ .../components/Blocks/Essentials/index.tsx | 2 +- apps/scandic-web/components/Blocks/index.tsx | 2 +- .../ContentType/CampaignPage/Blocks/index.tsx | 13 ++ .../HotelListItem/hotelListItem.module.css | 4 +- .../Fragments/Blocks/HotelListing.graphql | 7 + .../CampaignPage/IncludedHotels.graphql | 45 ++++++ .../Query/CampaignPage/CampaignPage.graphql | 12 +- .../contentstack/campaignPage/output.ts | 112 +++++++++++---- .../contentstack/contentPage/output.ts | 4 +- .../schemas/blocks/hotelListing.ts | 15 +- .../server/routers/hotels/input.ts | 2 +- apps/scandic-web/types/enums/blocks.ts | 3 +- apps/scandic-web/types/enums/campaignPage.ts | 1 + .../types/trpc/routers/contentstack/blocks.ts | 5 +- .../trpc/routers/contentstack/campaignPage.ts | 3 +- .../components/Icons/Logos/DowntownCamper.tsx | 3 +- .../components/Icons/Logos/GrandHotelOslo.tsx | 3 +- .../lib/components/Icons/Logos/Haymarket.tsx | 3 +- .../lib/components/Icons/Logos/HotelNorge.tsx | 3 +- .../lib/components/Icons/Logos/Marski.tsx | 3 +- .../components/Icons/Logos/ScandicGoLogo.tsx | 3 +- .../components/Icons/Logos/ScandicLogo.tsx | 3 +- .../lib/components/Icons/Logos/TheDock.tsx | 9 +- .../lib/components/Icons/Logos/index.tsx | 23 ++-- 31 files changed, 703 insertions(+), 57 deletions(-) create mode 100644 apps/scandic-web/components/Blocks/CampaignHotelListing/CampaignHotelListingSkeleton.tsx create mode 100644 apps/scandic-web/components/Blocks/CampaignHotelListing/Client.tsx create mode 100644 apps/scandic-web/components/Blocks/CampaignHotelListing/HotelListingItem/HotelListingItemSkeleton.tsx create mode 100644 apps/scandic-web/components/Blocks/CampaignHotelListing/HotelListingItem/hotelListingItem.module.css create mode 100644 apps/scandic-web/components/Blocks/CampaignHotelListing/HotelListingItem/index.tsx create mode 100644 apps/scandic-web/components/Blocks/CampaignHotelListing/campaignHotelListing.module.css create mode 100644 apps/scandic-web/components/Blocks/CampaignHotelListing/index.tsx create mode 100644 apps/scandic-web/lib/graphql/Fragments/CampaignPage/IncludedHotels.graphql diff --git a/apps/scandic-web/components/Blocks/CampaignHotelListing/CampaignHotelListingSkeleton.tsx b/apps/scandic-web/components/Blocks/CampaignHotelListing/CampaignHotelListingSkeleton.tsx new file mode 100644 index 000000000..bea00e931 --- /dev/null +++ b/apps/scandic-web/components/Blocks/CampaignHotelListing/CampaignHotelListingSkeleton.tsx @@ -0,0 +1,28 @@ +"use client" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import SkeletonShimmer from "@/components/SkeletonShimmer" + +import HotelListingItemSkeleton from "./HotelListingItem/HotelListingItemSkeleton" + +import styles from "./campaignHotelListing.module.css" + +export default function CampaignHotelListingSkeleton() { + return ( +
+
+ + + +
+ +
+ ) +} diff --git a/apps/scandic-web/components/Blocks/CampaignHotelListing/Client.tsx b/apps/scandic-web/components/Blocks/CampaignHotelListing/Client.tsx new file mode 100644 index 000000000..4aecf32fc --- /dev/null +++ b/apps/scandic-web/components/Blocks/CampaignHotelListing/Client.tsx @@ -0,0 +1,120 @@ +"use client" + +import { cx } from "class-variance-authority" +import { useRef, useState } from "react" +import { useIntl } from "react-intl" +import { useMediaQuery } from "usehooks-ts" + +import { Button } from "@scandic-hotels/design-system/Button" +import { + MaterialIcon, + type MaterialIconProps, +} from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import HotelListingItem from "./HotelListingItem" + +import styles from "./campaignHotelListing.module.css" + +import type { HotelDataWithUrl } from "@/types/hotel" + +interface CampaignHotelListingClientProps { + heading: string + hotels: HotelDataWithUrl[] +} + +export default function CampaignHotelListingClient({ + heading, + hotels, +}: 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 [visibleCount, setVisibleCount] = useState(() => + // Set initial visible count based on the number of hotels and the threshold + hotels.length <= thresholdCount ? hotels.length : initialCount + ) + + // Only show the show more/less button if the length of hotels exceeds the threshold count + const showButton = hotels.length >= thresholdCount + + // Determine if we are at the stage where the user can click to show all hotels + const canShowAll = + hotels.length > visibleCount && + (visibleCount + incrementCount > showAllThreshold || + visibleCount + incrementCount >= hotels.length) + + function handleButtonClick() { + if (visibleCount < hotels.length) { + if (canShowAll) { + setVisibleCount(hotels.length) + } else { + setVisibleCount((prev) => + Math.min(prev + incrementCount, hotels.length) + ) + } + } else { + setVisibleCount(initialCount) + if (scrollRef.current) { + scrollRef.current.scrollIntoView({ behavior: "smooth" }) + } + } + } + + let buttonText = intl.formatMessage({ + defaultMessage: "Show more", + }) + let iconDirection: MaterialIconProps["icon"] = "keyboard_arrow_down" + + if (visibleCount === hotels.length) { + buttonText = intl.formatMessage({ + defaultMessage: "Show less", + }) + iconDirection = "keyboard_arrow_up" + } else if (canShowAll) { + buttonText = intl.formatMessage({ + defaultMessage: "Show all", + }) + } + + return ( +
+
+ +

{heading}

+
+
+
    + {hotels.map(({ hotel, url }, index) => ( +
  • = visibleCount, + })} + > + +
  • + ))} +
+ + {showButton ? ( + + ) : null} +
+ ) +} diff --git a/apps/scandic-web/components/Blocks/CampaignHotelListing/HotelListingItem/HotelListingItemSkeleton.tsx b/apps/scandic-web/components/Blocks/CampaignHotelListing/HotelListingItem/HotelListingItemSkeleton.tsx new file mode 100644 index 000000000..9bb20ce9f --- /dev/null +++ b/apps/scandic-web/components/Blocks/CampaignHotelListing/HotelListingItem/HotelListingItemSkeleton.tsx @@ -0,0 +1,59 @@ +import { Divider } from "@scandic-hotels/design-system/Divider" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import SkeletonShimmer from "@/components/SkeletonShimmer" + +import styles from "./hotelListingItem.module.css" + +export default function HotelListingItemSkeleton() { + return ( +
+
+ +
+
+
+ + + + + +
+ + + + + +
+
+
+ +

+ + + +

+
+ +
    + {Array.from({ length: 5 }).map((_, index) => { + return ( +
  • + + +
  • + ) + })} +
+
+
+
+ +
+
+ ) +} diff --git a/apps/scandic-web/components/Blocks/CampaignHotelListing/HotelListingItem/hotelListingItem.module.css b/apps/scandic-web/components/Blocks/CampaignHotelListing/HotelListingItem/hotelListingItem.module.css new file mode 100644 index 000000000..16198e5eb --- /dev/null +++ b/apps/scandic-web/components/Blocks/CampaignHotelListing/HotelListingItem/hotelListingItem.module.css @@ -0,0 +1,64 @@ +.hotelListingItem { + border-radius: var(--Corner-radius-md); + overflow: hidden; + display: grid; + grid-template-rows: auto 1fr auto; + gap: var(--Space-x2); + height: 100%; + padding-bottom: var(--Space-x2); +} + +.imageWrapper { + height: 220px; + width: 100%; + position: relative; +} + +.tripAdvisor { + position: absolute; + top: var(--Space-x2); + left: var(--Space-x2); + display: flex; + align-items: center; + gap: var(--Space-x05); + background-color: var(--Surface-Primary-Default); + padding: var(--Space-x025) var(--Space-x1); + border-radius: var(--Corner-radius-sm); + color: var(--Text-Interactive-Default); +} + +.content { + padding: 0 var(--Space-x2); + display: grid; + gap: var(--Space-x15); + align-content: start; +} + +.intro { + display: grid; + gap: var(--Space-x05); +} + +.captions { + display: flex; + column-gap: var(--Space-x1); + flex-wrap: wrap; + color: var(--Text-Tertiary); +} + +.amenityList { + display: flex; + gap: var(--Space-x025) var(--Space-x1); + flex-wrap: wrap; + color: var(--Text-Secondary); +} + +.amenityItem { + display: flex; + gap: var(--Space-x05); + align-items: center; +} + +.ctaWrapper { + padding: 0 var(--Space-x2); +} diff --git a/apps/scandic-web/components/Blocks/CampaignHotelListing/HotelListingItem/index.tsx b/apps/scandic-web/components/Blocks/CampaignHotelListing/HotelListingItem/index.tsx new file mode 100644 index 000000000..97ee0a8ef --- /dev/null +++ b/apps/scandic-web/components/Blocks/CampaignHotelListing/HotelListingItem/index.tsx @@ -0,0 +1,129 @@ +import { useIntl } from "react-intl" + +import { Divider } from "@scandic-hotels/design-system/Divider" +import HotelLogoIcon from "@scandic-hotels/design-system/Icons/HotelLogoIcon" +import TripadvisorIcon from "@scandic-hotels/design-system/Icons/TripadvisorIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import ButtonLink from "@/components/ButtonLink" +import { FacilityToIcon } from "@/components/ContentType/HotelPage/data" +import ImageGallery from "@/components/ImageGallery" +import { mapApiImagesToGalleryImages } from "@/utils/imageGallery" +import { getSingleDecimal } from "@/utils/numberFormatting" + +import styles from "./hotelListingItem.module.css" + +import type { Hotel } from "@/types/hotel" + +interface HotelListingItemProps { + hotel: Hotel + url: string +} + +export default function HotelListingItem({ + hotel, + url, +}: HotelListingItemProps) { + const intl = useIntl() + const galleryImages = mapApiImagesToGalleryImages(hotel.galleryImages || []) + const tripadvisorRating = hotel.ratings?.tripAdvisor.rating + const address = `${hotel.address.streetAddress}, ${hotel.address.city}` + const amenities = hotel.detailedFacilities.slice(0, 5) + const hotelDescription = hotel.hotelContent.texts.descriptions?.short + + return ( +
+
+ + {tripadvisorRating ? ( + +
+ + {tripadvisorRating} +
+
+ ) : null} +
+
+
+ + +

{hotel.name}

+
+ +
+ {address} + + + + + {intl.formatMessage( + { + defaultMessage: "{number} km to city center", + }, + { + number: getSingleDecimal( + hotel.location.distanceToCentre / 1000 + ), + } + )} + +
+
+
+ {hotelDescription ? ( + +

{hotelDescription}

+
+ ) : null} + +
    + {amenities.map((amenity) => { + return ( +
  • + + {amenity.name} +
  • + ) + })} +
+
+
+
+ + {intl.formatMessage({ + defaultMessage: "See hotel details", + })} + +
+
+ ) +} diff --git a/apps/scandic-web/components/Blocks/CampaignHotelListing/campaignHotelListing.module.css b/apps/scandic-web/components/Blocks/CampaignHotelListing/campaignHotelListing.module.css new file mode 100644 index 000000000..75cd06075 --- /dev/null +++ b/apps/scandic-web/components/Blocks/CampaignHotelListing/campaignHotelListing.module.css @@ -0,0 +1,56 @@ +.hotelListingSection { + --scroll-margin-top: calc( + var(--booking-widget-mobile-height) + var(--Spacing-x2) + ); + + display: grid; + gap: var(--Space-x3); + scroll-margin-top: var(--scroll-margin-top); +} + +.heading { + color: var(--Text-Heading); +} + +.list { + list-style: none; + display: grid; + gap: var(--Space-x4); +} + +.listItem.hidden { + display: none; +} + +@media screen and (min-width: 768px) { + .hotelListingSection { + --scroll-margin-top: calc( + var(--booking-widget-tablet-height) + var(--Spacing-x2) + ); + gap: var(--Space-x5); + } + .list { + row-gap: var(--Space-x5); + column-gap: var(--Space-x2); + } +} + +@media screen and (min-width: 768px) and (max-width: 949px) { + .list { + grid-template-columns: repeat(2, 1fr); + } +} + +@media screen and (min-width: 950px) { + .list { + grid-template-columns: repeat(3, 1fr); + } +} + +@media screen and (min-width: 1367px) { + .hotelListingSection { + --scroll-margin-top: calc( + var(--booking-widget-desktop-height) + var(--Spacing-x2) + ); + } +} diff --git a/apps/scandic-web/components/Blocks/CampaignHotelListing/index.tsx b/apps/scandic-web/components/Blocks/CampaignHotelListing/index.tsx new file mode 100644 index 000000000..fc6cc1be2 --- /dev/null +++ b/apps/scandic-web/components/Blocks/CampaignHotelListing/index.tsx @@ -0,0 +1,21 @@ +import { getHotelsByCSFilter } from "@/lib/trpc/memoizedRequests" + +import CampaignHotelListingClient from "./Client" + +interface CampaignHotelListingProps { + heading: string + hotelIds: string[] +} + +export default async function CampaignHotelListing({ + heading, + hotelIds, +}: CampaignHotelListingProps) { + const hotels = await getHotelsByCSFilter({ hotelsToInclude: hotelIds }) + + if (!hotels.length) { + return null + } + + return +} diff --git a/apps/scandic-web/components/Blocks/Essentials/index.tsx b/apps/scandic-web/components/Blocks/Essentials/index.tsx index 9aaeca310..89a0b79cf 100644 --- a/apps/scandic-web/components/Blocks/Essentials/index.tsx +++ b/apps/scandic-web/components/Blocks/Essentials/index.tsx @@ -18,7 +18,7 @@ export default async function Essentials({ content }: EssentialsProps) { return (
- +

{title}

{preamble ? ( diff --git a/apps/scandic-web/components/Blocks/index.tsx b/apps/scandic-web/components/Blocks/index.tsx index b78b834d9..05ca3d60d 100644 --- a/apps/scandic-web/components/Blocks/index.tsx +++ b/apps/scandic-web/components/Blocks/index.tsx @@ -66,7 +66,7 @@ export default function Blocks({ blocks }: BlocksProps) { key={`${block.card_gallery.heading}-${idx}`} /> ) - case BlocksEnums.block.HotelListing: + case BlocksEnums.block.ContentPageHotelListing: const { heading, contentType, locationFilter, hotelsToInclude } = block.hotel_listing if (!locationFilter && !hotelsToInclude.length) { diff --git a/apps/scandic-web/components/ContentType/CampaignPage/Blocks/index.tsx b/apps/scandic-web/components/ContentType/CampaignPage/Blocks/index.tsx index 4f60fdbe3..e502d29e2 100644 --- a/apps/scandic-web/components/ContentType/CampaignPage/Blocks/index.tsx +++ b/apps/scandic-web/components/ContentType/CampaignPage/Blocks/index.tsx @@ -1,4 +1,8 @@ +import { Suspense } from "react" + import AccordionSection from "@/components/Blocks/Accordion" +import CampaignHotelListing from "@/components/Blocks/CampaignHotelListing" +import CampaignHotelListingSkeleton from "@/components/Blocks/CampaignHotelListing/CampaignHotelListingSkeleton" import CarouselCards from "@/components/Blocks/CarouselCards" import Essentials from "@/components/Blocks/Essentials" @@ -27,6 +31,15 @@ export default function Blocks({ blocks }: BlocksProps) { key={block.accordion.title} /> ) + case BlocksEnums.block.CampaignPageHotelListing: + return ( + }> + + + ) default: return null } diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/hotelListItem.module.css b/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/hotelListItem.module.css index a1338e301..c7c536733 100644 --- a/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/hotelListItem.module.css +++ b/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/hotelListItem.module.css @@ -27,8 +27,8 @@ .tripAdvisor { position: absolute; - top: 16px; - left: 16px; + top: var(--Space-x2); + left: var(--Space-x2); display: flex; align-items: center; gap: var(--Space-x05); diff --git a/apps/scandic-web/lib/graphql/Fragments/Blocks/HotelListing.graphql b/apps/scandic-web/lib/graphql/Fragments/Blocks/HotelListing.graphql index dc6cb8451..053d7ab0e 100644 --- a/apps/scandic-web/lib/graphql/Fragments/Blocks/HotelListing.graphql +++ b/apps/scandic-web/lib/graphql/Fragments/Blocks/HotelListing.graphql @@ -22,3 +22,10 @@ fragment HotelListing_ContentPage on ContentPageBlocksHotelListing { ...HotelListing } } + + +fragment HotelListing_CampaignPage on CampaignPageBlocksHotelListing { + hotel_listing { + heading + } +} diff --git a/apps/scandic-web/lib/graphql/Fragments/CampaignPage/IncludedHotels.graphql b/apps/scandic-web/lib/graphql/Fragments/CampaignPage/IncludedHotels.graphql new file mode 100644 index 000000000..9360bd956 --- /dev/null +++ b/apps/scandic-web/lib/graphql/Fragments/CampaignPage/IncludedHotels.graphql @@ -0,0 +1,45 @@ +fragment CampaignPageIncludedHotels on CampaignPageIncludedHotels { + list_1Connection { + edges { + node { + ... on HotelPage { + hotel_page_id + } + } + } + } + list_2Connection { + edges { + node { + ... on HotelPage { + hotel_page_id + } + } + } + } +} + +fragment CampaignPageIncludedHotelsRef on CampaignPageIncludedHotels { + list_1Connection { + edges { + node { + ... on HotelPage { + system { + ...System + } + } + } + } + } + list_2Connection { + edges { + node { + ... on HotelPage { + 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 745521c62..39ca5a823 100644 --- a/apps/scandic-web/lib/graphql/Query/CampaignPage/CampaignPage.graphql +++ b/apps/scandic-web/lib/graphql/Query/CampaignPage/CampaignPage.graphql @@ -1,9 +1,12 @@ #import "../../Fragments/System.graphql" +#import "../../Fragments/CampaignPage/IncludedHotels.graphql" +#import "../../Fragments/CampaignPage/Hero.graphql" + #import "../../Fragments/Blocks/Accordion.graphql" #import "../../Fragments/Blocks/Essentials.graphql" #import "../../Fragments/Blocks/CarouselCards.graphql" -#import "../../Fragments/CampaignPage/Hero.graphql" +#import "../../Fragments/Blocks/HotelListing.graphql" query GetCampaignPage($locale: String!, $uid: String!) { campaign_page(uid: $uid, locale: $locale) { @@ -16,11 +19,15 @@ query GetCampaignPage($locale: String!, $uid: String!) { first_column second_column } + included_hotels { + ...CampaignPageIncludedHotels + } blocks { __typename ...Essentials_CampaignPage ...CarouselCards_CampaignPage ...Accordion_CampaignPage + ...HotelListing_CampaignPage } system { ...System @@ -36,6 +43,9 @@ 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/campaignPage/output.ts b/apps/scandic-web/server/routers/contentstack/campaignPage/output.ts index d2b3aca0e..42c10243c 100644 --- a/apps/scandic-web/server/routers/contentstack/campaignPage/output.ts +++ b/apps/scandic-web/server/routers/contentstack/campaignPage/output.ts @@ -11,6 +11,7 @@ import { carouselCardsSchema, } from "../schemas/blocks/carouselCards" import { essentialsBlockSchema } from "../schemas/blocks/essentials" +import { campaignPageHotelListingSchema } from "../schemas/blocks/hotelListing" import { tempImageVaultAssetSchema } from "../schemas/imageVault" import { linkConnectionRefs, @@ -38,10 +39,17 @@ export const campaignPageAccordion = z }) .merge(accordionSchema) +export const campaignPageHotelListing = z + .object({ + __typename: z.literal(CampaignPageEnum.ContentStack.blocks.HotelListing), + }) + .merge(campaignPageHotelListingSchema) + export const blocksSchema = z.discriminatedUnion("__typename", [ campaignPageEssentials, campaignPageCarouselCards, campaignPageAccordion, + campaignPageHotelListing, ]) export const heroSchema = z.object({ @@ -58,30 +66,88 @@ export const heroSchema = z.object({ button: z.intersection(z.object({ cta: z.string() }), linkConnectionSchema), }) -export const campaignPageSchema = z.object({ - campaign_page: z.object({ - title: z.string(), - campaign_identifier: z.string().nullish(), - hero: heroSchema, - heading: z.string(), - subheading: z.string().nullish(), - preamble: z.object({ - is_two_columns: z.boolean().default(false), - first_column: z.string(), - second_column: z.string(), +const includedHotelsSchema = z + .object({ + list_1Connection: z.object({ + edges: z.array( + z.object({ + node: z.object({ + hotel_page_id: z.string(), + }), + }) + ), }), - blocks: discriminatedUnionArray(blocksSchema.options), - system: systemSchema.merge( - z.object({ - created_at: z.string(), - updated_at: z.string(), - }) - ), - }), - trackingProps: z.object({ - url: z.string(), - }), -}) + list_2Connection: z.object({ + edges: z.array( + z.object({ + node: z.object({ + hotel_page_id: z.string(), + }), + }) + ), + }), + }) + .transform((data) => { + const list1HotelIds = data.list_1Connection.edges + .map((edge) => edge.node.hotel_page_id) + .filter(Boolean) + const list2HotelIds = data.list_2Connection.edges + .map((edge) => edge.node.hotel_page_id) + .filter(Boolean) + + return [...new Set([...list1HotelIds, ...list2HotelIds])] + }) + +export const campaignPageSchema = z + .object({ + campaign_page: z.object({ + title: z.string(), + campaign_identifier: z.string().nullish(), + hero: heroSchema, + heading: z.string(), + subheading: z.string().nullish(), + included_hotels: includedHotelsSchema, + preamble: z.object({ + is_two_columns: z.boolean().default(false), + first_column: z.string(), + second_column: z.string(), + }), + blocks: discriminatedUnionArray(blocksSchema.options), + system: systemSchema.merge( + z.object({ + created_at: z.string(), + updated_at: z.string(), + }) + ), + }), + trackingProps: z.object({ + url: z.string(), + }), + }) + .transform((data) => { + const blocks = data.campaign_page.blocks.map((block) => { + if ( + block.__typename === CampaignPageEnum.ContentStack.blocks.HotelListing + ) { + return { + ...block, + hotel_listing: { + ...block.hotel_listing, + hotelIds: data.campaign_page.included_hotels, + }, + } + } + return block + }) + + return { + ...data, + campaign_page: { + ...data.campaign_page, + blocks: [...blocks], + }, + } + }) /** REFS */ const campaignPageCarouselCardsRef = z diff --git a/apps/scandic-web/server/routers/contentstack/contentPage/output.ts b/apps/scandic-web/server/routers/contentstack/contentPage/output.ts index 8ccf460f6..e03979ee0 100644 --- a/apps/scandic-web/server/routers/contentstack/contentPage/output.ts +++ b/apps/scandic-web/server/routers/contentstack/contentPage/output.ts @@ -18,7 +18,7 @@ import { dynamicContentRefsSchema, dynamicContentSchema as blockDynamicContentSchema, } from "../schemas/blocks/dynamicContent" -import { hotelListingSchema } from "../schemas/blocks/hotelListing" +import { contentPageHotelListingSchema } from "../schemas/blocks/hotelListing" import { shortcutsRefsSchema, shortcutsSchema, @@ -113,7 +113,7 @@ export const contentPageHotelListing = z .object({ __typename: z.literal(ContentPageEnum.ContentStack.blocks.HotelListing), }) - .merge(hotelListingSchema) + .merge(contentPageHotelListingSchema) export const blocksSchema = z.discriminatedUnion("__typename", [ contentPageAccordion, diff --git a/apps/scandic-web/server/routers/contentstack/schemas/blocks/hotelListing.ts b/apps/scandic-web/server/routers/contentstack/schemas/blocks/hotelListing.ts index e228c95b1..bc53e9b68 100644 --- a/apps/scandic-web/server/routers/contentstack/schemas/blocks/hotelListing.ts +++ b/apps/scandic-web/server/routers/contentstack/schemas/blocks/hotelListing.ts @@ -36,10 +36,10 @@ export const locationFilterSchema = z } }) -export const hotelListingSchema = z.object({ +export const contentPageHotelListingSchema = z.object({ typename: z - .literal(BlocksEnums.block.HotelListing) - .default(BlocksEnums.block.HotelListing), + .literal(BlocksEnums.block.ContentPageHotelListing) + .default(BlocksEnums.block.ContentPageHotelListing), hotel_listing: z .object({ heading: z.string().optional(), @@ -60,3 +60,12 @@ export const hotelListingSchema = z.object({ } }), }) + +export const campaignPageHotelListingSchema = z.object({ + typename: z + .literal(BlocksEnums.block.CampaignPageHotelListing) + .default(BlocksEnums.block.CampaignPageHotelListing), + hotel_listing: z.object({ + heading: z.string(), + }), +}) diff --git a/apps/scandic-web/server/routers/hotels/input.ts b/apps/scandic-web/server/routers/hotels/input.ts index ce14ffbd8..9241f5aa7 100644 --- a/apps/scandic-web/server/routers/hotels/input.ts +++ b/apps/scandic-web/server/routers/hotels/input.ts @@ -237,7 +237,7 @@ export const getHotelsByCSFilterInput = z.object({ country: z.nativeEnum(Country).nullable(), excluded: z.array(z.string()), }) - .nullable(), + .nullish(), hotelsToInclude: z.array(z.string()), }) export interface GetHotelsByCSFilterInput diff --git a/apps/scandic-web/types/enums/blocks.ts b/apps/scandic-web/types/enums/blocks.ts index 69d8f60cb..403df8f42 100644 --- a/apps/scandic-web/types/enums/blocks.ts +++ b/apps/scandic-web/types/enums/blocks.ts @@ -7,7 +7,8 @@ export namespace BlocksEnums { Content = "Content", DynamicContent = "DynamicContent", FullWidthCampaign = "FullWidthCampaign", - HotelListing = "HotelListing", + CampaignPageHotelListing = "CampaignPageHotelListing", + ContentPageHotelListing = "ContentPageHotelListing", JoinScandicFriends = "JoinScandicFriends", Shortcuts = "Shortcuts", Table = "Table", diff --git a/apps/scandic-web/types/enums/campaignPage.ts b/apps/scandic-web/types/enums/campaignPage.ts index d7cff74d9..000b6eb26 100644 --- a/apps/scandic-web/types/enums/campaignPage.ts +++ b/apps/scandic-web/types/enums/campaignPage.ts @@ -4,6 +4,7 @@ export namespace CampaignPageEnum { Essentials = "CampaignPageBlocksEssentials", CarouselCards = "CampaignPageBlocksCarouselCards", Accordion = "CampaignPageBlocksAccordion", + HotelListing = "CampaignPageBlocksHotelListing", } } } diff --git a/apps/scandic-web/types/trpc/routers/contentstack/blocks.ts b/apps/scandic-web/types/trpc/routers/contentstack/blocks.ts index 22a88e1d9..b2f0e4b02 100644 --- a/apps/scandic-web/types/trpc/routers/contentstack/blocks.ts +++ b/apps/scandic-web/types/trpc/routers/contentstack/blocks.ts @@ -6,7 +6,7 @@ import type { cardsGridSchema } from "@/server/routers/contentstack/schemas/bloc import type { carouselCardsSchema } from "@/server/routers/contentstack/schemas/blocks/carouselCards" import type { contentSchema } from "@/server/routers/contentstack/schemas/blocks/content" import type { dynamicContentSchema } from "@/server/routers/contentstack/schemas/blocks/dynamicContent" -import type { hotelListingSchema } from "@/server/routers/contentstack/schemas/blocks/hotelListing" +import type { contentPageHotelListingSchema } from "@/server/routers/contentstack/schemas/blocks/hotelListing" import type { shortcutsSchema } from "@/server/routers/contentstack/schemas/blocks/shortcuts" import type { tableSchema } from "@/server/routers/contentstack/schemas/blocks/table" import type { textColsSchema } from "@/server/routers/contentstack/schemas/blocks/textCols" @@ -22,7 +22,8 @@ export interface TableBlock extends z.output {} export type TableData = TableBlock["table"] export interface TextCols extends z.output {} export interface UspGrid extends z.output {} -interface GetHotelListing 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 {} diff --git a/apps/scandic-web/types/trpc/routers/contentstack/campaignPage.ts b/apps/scandic-web/types/trpc/routers/contentstack/campaignPage.ts index c3dd08ae4..af861ca40 100644 --- a/apps/scandic-web/types/trpc/routers/contentstack/campaignPage.ts +++ b/apps/scandic-web/types/trpc/routers/contentstack/campaignPage.ts @@ -1,7 +1,6 @@ import type { z } from "zod" import type { - blocksSchema, campaignPageRefsSchema, campaignPageSchema, heroSchema, @@ -19,7 +18,7 @@ export interface GetCampaignPageRefsData export interface CampaignPageRefs extends z.output {} -export type Block = z.output +export type Block = CampaignPageData["blocks"][number] export type EssentialsBlock = z.output["essentials"] diff --git a/packages/design-system/lib/components/Icons/Logos/DowntownCamper.tsx b/packages/design-system/lib/components/Icons/Logos/DowntownCamper.tsx index 4c5a8aaf9..1f8cf5099 100644 --- a/packages/design-system/lib/components/Icons/Logos/DowntownCamper.tsx +++ b/packages/design-system/lib/components/Icons/Logos/DowntownCamper.tsx @@ -4,6 +4,7 @@ import { iconVariants } from '../variants' export default function DowntownCamperIcon({ className, color, + height = 30, ...props }: LogoProps) { const classNames = iconVariants({ className, color }) @@ -11,7 +12,7 @@ export default function DowntownCamperIcon({ + return } switch (hotelId) { case SignatureHotelEnum.Haymarket: - return + return case SignatureHotelEnum.HotelNorge: - return + return case SignatureHotelEnum.DowntownCamper: - return + return case SignatureHotelEnum.GrandHotelOslo: - return + return case SignatureHotelEnum.Marski: - return + return case SignatureHotelEnum.TheDock: - return + return default: - return + return } }