feat(SW-2708): Meeting package widget mobile UI
Approved-by: Matilda Landström
This commit is contained in:
@@ -59,8 +59,6 @@
|
|||||||
|
|
||||||
.meetingPackageWidget {
|
.meetingPackageWidget {
|
||||||
border-radius: var(--Corner-radius-lg);
|
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;
|
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) {
|
@media (min-width: 1367px) {
|
||||||
.content .contentContainer {
|
.content .contentContainer {
|
||||||
grid-template-areas: "main sidebar";
|
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"
|
import styles from "./skeleton.module.css"
|
||||||
|
|
||||||
|
|||||||
@@ -1,59 +1,100 @@
|
|||||||
"use client"
|
"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"
|
import styles from "./meetingPackageWidget.module.css"
|
||||||
|
|
||||||
const SOURCE =
|
export interface MeetingPackageWidgetProps {
|
||||||
"https://scandic-bookingengine.s3.eu-central-1.amazonaws.com/script_stage.js"
|
|
||||||
|
|
||||||
interface MeetingPackageWidgetProps {
|
|
||||||
destination?: string
|
destination?: string
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MeetingPackageWidget({
|
export default function MeetingPackageWidget(props: MeetingPackageWidgetProps) {
|
||||||
destination,
|
const intl = useIntl()
|
||||||
className,
|
/* Meeting booking widget changes design at 948px */
|
||||||
}: MeetingPackageWidgetProps) {
|
const isDesktop = useMediaQuery("(min-width: 948px)", {
|
||||||
const lang = useLang()
|
initializeWithValue: false,
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
return isDesktop ? (
|
||||||
const script = document.createElement("script")
|
<MeetingPackageWidgetContent {...props} />
|
||||||
script.src = SOURCE
|
) : (
|
||||||
script.setAttribute("langcode", lang)
|
<div className={props.className}>
|
||||||
script.setAttribute("whitelabel_id", "224905")
|
<DialogTrigger>
|
||||||
script.setAttribute("widget_id", "scandic_default_new")
|
<div className={styles.buttonWrapper}>
|
||||||
script.setAttribute("version", "frontpage-scandic")
|
<Button className={styles.button}>
|
||||||
if (destination) {
|
<span className={styles.fakeInput}>
|
||||||
script.setAttribute("destination", destination)
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
}
|
<span>
|
||||||
document.body.appendChild(script)
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Meeting location",
|
||||||
function onLoad() {
|
})}
|
||||||
setIsLoading(false)
|
</span>
|
||||||
}
|
</Typography>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
script.addEventListener("load", onLoad)
|
<span className={styles.fakePlaceholder}>
|
||||||
|
{intl.formatMessage({
|
||||||
return () => {
|
defaultMessage: "Hotels & destinations",
|
||||||
script.removeEventListener("load", onLoad)
|
})}
|
||||||
document.body.removeChild(script)
|
</span>
|
||||||
}
|
</Typography>
|
||||||
}, [destination, lang])
|
</span>
|
||||||
|
<span className={styles.fakeButton}>
|
||||||
return (
|
<MaterialIcon icon="search" color="CurrentColor" />
|
||||||
<div className={className}>
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
{isLoading && <MeetingPackageWidgetSkeleton />}
|
<span>
|
||||||
<div
|
{intl.formatMessage({
|
||||||
id="mp-booking-engine-iframe-container"
|
defaultMessage: "Search",
|
||||||
className={`${styles.widget} ${isLoading ? styles.isLoading : ""}`}
|
})}
|
||||||
/>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,125 @@
|
|||||||
.widget {
|
.buttonWrapper {
|
||||||
width: 100%;
|
padding: var(--Space-x15) var(--Space-x2);
|
||||||
max-width: var(--max-width-page);
|
display: flex;
|
||||||
margin: 0 auto;
|
background-color: var(--Component-Button-Brand-Primary-On-fill-Default);
|
||||||
|
color: var(--Text-Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget.isLoading {
|
.button {
|
||||||
display: none;
|
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 */
|
/* Meeting booking widget changes design at 948px */
|
||||||
@media screen and (min-width: 948px) {
|
@media screen and (min-width: 948px) {
|
||||||
.widget {
|
.overlay {
|
||||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
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
|
// 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
|
// The destination id is used to prefill the correct destination
|
||||||
|
// Last updated: 2025-03-11
|
||||||
export const meetingPackageDestinationByHotelId: Record<string, string> = {
|
export const meetingPackageDestinationByHotelId: Record<string, string> = {
|
||||||
"214": "341161",
|
"214": "341161",
|
||||||
"215": "371906",
|
"215": "371906",
|
||||||
|
|||||||
@@ -5,11 +5,3 @@
|
|||||||
box-shadow: 0px 16px 24px 0px rgba(0, 0, 0, 0.08);
|
box-shadow: 0px 16px 24px 0px rgba(0, 0, 0, 0.08);
|
||||||
z-index: var(--booking-widget-z-index);
|
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