feat(SW-2708): Meeting package widget mobile UI
Approved-by: Matilda Landström
This commit is contained in:
@@ -59,8 +59,6 @@
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -84,6 +82,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Meeting booking widget changes design at 948px */
|
||||
@media screen and (min-width: 948px) {
|
||||
.meetingPackageWidget {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1367px) {
|
||||
.content .contentContainer {
|
||||
grid-template-areas: "main sidebar";
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { cx } from "class-variance-authority"
|
||||
|
||||
import MeetingPackageWidgetSkeleton from "../Skeleton"
|
||||
import { useMeetingPackageWidget } from "../useMeetingPackageWidget"
|
||||
|
||||
import styles from "./content.module.css"
|
||||
|
||||
import type { MeetingPackageWidgetProps } from ".."
|
||||
|
||||
export default function MeetingPackageWidgetContent({
|
||||
destination,
|
||||
className,
|
||||
}: MeetingPackageWidgetProps) {
|
||||
const { isLoading } = useMeetingPackageWidget(destination)
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{isLoading && <MeetingPackageWidgetSkeleton />}
|
||||
<div
|
||||
id="mp-booking-engine-iframe-container"
|
||||
className={cx(styles.widget, { [styles.isLoading]: isLoading })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
.widget {
|
||||
width: min(var(--max-width-page), 100%);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.widget.isLoading {
|
||||
display: none;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import SkeletonShimmer from "../../SkeletonShimmer"
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
|
||||
import styles from "./skeleton.module.css"
|
||||
|
||||
|
||||
@@ -1,59 +1,100 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
} from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
import { useMediaQuery } from "usehooks-ts"
|
||||
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { IconButton } from "@scandic-hotels/design-system/IconButton"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import MeetingPackageWidgetSkeleton from "./Skeleton"
|
||||
import MeetingPackageWidgetContent from "./Content/Content"
|
||||
|
||||
import styles from "./meetingPackageWidget.module.css"
|
||||
|
||||
const SOURCE =
|
||||
"https://scandic-bookingengine.s3.eu-central-1.amazonaws.com/script_stage.js"
|
||||
|
||||
interface MeetingPackageWidgetProps {
|
||||
export interface MeetingPackageWidgetProps {
|
||||
destination?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function MeetingPackageWidget({
|
||||
destination,
|
||||
className,
|
||||
}: MeetingPackageWidgetProps) {
|
||||
const lang = useLang()
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
export default function MeetingPackageWidget(props: MeetingPackageWidgetProps) {
|
||||
const intl = useIntl()
|
||||
/* Meeting booking widget changes design at 948px */
|
||||
const isDesktop = useMediaQuery("(min-width: 948px)", {
|
||||
initializeWithValue: 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.isLoading : ""}`}
|
||||
/>
|
||||
return isDesktop ? (
|
||||
<MeetingPackageWidgetContent {...props} />
|
||||
) : (
|
||||
<div className={props.className}>
|
||||
<DialogTrigger>
|
||||
<div className={styles.buttonWrapper}>
|
||||
<Button className={styles.button}>
|
||||
<span className={styles.fakeInput}>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Meeting location",
|
||||
})}
|
||||
</span>
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<span className={styles.fakePlaceholder}>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Hotels & destinations",
|
||||
})}
|
||||
</span>
|
||||
</Typography>
|
||||
</span>
|
||||
<span className={styles.fakeButton}>
|
||||
<MaterialIcon icon="search" color="CurrentColor" />
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Search",
|
||||
})}
|
||||
</span>
|
||||
</Typography>
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
<ModalOverlay isDismissable className={styles.overlay}>
|
||||
<Modal className={styles.modal}>
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
aria-label={intl.formatMessage({
|
||||
defaultMessage: "Book a meeting",
|
||||
})}
|
||||
>
|
||||
{({ close }) => (
|
||||
<>
|
||||
<div className={styles.closeButtonWrapper}>
|
||||
<IconButton
|
||||
theme="Black"
|
||||
style="Muted"
|
||||
onPress={close}
|
||||
className={styles.closeButton}
|
||||
aria-label={intl.formatMessage({
|
||||
defaultMessage: "Close",
|
||||
})}
|
||||
>
|
||||
<MaterialIcon icon="close" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<MeetingPackageWidgetContent {...props} />
|
||||
</>
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
</DialogTrigger>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,125 @@
|
||||
.widget {
|
||||
width: 100%;
|
||||
max-width: var(--max-width-page);
|
||||
margin: 0 auto;
|
||||
.buttonWrapper {
|
||||
padding: var(--Space-x15) var(--Space-x2);
|
||||
display: flex;
|
||||
background-color: var(--Component-Button-Brand-Primary-On-fill-Default);
|
||||
color: var(--Text-Default);
|
||||
}
|
||||
|
||||
.widget.isLoading {
|
||||
display: none;
|
||||
.button {
|
||||
display: flex;
|
||||
border-width: 0;
|
||||
padding: 0;
|
||||
gap: var(--Space-x1);
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover .fakeButton {
|
||||
background-color: var(--Component-Button-Brand-Primary-Fill-Hover);
|
||||
border-color: var(--Component-Button-Brand-Primary-Border-Hover);
|
||||
color: var(--Component-Button-Brand-Primary-On-fill-Hover);
|
||||
}
|
||||
}
|
||||
|
||||
.fakeInput {
|
||||
display: grid;
|
||||
justify-items: start;
|
||||
padding: var(--Space-x1) var(--Space-x15);
|
||||
background-color: var(--Surface-Primary-OnSurface-Default);
|
||||
border-radius: var(--Corner-radius-md);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.fakePlaceholder {
|
||||
color: var(--Text-Interactive-Placeholder);
|
||||
}
|
||||
|
||||
.fakeButton {
|
||||
border-radius: var(--Corner-radius-rounded);
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--Space-x05);
|
||||
padding: 10px var(--Space-x2);
|
||||
background-color: var(--Component-Button-Brand-Primary-Fill-Default);
|
||||
border-color: var(--Component-Button-Brand-Primary-Border-Default);
|
||||
color: var(--Component-Button-Brand-Primary-On-fill-Default);
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: var(--Overlay-40);
|
||||
|
||||
&[data-entering] {
|
||||
animation: overlay-fade 200ms;
|
||||
}
|
||||
|
||||
&[data-exiting] {
|
||||
animation: overlay-fade 150ms reverse ease-in;
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-radius: var(--Corner-radius-md) var(--Corner-radius-md) 0 0;
|
||||
background-color: var(--Surface-Primary-Default);
|
||||
box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&[data-entering] {
|
||||
animation: modal-anim 200ms;
|
||||
}
|
||||
|
||||
&[data-exiting] {
|
||||
animation: modal-anim 150ms reverse ease-in;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: var(--Space-x2);
|
||||
overflow-y: auto;
|
||||
height: 95dvh;
|
||||
padding: var(--Space-x3) 0;
|
||||
}
|
||||
|
||||
.closeButtonWrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: var(--max-width-page);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Meeting booking widget changes design at 948px */
|
||||
@media screen and (min-width: 948px) {
|
||||
.widget {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
.overlay {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes overlay-fade {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes modal-anim {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
const SOURCE =
|
||||
"https://scandic-bookingengine.s3.eu-central-1.amazonaws.com/script_stage.js"
|
||||
|
||||
export function useMeetingPackageWidget(destination?: string) {
|
||||
const lang = useLang()
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const script = document.createElement("script")
|
||||
script.src = SOURCE
|
||||
script.setAttribute("langcode", lang || "en")
|
||||
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 { isLoading }
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// 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
|
||||
// Last updated: 2025-03-11
|
||||
export const meetingPackageDestinationByHotelId: Record<string, string> = {
|
||||
"214": "341161",
|
||||
"215": "371906",
|
||||
|
||||
@@ -5,11 +5,3 @@
|
||||
box-shadow: 0px 16px 24px 0px rgba(0, 0, 0, 0.08);
|
||||
z-index: var(--booking-widget-z-index);
|
||||
}
|
||||
|
||||
/* Temporary solution to show the Meeting package widget on mobile, but nonsticky */
|
||||
/* Meeting booking widget changes design at 948px */
|
||||
@media screen and (max-width: 947px) {
|
||||
.wrapper {
|
||||
position: unset;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user