Merged in feat/SW-716-multiroom-guest-picker (pull request #1084)

feat(SW-716) Added UI to add more rooms in guest/room picker

* feat(SW-716) Added UI to add more rooms in guest/room picker

* feat(SW-716) Renamed GuestRoom Type and updated html structure

* Feat(SW-716): Created a BookingFlowIteration1 folder

* feat(SW-716) Moved forms/bookingwidget to new flow

* feat(SW-716) Added new ENABLE_BOOKING_FLOW_ITERATION_1 and ENABLE_BOOKING_FLOW_ITERATION_2

* feat(SW-716) Re added booking widget into interaction1 of how it looks now in prod

* Revert "feat(SW-716) Re added booking widget into interaction1 of how it looks now in prod"

This reverts commit 9a5514e8e71b1487e610bf64986ca77a538c0023.

* Revert "feat(SW-716) Added new ENABLE_BOOKING_FLOW_ITERATION_1 and ENABLE_BOOKING_FLOW_ITERATION_2"

This reverts commit b00bfc08cb7878d91483220ba3e8322671c145e4.

* Revert "feat(SW-716) Moved forms/bookingwidget to new flow"

This reverts commit 6c81635fe929a71fb3a42d8f174706787d8578ed.

* Revert "Feat(SW-716): Created a BookingFlowIteration1 folder"

This reverts commit db41f1c7fcd8e3adf15713d5d36f0da11e03e3a4.

* feat(SW-716): Added NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE

* feat(SW-716) Readded Tooltip if NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE is true

* feat(SW-716) remove log


Approved-by: Niclas Edenvin
Approved-by: Christian Andolf
This commit is contained in:
Pontus Dreij
2025-01-08 15:09:29 +00:00
parent 85c9ec5b3b
commit b4060d720b
9 changed files with 266 additions and 143 deletions

View File

@@ -4,6 +4,7 @@ import "@scandic-hotels/design-system/style.css"
import Script from "next/script" import Script from "next/script"
import TokenRefresher from "@/components/Auth/TokenRefresher" import TokenRefresher from "@/components/Auth/TokenRefresher"
import BookingWidget from "@/components/BookingWidget"
import CookieBotConsent from "@/components/CookieBot" import CookieBotConsent from "@/components/CookieBot"
import AdobeScript from "@/components/Current/AdobeScript" import AdobeScript from "@/components/Current/AdobeScript"
import Footer from "@/components/Current/Footer" import Footer from "@/components/Current/Footer"

View File

@@ -129,7 +129,7 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
onClick={() => setIsOpen(true)} onClick={() => setIsOpen(true)}
type="button" type="button"
> >
<Body className={styles.body} asChild> <Body className={styles.body} asChild color="uiTextHighContrast">
<span> <span>
{selectedFromDate} - {selectedToDate} {selectedFromDate} - {selectedToDate}
</span> </span>

View File

@@ -10,7 +10,7 @@ const Input = forwardRef<
InputHTMLAttributes<HTMLInputElement> InputHTMLAttributes<HTMLInputElement>
>(function InputComponent(props, ref) { >(function InputComponent(props, ref) {
return ( return (
<Body asChild> <Body asChild color="uiTextHighContrast">
<InputRAC {...props} ref={ref} className={styles.input} /> <InputRAC {...props} ref={ref} className={styles.input} />
</Body> </Body>
) )

View File

@@ -1,61 +1,77 @@
"use client" "use client"
import { useEffect } from "react" import { useCallback, useEffect } from "react"
import { useFormContext, useWatch } from "react-hook-form" import { useFormContext, useWatch } from "react-hook-form"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { env } from "@/env/client"
import { CloseLargeIcon, PlusCircleIcon, PlusIcon } from "../Icons" import { CloseLargeIcon, PlusCircleIcon, PlusIcon } from "../Icons"
import Button from "../TempDesignSystem/Button" import Button from "../TempDesignSystem/Button"
import Divider from "../TempDesignSystem/Divider"
import Subtitle from "../TempDesignSystem/Text/Subtitle"
import { Tooltip } from "../TempDesignSystem/Tooltip" import { Tooltip } from "../TempDesignSystem/Tooltip"
import AdultSelector from "./AdultSelector" import { GuestsRoom } from "./GuestsRoom"
import ChildSelector from "./ChildSelector"
import styles from "./guests-rooms-picker.module.css" import styles from "./guests-rooms-picker.module.css"
import type { BookingWidgetSchema } from "@/types/components/bookingWidget" import type { BookingWidgetSchema } from "@/types/components/bookingWidget"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" import type { TGuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker"
import type { GuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker"
const MAX_ROOMS = 4
interface GuestsRoomsPickerDialogProps {
rooms: TGuestsRoom[]
onClose: () => void
isOverflowed?: boolean // ToDo Remove once Tooltip below is no longer required
}
export default function GuestsRoomsPickerDialog({ export default function GuestsRoomsPickerDialog({
rooms, rooms,
onClose, onClose,
isOverflowed = false, isOverflowed = false,
}: { }: GuestsRoomsPickerDialogProps) {
rooms: GuestsRoom[]
onClose: () => void
isOverflowed?: boolean // ToDo Remove once Tooltip below is no longer required
}) {
const intl = useIntl() const intl = useIntl()
const { getFieldState, trigger, setValue } =
useFormContext<BookingWidgetSchema>()
const roomsValue = useWatch<BookingWidgetSchema, "rooms">({ name: "rooms" })
const addRoomLabel = intl.formatMessage({ id: "Add Room" })
const doneLabel = intl.formatMessage({ id: "Done" }) const doneLabel = intl.formatMessage({ id: "Done" })
const roomLabel = intl.formatMessage({ id: "Room" })
const disabledBookingOptionsHeader = intl.formatMessage({ const disabledBookingOptionsHeader = intl.formatMessage({
id: "Disabled booking options header", id: "Disabled booking options header",
}) })
const disabledBookingOptionsText = intl.formatMessage({ const disabledBookingOptionsText = intl.formatMessage({
id: "Disabled adding room", id: "Disabled adding room",
}) })
const addRoomLabel = intl.formatMessage({ id: "Add Room" })
const { getFieldState, trigger } = useFormContext<BookingWidgetSchema>() const handleClose = useCallback(async () => {
const isValid = await trigger("rooms")
if (isValid) onClose()
}, [trigger, onClose])
const roomsValue = useWatch({ name: "rooms" }) const handleAddRoom = useCallback(() => {
setValue("rooms", [...roomsValue, { adults: 1, child: [] }], {
shouldValidate: true,
})
}, [roomsValue, setValue])
async function handleOnClose() { const handleRemoveRoom = useCallback(
const state = await trigger("rooms") (index: number) => {
if (state) { setValue(
onClose() "rooms",
} roomsValue.filter((_, i) => i !== index),
} { shouldValidate: true }
)
const fieldState = getFieldState("rooms") },
[roomsValue, setValue]
)
// Validate rooms when they change
useEffect(() => { useEffect(() => {
if (fieldState.invalid) { const fieldState = getFieldState("rooms")
trigger("rooms") if (fieldState.invalid) trigger("rooms")
} }, [roomsValue, getFieldState, trigger])
}, [roomsValue, fieldState.invalid, trigger])
const isInvalid = getFieldState("rooms").invalid
const canAddRooms = rooms.length < MAX_ROOMS
return ( return (
<> <>
@@ -65,97 +81,99 @@ export default function GuestsRoomsPickerDialog({
<CloseLargeIcon /> <CloseLargeIcon />
</button> </button>
</header> </header>
<div className={styles.contentContainer}>
{rooms.map((room, index) => {
const currentAdults = room.adults
const currentChildren = room.child
const childrenInAdultsBed =
currentChildren.filter(
(child) => child.bed === ChildBedMapEnum.IN_ADULTS_BED
).length ?? 0
return ( <div className={styles.contentContainer}>
<div className={styles.roomContainer} key={index}> {rooms.map((room, index) => (
<section className={styles.roomDetailsContainer}> <GuestsRoom
<Subtitle type="two" className={styles.roomHeading}> key={index}
{roomLabel} {index + 1} room={room}
</Subtitle> index={index}
<AdultSelector onRemove={handleRemoveRoom}
roomIndex={index} />
currentAdults={currentAdults} ))}
currentChildren={currentChildren}
childrenInAdultsBed={childrenInAdultsBed} {env.NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE ? (
/> <div className={styles.addRoomMobileContainer}>
<ChildSelector <Tooltip
roomIndex={index} heading={disabledBookingOptionsHeader}
currentAdults={currentAdults} text={disabledBookingOptionsText}
currentChildren={currentChildren} position="bottom"
childrenInAdultsBed={childrenInAdultsBed} arrow="left"
/> >
</section>
<Divider color="primaryLightSubtle" />
</div>
)
})}
<div className={styles.addRoomMobileContainer}>
<Tooltip
heading={disabledBookingOptionsHeader}
text={disabledBookingOptionsText}
position="bottom"
arrow="left"
>
{rooms.length < 4 ? (
<Button <Button
intent="text" intent="text"
variant="icon" variant="icon"
wrapping wrapping
disabled
theme="base" theme="base"
fullWidth fullWidth
onPress={handleAddRoom}
disabled
> >
<PlusIcon /> <PlusIcon />
{addRoomLabel} {addRoomLabel}
</Button> </Button>
) : null} </Tooltip>
</Tooltip> </div>
</div> ) : (
canAddRooms && (
<div className={styles.addRoomMobileContainer}>
<Button
intent="text"
variant="icon"
wrapping
theme="base"
fullWidth
onPress={handleAddRoom}
>
<PlusIcon />
{addRoomLabel}
</Button>
</div>
)
)}
</div> </div>
</section> </section>
<footer className={styles.footer}> <footer className={styles.footer}>
<div className={styles.hideOnMobile}> {env.NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE ? (
<Tooltip <div className={styles.hideOnMobile}>
heading={disabledBookingOptionsHeader} <Tooltip
text={disabledBookingOptionsText} heading={disabledBookingOptionsHeader}
position={isOverflowed ? "top" : "bottom"} text={disabledBookingOptionsText}
arrow="left" position={isOverflowed ? "top" : "bottom"}
> arrow="left"
{rooms.length < 4 ? ( >
<Button <Button
intent="text" intent="text"
variant="icon" variant="icon"
wrapping wrapping
disabled
theme="base" theme="base"
disabled
onPress={handleAddRoom}
> >
<PlusCircleIcon /> <PlusCircleIcon />
{addRoomLabel} {addRoomLabel}
</Button> </Button>
) : null} </Tooltip>
</Tooltip> </div>
</div> ) : (
canAddRooms && (
<div className={styles.hideOnMobile}>
<Button
intent="text"
variant="icon"
wrapping
theme="base"
onPress={handleAddRoom}
>
<PlusCircleIcon />
{addRoomLabel}
</Button>
</div>
)
)}
<Button <Button
onPress={handleOnClose} onPress={handleClose}
disabled={getFieldState("rooms").invalid} disabled={isInvalid}
className={styles.hideOnMobile}
intent="tertiary"
theme="base"
size="small"
>
{doneLabel}
</Button>
<Button
onPress={handleOnClose}
disabled={getFieldState("rooms").invalid}
className={styles.hideOnDesktop} className={styles.hideOnDesktop}
intent="tertiary" intent="tertiary"
theme="base" theme="base"
@@ -163,6 +181,16 @@ export default function GuestsRoomsPickerDialog({
> >
{doneLabel} {doneLabel}
</Button> </Button>
<Button
onPress={handleClose}
disabled={isInvalid}
className={styles.hideOnMobile}
intent="tertiary"
theme="base"
size="small"
>
{doneLabel}
</Button>
</footer> </footer>
</> </>
) )

View File

@@ -0,0 +1,72 @@
import { useIntl } from "react-intl"
import { DeleteIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import Divider from "@/components/TempDesignSystem/Divider"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import AdultSelector from "../AdultSelector"
import ChildSelector from "../ChildSelector"
import styles from "../guests-rooms-picker.module.css"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
import type { TGuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker"
export function GuestsRoom({
room,
index,
onRemove,
}: {
room: TGuestsRoom
index: number
onRemove: (index: number) => void
}) {
const intl = useIntl()
const roomLabel = intl.formatMessage({ id: "Room" })
const childrenInAdultsBed = room.child.filter(
(child) => child.bed === ChildBedMapEnum.IN_ADULTS_BED
).length
return (
<div className={styles.roomContainer}>
<section className={styles.roomDetailsContainer}>
<Subtitle type="two" className={styles.roomHeading}>
{roomLabel} {index + 1}
</Subtitle>
<AdultSelector
roomIndex={index}
currentAdults={room.adults}
currentChildren={room.child}
childrenInAdultsBed={childrenInAdultsBed}
/>
<ChildSelector
roomIndex={index}
currentAdults={room.adults}
currentChildren={room.child}
childrenInAdultsBed={childrenInAdultsBed}
/>
{index !== 0 && (
<div className={styles.roomActions}>
<Button
intent="text"
variant="icon"
wrapping
theme="secondaryLight"
onPress={() => onRemove(index)}
size="small"
className={styles.roomActionsButton}
>
<DeleteIcon color="red" />
<span className={styles.roomActionsLabel}>
{intl.formatMessage({ id: "Remove room" })}
</span>
</Button>
</div>
)}
</section>
<Divider color="primaryLightSubtle" />
</div>
)
}

View File

@@ -56,9 +56,25 @@
} }
.footer { .footer {
display: grid; display: flex;
flex-direction: row;
gap: var(--Spacing-x1); gap: var(--Spacing-x1);
grid-template-columns: auto; }
.roomContainer {
padding: var(--Spacing-x2);
}
.roomContainer:last-of-type {
padding-bottom: calc(var(--sticky-button-height) + 20px);
}
.roomActionsButton {
margin-left: auto;
color: var(--Base-Text-Accent);
}
.footer button {
width: 100%;
} }
@media screen and (max-width: 1366px) { @media screen and (max-width: 1366px) {
@@ -71,7 +87,7 @@
.header { .header {
display: grid; display: grid;
grid-area: header; grid-area: header;
padding: var(--Spacing-x3) var(--Spacing-x2); padding: var(--Spacing-x3) var(--Spacing-x2) 0;
} }
.close { .close {
@@ -83,13 +99,6 @@
padding: 0; padding: 0;
} }
.roomContainer {
padding: 0 var(--Spacing-x2);
}
.roomContainer:last-of-type {
padding-bottom: calc(var(--sticky-button-height) + 20px);
}
.footer { .footer {
background: linear-gradient( background: linear-gradient(
180deg, 180deg,
@@ -125,6 +134,17 @@
grid-template-rows: auto; grid-template-rows: auto;
} }
.roomContainer {
padding: var(--Spacing-x2) 0 0 0;
}
.roomContainer:first-of-type {
padding-top: 0;
}
.roomContainer:last-of-type {
padding-bottom: 0;
}
.contentContainer { .contentContainer {
overflow-y: visible; overflow-y: visible;
} }
@@ -163,6 +183,11 @@
padding-top: var(--Spacing-x2); padding-top: var(--Spacing-x2);
} }
.footer button {
margin-left: auto;
width: 125px;
}
.footer .hideOnDesktop, .footer .hideOnDesktop,
.addRoomMobileContainer { .addRoomMobileContainer {
display: none; display: none;

View File

@@ -18,11 +18,11 @@ import PickerForm from "./Form"
import styles from "./guests-rooms-picker.module.css" import styles from "./guests-rooms-picker.module.css"
import type { GuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker" import type { TGuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker"
export default function GuestsRoomsPickerForm() { export default function GuestsRoomsPickerForm() {
const { watch, trigger } = useFormContext() const { watch, trigger } = useFormContext()
const rooms = watch("rooms") as GuestsRoom[] const rooms = watch("rooms") as TGuestsRoom[]
const checkIsDesktop = useMediaQuery("(min-width: 1367px)") const checkIsDesktop = useMediaQuery("(min-width: 1367px)")
const [isDesktop, setIsDesktop] = useState(true) const [isDesktop, setIsDesktop] = useState(true)
@@ -83,10 +83,10 @@ export default function GuestsRoomsPickerForm() {
}, [containerHeight]) }, [containerHeight])
useEffect(() => { useEffect(() => {
if (typeof window !== undefined && isDesktop) { if (typeof window !== undefined && isDesktop && rooms.length > 0) {
updateHeight() updateHeight()
} }
}, [childCount, isDesktop, updateHeight]) }, [childCount, isDesktop, updateHeight, rooms])
return isDesktop ? ( return isDesktop ? (
<DialogTrigger onOpenChange={setOverflowClip} isOpen={isOpen}> <DialogTrigger onOpenChange={setOverflowClip} isOpen={isOpen}>
@@ -104,13 +104,7 @@ export default function GuestsRoomsPickerForm() {
style={containerHeight ? { overflow: "auto" } : {}} style={containerHeight ? { overflow: "auto" } : {}}
> >
<Dialog className={styles.pickerContainerDesktop}> <Dialog className={styles.pickerContainerDesktop}>
{({ close }) => ( {({ close }) => <PickerForm rooms={rooms} onClose={close} />}
<PickerForm
rooms={rooms}
onClose={close}
isOverflowed={!!containerHeight}
/>
)}
</Dialog> </Dialog>
</Popover> </Popover>
</DialogTrigger> </DialogTrigger>
@@ -137,7 +131,7 @@ function Trigger({
className, className,
triggerFn, triggerFn,
}: { }: {
rooms: GuestsRoom[] rooms: TGuestsRoom[]
className: string className: string
triggerFn?: () => void triggerFn?: () => void
}) { }) {
@@ -149,27 +143,30 @@ function Trigger({
type="button" type="button"
onPress={triggerFn} onPress={triggerFn}
> >
<Body> <Body color="uiTextHighContrast">
{rooms.map((room, i) => ( <span>
<span key={i}> {intl.formatMessage(
{intl.formatMessage( { id: "booking.rooms" },
{ id: "booking.rooms" }, { totalRooms: rooms.length }
{ totalRooms: rooms.length } )}
)} {", "}
{", "} {intl.formatMessage(
{intl.formatMessage( { id: "booking.adults" },
{ id: "booking.adults" }, { totalAdults: rooms.reduce((acc, room) => acc + room.adults, 0) }
{ totalAdults: room.adults } )}
)} {rooms.some((room) => room.child.length > 0)
{room.child.length > 0 ? ", " +
? ", " + intl.formatMessage(
intl.formatMessage( { id: "booking.children" },
{ id: "booking.children" }, {
{ totalChildren: room.child.length } totalChildren: rooms.reduce(
) (acc, room) => acc + room.child.length,
: null} 0
</span> ),
))} }
)
: null}
</span>
</Body> </Body>
</Button> </Button>
) )

View File

@@ -8,7 +8,7 @@ export type Child = {
bed: number bed: number
} }
export type GuestsRoom = { export type TGuestsRoom = {
adults: number adults: number
child: Child[] child: Child[]
} }

View File

@@ -4,7 +4,7 @@ import type { z } from "zod"
import type { Locations } from "@/types/trpc/routers/hotel/locations" import type { Locations } from "@/types/trpc/routers/hotel/locations"
import type { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema" import type { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema"
import type { bookingWidgetVariants } from "@/components/Forms/BookingWidget/variants" import type { bookingWidgetVariants } from "@/components/Forms/BookingWidget/variants"
import type { GuestsRoom } from "./guestsRoomsPicker" import type { TGuestsRoom } from "./guestsRoomsPicker"
export type BookingWidgetSchema = z.output<typeof bookingWidgetSchema> export type BookingWidgetSchema = z.output<typeof bookingWidgetSchema>
@@ -13,7 +13,7 @@ export type BookingWidgetSearchParams = {
hotel?: string hotel?: string
fromDate?: string fromDate?: string
toDate?: string toDate?: string
room?: GuestsRoom[] room?: TGuestsRoom[]
} }
export type BookingWidgetType = VariantProps< export type BookingWidgetType = VariantProps<