feat/SW-1756-meeting-package-content-pages

* feat(SW-1230): Changes to script to be able to reload after soft navigation
* feat(SW-1756): Added meeting package widget on content pages

Approved-by: Matilda Landström
This commit is contained in:
Erik Tiekstra
2025-03-21 13:31:33 +00:00
parent 369cc964f0
commit 91e26e30af
15 changed files with 120 additions and 53 deletions

View File

@@ -8,6 +8,7 @@ import { PageContentTypeEnum } from "@/types/requests/contentType"
const IGNORED_CONTENT_TYPES = [
PageContentTypeEnum.hotelPage,
PageContentTypeEnum.contentPage,
PageContentTypeEnum.destinationCityPage,
PageContentTypeEnum.destinationCountryPage,
]

View File

@@ -11,6 +11,8 @@ import {
import Breadcrumbs from "@/components/Breadcrumbs"
import Hero from "@/components/Hero"
import { meetingPackageDestinationByHotelId } from "@/components/MeetingPackageWidget/utils"
import StickyMeetingPackageWidget from "@/components/StickyMeetingPackageWidget"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import Button from "@/components/TempDesignSystem/Button"
import Divider from "@/components/TempDesignSystem/Divider"
@@ -21,7 +23,6 @@ import { safeTry } from "@/utils/safeTry"
import MeetingsAdditionalContent from "./AdditionalContent/Meetings"
import HotelSubpageAdditionalContent from "./AdditionalContent"
import HtmlContent from "./HtmlContent"
import { MeetingWidget } from "./MeetingWidget"
import HotelSubpageSidebar from "./Sidebar"
import { getSubpageData, verifySubpageShouldExist } from "./utils"
@@ -63,13 +64,18 @@ export default async function HotelSubpage({
const restaurantButton = restaurants.find(
(restaurant) => restaurant.nameInUrl === subpage
)
const meetingPackageDestination = hotelId
? meetingPackageDestinationByHotelId[hotelId]
: undefined
return (
<>
<section
className={`${styles.hotelSubpage} ${restaurantButton?.bookTableUrl ? styles.hasStickyButton : ""} `}
>
{meetingRooms && <MeetingWidget hotelId={hotelId} />}
{meetingRooms && (
<StickyMeetingPackageWidget destination={meetingPackageDestination} />
)}
<div className={styles.header}>
<Suspense fallback={<BreadcrumbsSkeleton />}>
<Breadcrumbs

View File

@@ -1,7 +1,15 @@
import { Suspense } from "react"
import { serverClient } from "@/lib/trpc/server"
import Breadcrumbs from "@/components/Breadcrumbs"
import StickyMeetingPackageWidget from "@/components/StickyMeetingPackageWidget"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import StaticPage from ".."
import { PageContentTypeEnum } from "@/types/requests/contentType"
export default async function ContentPage() {
const contentPageRes = await serverClient().contentstack.contentPage.get()
@@ -12,6 +20,20 @@ export default async function ContentPage() {
const { tracking, contentPage } = contentPageRes
return (
<StaticPage content={contentPage} tracking={tracking} pageType="content" />
<>
{contentPage.meeting_package?.show_widget && (
<StickyMeetingPackageWidget
destination={contentPage.meeting_package.location}
/>
)}
<Suspense fallback={<BreadcrumbsSkeleton />}>
<Breadcrumbs variant={PageContentTypeEnum.contentPage} />
</Suspense>
<StaticPage
content={contentPage}
tracking={tracking}
pageType="content"
/>
</>
)
}

View File

@@ -15,6 +15,7 @@ import { staticPageVariants } from "./variants"
import styles from "./staticPage.module.css"
import MeetingPackageWidget from "@/components/MeetingPackageWidget"
import type { StaticPageProps } from "./staticPage"
export default async function StaticPage({
@@ -22,7 +23,7 @@ export default async function StaticPage({
tracking,
pageType,
}: StaticPageProps) {
const { blocks, hero_image, header } = content
const { blocks, hero_image, header, meeting_package } = content
return (
<>
@@ -68,6 +69,12 @@ export default async function StaticPage({
<div className={styles.contentContainer}>
<main className={styles.mainContent}>
{pageType === "collection" && meeting_package?.show_widget && (
<MeetingPackageWidget
destination={meeting_package.location}
className={styles.meetingPackageWidget}
/>
)}
{blocks ? <Blocks blocks={blocks} /> : null}
</main>

View File

@@ -58,6 +58,13 @@
grid-area: main;
}
.meetingPackageWidget {
border-radius: var(--Corner-radius-lg);
background-color: var(--Base-Surface-Primary-light-Normal);
box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.05);
overflow: hidden;
}
.button {
width: fit-content;
}

View File

@@ -1,11 +1,14 @@
.mobile,
.desktop {
padding: var(--Spacing-x2) 0;
padding: var(--Spacing-x2) var(--Spacing-x2);
background-color: var(--Base-Surface-Primary-light-Normal);
}
.desktop {
display: none;
width: 100%;
max-width: var(--max-width-page);
margin: 0 auto;
}
/* Meeting booking widget changes design at 948px */

View File

@@ -1,53 +1,58 @@
"use client"
import Script from "next/script"
import { useState } from "react"
import { useEffect, useState } from "react"
import useLang from "@/hooks/useLang"
import MeetingPackageWidgetSkeleton from "./Skeleton"
import { meetingPackageDestinationByHotelId } from "./utils"
import styles from "./meetingPackageWidget.module.css"
const SOURCE = "https://bookingengine-mp.s3.eu-west-2.amazonaws.com/script.js"
const SOURCE =
"https://scandic-bookingengine.s3.eu-central-1.amazonaws.com/script_stage.js"
interface MeetingPackageWidgetProps {
hotelId?: string
destination?: string
className?: string
}
export default function MeetingPackageWidget({
hotelId,
className = "",
destination,
className,
}: MeetingPackageWidgetProps) {
const lang = useLang()
const [isLoading, setIsLoading] = useState(true)
const destination = hotelId
? meetingPackageDestinationByHotelId[hotelId]
: undefined
function handleOnReady() {
setIsLoading(false)
}
useEffect(() => {
const script = document.createElement("script")
script.src = SOURCE
script.setAttribute("langcode", lang)
script.setAttribute("whitelabel_id", "224905")
script.setAttribute("widget_id", "scandic_default_new")
script.setAttribute("version", "frontpage-scandic")
if (destination) {
script.setAttribute("destination", destination)
}
document.body.appendChild(script)
function onLoad() {
setIsLoading(false)
}
script.addEventListener("load", onLoad)
return () => {
script.removeEventListener("load", onLoad)
document.body.removeChild(script)
}
}, [destination, lang])
return (
<div className={className}>
{isLoading && <MeetingPackageWidgetSkeleton />}
<div
id="mp-booking-engine-iframe-container"
className={`${styles.widget} ${!isLoading ? styles.ready : ""}`}
/>
<Script
//@ts-expect-error: invalid attributes because of external script
langcode={lang}
destination={destination}
whitelabel_id="224905"
widget_id="scandic_default_new"
version="frontpage-scandic"
src={SOURCE}
onReady={handleOnReady}
className={`${styles.widget} ${isLoading ? styles.isLoading : ""}`}
/>
</div>
)

View File

@@ -1,7 +1,12 @@
/* Hiding widget on mobile for now as the widget is not ready for mobile use at the moment */
.widget {
background-color: var(--Base-Surface-Primary-light-Normal);
width: 100%;
max-width: var(--max-width-page);
margin: 0 auto;
/* Hiding widget on mobile for now as the widget is not ready for mobile use at the moment */
display: none;
}
.widget.isLoading {
display: none;
}
@@ -9,8 +14,6 @@
@media screen and (min-width: 948px) {
.widget {
background-color: var(--Base-Surface-Primary-light-Normal);
}
.widget.ready {
display: block;
}
}

View File

@@ -7,25 +7,24 @@ import { StickyElementNameEnum } from "@/stores/sticky-position"
import MeetingPackageWidget from "@/components/MeetingPackageWidget"
import useStickyPosition from "@/hooks/useStickyPosition"
import styles from "./meetingWidget.module.css"
import styles from "./stickyMeetingPackageWidget.module.css"
interface MeetingWidgetProps {
hotelId: string
interface StickyMeetingPackageWidgetProps {
destination?: string
}
export function MeetingWidget({ hotelId }: MeetingWidgetProps) {
export default function StickyMeetingPackageWidget({
destination,
}: StickyMeetingPackageWidgetProps) {
const ref = useRef<HTMLDivElement>(null)
useStickyPosition({
ref,
name: StickyElementNameEnum.MEETING_BOOKING_WIDGET,
name: StickyElementNameEnum.MEETING_PACKAGE_WIDGET,
})
return (
<div ref={ref} className={styles.wrapper}>
<MeetingPackageWidget
hotelId={hotelId}
className={styles.meetingPackageWidget}
/>
<MeetingPackageWidget destination={destination} />
</div>
)
}

View File

@@ -5,9 +5,3 @@
box-shadow: 0px 16px 24px 0px rgba(0, 0, 0, 0.08);
z-index: var(--booking-widget-z-index);
}
.meetingPackageWidget {
width: 100%;
max-width: var(--max-width-page);
margin: 0 auto;
}

View File

@@ -18,6 +18,10 @@ query GetCollectionPage($locale: String!, $uid: String!) {
...TopPrimaryButton_CollectionPage
...NavigationLinks_CollectionPage
}
meeting_package {
show_widget
location
}
blocks {
__typename
...CardsGrid_CollectionPage

View File

@@ -29,6 +29,10 @@ query GetContentPage($locale: String!, $uid: String!) {
...TopPrimaryButton_ContentPage
...NavigationLinks_ContentPage
}
meeting_package {
show_widget
location
}
blocks {
__typename
}

View File

@@ -7,8 +7,8 @@ import {
cardsGridSchema,
} from "../schemas/blocks/cardsGrid"
import {
dynamicContentRefsSchema,
dynamicContentSchema as blockDynamicContentSchema,
dynamicContentRefsSchema,
} from "../schemas/blocks/dynamicContent"
import {
shortcutsRefsSchema,
@@ -98,6 +98,12 @@ export const collectionPageSchema = z.object({
top_primary_button: topPrimaryButtonSchema,
navigation_links: navigationLinksSchema,
}),
meeting_package: z
.object({
show_widget: z.boolean(),
location: z.string(),
})
.nullable(),
system: systemSchema.merge(
z.object({
created_at: z.string(),

View File

@@ -211,6 +211,12 @@ export const contentPageSchema = z.object({
top_primary_button: topPrimaryButtonSchema,
navigation_links: navigationLinksSchema,
}),
meeting_package: z
.object({
show_widget: z.boolean(),
location: z.string(),
})
.nullable(),
system: systemSchema.merge(
z.object({
created_at: z.string(),

View File

@@ -3,7 +3,7 @@ import { create } from "zustand"
export enum StickyElementNameEnum {
SITEWIDE_ALERT = "SITEWIDE_ALERT",
BOOKING_WIDGET = "BOOKING_WIDGET",
MEETING_BOOKING_WIDGET = "MEETING_BOOKING_WIDGET",
MEETING_PACKAGE_WIDGET = "MEETING_PACKAGE_WIDGET",
HOTEL_TAB_NAVIGATION = "HOTEL_TAB_NAVIGATION",
HOTEL_STATIC_MAP = "HOTEL_STATIC_MAP",
DESTINATION_SIDEBAR = "DESTINATION_SIDEBAR",
@@ -37,7 +37,7 @@ const priorityMap: Record<StickyElementNameEnum, number> = {
[StickyElementNameEnum.HOTEL_TAB_NAVIGATION]: 3,
[StickyElementNameEnum.HOTEL_STATIC_MAP]: 3,
[StickyElementNameEnum.MEETING_BOOKING_WIDGET]: 3,
[StickyElementNameEnum.MEETING_PACKAGE_WIDGET]: 3,
[StickyElementNameEnum.DESTINATION_SIDEBAR]: 3,
}