Merged in feat/SW-1230-meeting-booking-widget (pull request #1507)

feat(SW-1230): Added meeting booking widget to hotel meeting pages

* feat(SW-1230): Added meeting booking widget to hotel meeting pages


Approved-by: Fredrik Thorsson
This commit is contained in:
Erik Tiekstra
2025-03-11 06:33:04 +00:00
parent 67004e5904
commit 2ef2b2e28d
10 changed files with 387 additions and 15 deletions

View File

@@ -0,0 +1,31 @@
"use client"
import { useRef } from "react"
import { StickyElementNameEnum } from "@/stores/sticky-position"
import MeetingPackageWidget from "@/components/MeetingPackageWidget"
import useStickyPosition from "@/hooks/useStickyPosition"
import styles from "./meetingWidget.module.css"
interface MeetingWidgetProps {
hotelId: string
}
export function MeetingWidget({ hotelId }: MeetingWidgetProps) {
const ref = useRef<HTMLDivElement>(null)
useStickyPosition({
ref,
name: StickyElementNameEnum.MEETING_BOOKING_WIDGET,
})
return (
<div ref={ref} className={styles.wrapper}>
<MeetingPackageWidget
hotelId={hotelId}
className={styles.meetingPackageWidget}
/>
</div>
)
}

View File

@@ -0,0 +1,13 @@
.wrapper {
position: sticky;
top: 0;
background-color: var(--Base-Surface-Primary-light-Normal);
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

@@ -43,12 +43,6 @@
grid-column: 1;
}
.meetingBookingWidget {
padding: var(--Spacing-x4);
background-color: var(--Base-Surface-Primary-dark-Normal);
text-align: center;
}
.buttonContainer {
position: sticky;
padding: var(--Spacing-x3) var(--Spacing-x2);

View File

@@ -20,6 +20,7 @@ import { getLang } from "@/i18n/serverContext"
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"
@@ -55,24 +56,19 @@ export default async function HotelSubpage({
let meetingRooms
if (hotelData.additionalData.meetingRooms.nameInUrl === subpage) {
meetingRooms = await getMeetingRooms({ hotelId: hotelId, language: lang })
meetingRooms = await getMeetingRooms({ hotelId, language: lang })
}
const restaurantButton = restaurants.find(
(restaurant) => restaurant.nameInUrl === subpage
)
const meetingBookingWidget = meetingRooms ? (
<div className={styles.meetingBookingWidget}>
Booking Widget Placeholder
</div>
) : null
return (
<>
<section
className={`${styles.hotelSubpage} ${restaurantButton?.bookTableUrl ? styles.hasStickyButton : ""} `}
>
{meetingRooms && <MeetingWidget hotelId={hotelId} />}
<div className={styles.header}>
<Suspense fallback={<BreadcrumbsSkeleton />}>
<Breadcrumbs
@@ -81,9 +77,8 @@ export default async function HotelSubpage({
/>
</Suspense>
{pageData.heroImage || meetingBookingWidget ? (
{pageData.heroImage ? (
<div className={styles.heroWrapper}>
{meetingBookingWidget}
{pageData.heroImage && (
<Hero
src={pageData.heroImage.src}

View File

@@ -0,0 +1,21 @@
import SkeletonShimmer from "../../SkeletonShimmer"
import styles from "./skeleton.module.css"
export default function MeetingPackageWidgetSkeleton() {
return (
<>
<div className={styles.mobile}>
<SkeletonShimmer height="40px" width="100%" />
</div>
<div className={styles.desktop}>
<SkeletonShimmer height="60px" width="30%" />
<SkeletonShimmer height="60px" width="22%" />
<SkeletonShimmer height="60px" width="12%" />
<SkeletonShimmer height="60px" width="12%" />
<SkeletonShimmer height="60px" width="12%" />
<SkeletonShimmer height="60px" width="12%" />
</div>
</>
)
}

View File

@@ -0,0 +1,20 @@
.mobile,
.desktop {
padding: var(--Spacing-x2) 0;
background-color: var(--Base-Surface-Primary-light-Normal);
}
.desktop {
display: none;
}
/* Meeting booking widget changes design at 948px */
@media screen and (min-width: 948px) {
.mobile {
display: none;
}
.desktop {
display: flex;
gap: var(--Spacing-x2);
}
}

View File

@@ -0,0 +1,54 @@
"use client"
import Script from "next/script"
import { 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"
interface MeetingPackageWidgetProps {
hotelId?: string
className?: string
}
export default function MeetingPackageWidget({
hotelId,
className = "",
}: MeetingPackageWidgetProps) {
const lang = useLang()
const [isLoading, setIsLoading] = useState(true)
const destination = hotelId
? meetingPackageDestinationByHotelId[hotelId]
: undefined
function handleOnReady() {
setIsLoading(false)
}
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}
/>
</div>
)
}

View File

@@ -0,0 +1,16 @@
/* 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);
display: none;
}
/* Meeting booking widget changes design at 948px */
@media screen and (min-width: 948px) {
.widget {
background-color: var(--Base-Surface-Primary-light-Normal);
}
.widget.ready {
display: block;
}
}

View File

@@ -0,0 +1,226 @@
// Hotel ids are used as keys to map to the destination id for the meeting package widget
// The destination id is used to prefill the correct destination
export const meetingPackageDestinationByHotelId: Record<string, string> = {
"214": "341161",
"215": "371906",
"216": "356354",
"217": "371946",
"218": "371911",
"220": "341759",
"245": "402316",
"301": "356868",
"302": "403567",
"303": "575521",
"305": "399893",
"306": "404898",
"307": "357266",
"310": "356877",
"311": "572941",
"312": "357692",
"313": "356963",
"314": "356968",
"315": "357087",
"316": "357057",
"317": "357193",
"318": "575528",
"319": "404303",
"320": "385847",
"321": "357003",
"322": "357242",
"323": "602697",
"325": "386725",
"326": "357286",
"328": "404927",
"329": "357299",
"332": "357013",
"334": "402321",
"337": "357077",
"343": "357251",
"359": "360090",
"360": "600413",
"363": "359332",
"365": "359337",
"367": "572946",
"368": "359343",
"374": "600423",
"380": "602876",
"388": "359359",
"441": "469724",
"442": "469717",
"550": "357675",
"551": "357685",
"554": "357680",
"555": "357578",
"556": "375174",
"557": "427128",
"558": "533019",
"601": "31571",
"603": "356654",
"605": "356693",
"607": "356779",
"608": "356638",
"609": "356643",
"611": "356549",
"615": "356559",
"617": "356797",
"619": "356852",
"622": "356564",
"624": "356666",
"626": "356704",
"628": "356769",
"629": "356889",
"635": "356788",
"637": "356754",
"638": "356994",
"639": "356719",
"640": "356904",
"666": "357035",
"667": "356554",
"668": "356544",
"669": "341634",
"670": "341712",
"672": "356539",
"674": "356576",
"675": "356671",
"676": "356859",
"677": "356845",
"678": "356824",
"679": "356764",
"683": "356899",
"684": "356774",
"686": "356759",
"687": "356749",
"688": "356591",
"689": "356607",
"691": "356625",
"692": "356688",
"693": "356612",
"694": "356744",
"696": "356620",
"697": "356534",
"698": "372158",
"713": "412876",
"715": "357670",
"716": "371922",
"718": "357481",
"719": "357492",
"721": "357486",
"723": "357521",
"724": "357634",
"725": "357506",
"726": "357018",
"728": "357601",
"729": "357101",
"731": "357545",
"732": "357466",
"733": "357500",
"734": "356977",
"735": "356813",
"736": "356987",
"737": "357516",
"738": "357008",
"744": "357526",
"745": "357511",
"746": "356982",
"747": "357043",
"748": "357471",
"749": "357476",
"751": "357092",
"756": "357119",
"757": "403058",
"759": "357198",
"760": "357313",
"764": "357114",
"765": "357159",
"766": "515835",
"770": "357164",
"771": "357230",
"772": "575326",
"773": "357181",
"774": "359353",
"775": "357208",
"776": "357261",
"778": "357279",
"780": "357274",
"781": "357294",
"782": "357307",
"784": "357365",
"786": "357188",
"787": "357225",
"788": "357235",
"789": "428447",
"790": "357256",
"792": "357203",
"793": "357213",
"795": "386272",
"801": "356374",
"802": "341786",
"803": "341875",
"805": "341625",
"806": "356425",
"808": "341722",
"809": "341764",
"810": "341728",
"811": "32347",
"813": "341797",
"816": "356458",
"817": "356496",
"818": "356446",
"822": "341965",
"823": "356359",
"824": "357646",
"826": "341717",
"827": "341983",
"828": "356329",
"829": "360047",
"830": "341742",
"832": "341880",
"834": "356396",
"835": "341662",
"836": "341953",
"838": "401127",
"839": "356334",
"840": "356475",
"841": "356529",
"842": "360018",
"843": "356390",
"844": "342031",
"845": "360027",
"846": "341908",
"847": "356379",
"848": "341902",
"849": "356480",
"850": "356489",
"852": "342002",
"853": "356402",
"854": "341970",
"855": "356344",
"856": "341870",
"858": "356409",
"859": "356339",
"860": "356470",
"861": "356324",
"863": "341895",
"864": "356501",
"865": "341993",
"866": "341978",
"867": "356583",
"868": "356384",
"869": "341942",
"870": "356417",
"871": "356349",
"872": "341913",
"873": "342011",
"875": "401143",
"876": "341885",
"877": "342024",
"878": "356453",
"879": "341865",
"882": "356438",
"883": "356364",
"885": "341988",
"886": "341933",
"887": "341918",
"889": "356596",
"890": "341890",
}

View File

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