fix: remove guest picker store

This commit is contained in:
Christel Westerberg
2024-11-12 16:45:25 +01:00
parent 58678244fc
commit ca3819f7cc
17 changed files with 368 additions and 597 deletions

View File

@@ -114,6 +114,8 @@
/* Z-INDEX */ /* Z-INDEX */
--header-z-index: 11; --header-z-index: 11;
--menu-overlay-z-index: 11; --menu-overlay-z-index: 11;
--booking-widget-z-index: 10;
--booking-widget-open-z-index: 100;
--dialog-z-index: 9; --dialog-z-index: 9;
--sidepeek-z-index: 100; --sidepeek-z-index: 100;
--lightbox-z-index: 150; --lightbox-z-index: 150;

View File

@@ -36,7 +36,6 @@ export default function BookingWidgetClient({
name: StickyElementNameEnum.BOOKING_WIDGET, name: StickyElementNameEnum.BOOKING_WIDGET,
}) })
const bookingWidgetSearchData: BookingWidgetSearchParams | undefined = const bookingWidgetSearchData: BookingWidgetSearchParams | undefined =
searchParams searchParams
? (getFormattedUrlQueryParams(new URLSearchParams(searchParams), { ? (getFormattedUrlQueryParams(new URLSearchParams(searchParams), {
@@ -79,9 +78,7 @@ export default function BookingWidgetClient({
const methods = useForm<BookingWidgetSchema>({ const methods = useForm<BookingWidgetSchema>({
defaultValues: { defaultValues: {
search: selectedLocation?.name ?? "", search: selectedLocation?.name ?? "",
location: selectedLocation location: selectedLocation ? JSON.stringify(selectedLocation) : undefined,
? JSON.stringify(selectedLocation)
: undefined,
date: { date: {
// UTC is required to handle requests from far away timezones https://scandichotels.atlassian.net/browse/SWAP-6375 & PET-507 // UTC is required to handle requests from far away timezones https://scandichotels.atlassian.net/browse/SWAP-6375 & PET-507
// This is specifically to handle timezones falling in different dates. // This is specifically to handle timezones falling in different dates.
@@ -147,29 +144,34 @@ export default function BookingWidgetClient({
? JSON.parse(sessionStorageSearchData) ? JSON.parse(sessionStorageSearchData)
: undefined : undefined
!(selectedLocation?.name) && initialSelectedLocation?.name && !selectedLocation?.name &&
initialSelectedLocation?.name &&
methods.setValue("search", initialSelectedLocation.name) methods.setValue("search", initialSelectedLocation.name)
!selectedLocation && sessionStorageSearchData && !selectedLocation &&
sessionStorageSearchData &&
methods.setValue("location", encodeURIComponent(sessionStorageSearchData)) methods.setValue("location", encodeURIComponent(sessionStorageSearchData))
}, [methods, selectedLocation]) }, [methods, selectedLocation])
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>
<section ref={bookingWidgetRef} className={styles.containerDesktop}> <section
<Form locations={locations} type={type} /> ref={bookingWidgetRef}
</section> className={styles.wrapper}
<section className={styles.containerMobile} data-open={isOpen}> data-open={isOpen}
<button >
className={styles.close} <MobileToggleButton openMobileSearch={openMobileSearch} />
onClick={closeMobileSearch} <div className={styles.formContainer}>
type="button" <button
> className={styles.close}
<CloseLargeIcon /> onClick={closeMobileSearch}
</button> type="button"
<Form locations={locations} type={type} /> >
<CloseLargeIcon />
</button>
<Form locations={locations} type={type} />
</div>
</section> </section>
<div className={styles.backdrop} onClick={closeMobileSearch} /> <div className={styles.backdrop} onClick={closeMobileSearch} />
<MobileToggleButton openMobileSearch={openMobileSearch} />
</FormProvider> </FormProvider>
) )
} }

View File

@@ -6,8 +6,6 @@
display: grid; display: grid;
gap: var(--Spacing-x-one-and-half); gap: var(--Spacing-x-one-and-half);
padding: var(--Spacing-x2); padding: var(--Spacing-x2);
position: sticky;
top: 0;
z-index: 1; z-index: 1;
background-color: var(--Base-Surface-Primary-light-Normal); background-color: var(--Base-Surface-Primary-light-Normal);
} }

View File

@@ -31,12 +31,6 @@ export default function MobileToggleButton({
const location = useWatch({ name: "location" }) const location = useWatch({ name: "location" })
const rooms: BookingWidgetSchema["rooms"] = useWatch({ name: "rooms" }) const rooms: BookingWidgetSchema["rooms"] = useWatch({ name: "rooms" })
const bookingWidgetMobileRef = useRef(null)
useStickyPosition({
ref: bookingWidgetMobileRef,
name: StickyElementNameEnum.BOOKING_WIDGET_MOBILE,
})
const parsedLocation: Location | null = location const parsedLocation: Location | null = location
? JSON.parse(decodeURIComponent(location)) ? JSON.parse(decodeURIComponent(location))
: null : null
@@ -75,7 +69,6 @@ export default function MobileToggleButton({
className={locationAndDateIsSet ? styles.complete : styles.partial} className={locationAndDateIsSet ? styles.complete : styles.partial}
onClick={openMobileSearch} onClick={openMobileSearch}
role="button" role="button"
ref={bookingWidgetMobileRef}
> >
{!locationAndDateIsSet && ( {!locationAndDateIsSet && (
<> <>

View File

@@ -1,60 +1,62 @@
.containerDesktop, .wrapper {
.containerMobile, position: sticky;
.close { z-index: var(--booking-widget-z-index);
display: none;
} }
@media screen and (max-width: 767px) { .formContainer {
.containerMobile { display: grid;
background-color: var(--UI-Input-Controls-Surface-Normal); grid-template-rows: 36px 1fr;
bottom: -100%; background-color: var(--UI-Input-Controls-Surface-Normal);
display: grid; border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0;
gap: var(--Spacing-x3); gap: var(--Spacing-x3);
grid-template-rows: 36px 1fr; height: calc(100dvh - 20px);
height: calc(100dvh - 20px); width: 100%;
padding: var(--Spacing-x3) var(--Spacing-x2) var(--Spacing-x7); padding: var(--Spacing-x3) var(--Spacing-x2) var(--Spacing-x7);
position: fixed; position: fixed;
transition: bottom 300ms ease; bottom: -100%;
width: 100%; transition: bottom 300ms ease;
z-index: 10000; }
border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0;
}
.containerMobile[data-open="true"] { .wrapper[data-open="true"] {
bottom: 0; z-index: var(--booking-widget-open-z-index);
} }
.close { .wrapper[data-open="true"] .formContainer {
background: none; bottom: 0;
border: none; }
cursor: pointer;
justify-self: flex-end;
}
.containerMobile[data-open="true"] + .backdrop { .close {
background-color: rgba(0, 0, 0, 0.4); background: none;
height: 100%; border: none;
left: 0; cursor: pointer;
position: absolute; justify-self: flex-end;
top: 0; }
width: 100%;
z-index: 1000; .wrapper[data-open="true"] + .backdrop {
} background-color: rgba(0, 0, 0, 0.4);
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
z-index: calc(var(--booking-widget-open-z-index) - 1);
} }
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
.containerDesktop { .wrapper {
display: block;
box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.05);
position: sticky;
top: 0; top: 0;
z-index: 10;
background-color: var(--Base-Surface-Primary-light-Normal);
} }
}
@media screen and (min-width: 1367px) { .formContainer {
.container { display: block;
z-index: 9; background-color: var(--Base-Surface-Primary-light-Normal);
box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.05);
height: auto;
position: static;
padding: 0;
}
.close {
display: none;
} }
} }

View File

@@ -1,5 +1,4 @@
"use client" "use client"
import { useState } from "react"
import { useWatch } from "react-hook-form" import { useWatch } from "react-hook-form"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
@@ -7,7 +6,6 @@ import { dt } from "@/lib/dt"
import DatePicker from "@/components/DatePicker" import DatePicker from "@/components/DatePicker"
import GuestsRoomsPickerForm from "@/components/GuestsRoomsPicker" import GuestsRoomsPickerForm from "@/components/GuestsRoomsPicker"
import GuestsRoomsProvider from "@/components/GuestsRoomsPicker/Provider/GuestsRoomsProvider"
import { SearchIcon } from "@/components/Icons" import { SearchIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
@@ -26,12 +24,10 @@ export default function FormContent({
const intl = useIntl() const intl = useIntl()
const selectedDate = useWatch({ name: "date" }) const selectedDate = useWatch({ name: "date" })
const rooms = intl.formatMessage({ id: "Guests & Rooms" }) const roomsLabel = intl.formatMessage({ id: "Guests & Rooms" })
const nights = dt(selectedDate.toDate).diff(dt(selectedDate.fromDate), "days") const nights = dt(selectedDate.toDate).diff(dt(selectedDate.fromDate), "days")
const selectedGuests = useWatch({ name: "rooms" })
return ( return (
<> <>
<div className={styles.input}> <div className={styles.input}>
@@ -51,12 +47,10 @@ export default function FormContent({
<div className={styles.rooms}> <div className={styles.rooms}>
<label> <label>
<Caption color="red" type="bold" asChild> <Caption color="red" type="bold" asChild>
<span>{rooms}</span> <span>{roomsLabel}</span>
</Caption> </Caption>
</label> </label>
<GuestsRoomsProvider selectedGuests={selectedGuests}> <GuestsRoomsPickerForm />
<GuestsRoomsPickerForm name="rooms" />
</GuestsRoomsProvider>
</div> </div>
</div> </div>
<div className={styles.voucherContainer}> <div className={styles.voucherContainer}>

View File

@@ -3,8 +3,6 @@
import { useFormContext } from "react-hook-form" import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { useGuestsRoomsStore } from "@/stores/guests-rooms"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import Counter from "../Counter" import Counter from "../Counter"
@@ -13,39 +11,37 @@ import styles from "./adult-selector.module.css"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
import { import {
AdultSelectorProps,
Child, Child,
SelectorProps,
} from "@/types/components/bookingWidget/guestsRoomsPicker" } from "@/types/components/bookingWidget/guestsRoomsPicker"
export default function AdultSelector({ roomIndex = 0 }: AdultSelectorProps) { export default function AdultSelector({
roomIndex = 0,
currentAdults,
currentChildren,
childrenInAdultsBed,
}: SelectorProps) {
const intl = useIntl() const intl = useIntl()
const adultsLabel = intl.formatMessage({ id: "Adults" }) const adultsLabel = intl.formatMessage({ id: "Adults" })
const { setValue } = useFormContext() const { setValue } = useFormContext()
const { adults, child, childrenInAdultsBed } = useGuestsRoomsStore(
(state) => state.rooms[roomIndex]
)
const increaseAdults = useGuestsRoomsStore((state) => state.increaseAdults)
const decreaseAdults = useGuestsRoomsStore((state) => state.decreaseAdults)
function increaseAdultsCount(roomIndex: number) { function increaseAdultsCount(roomIndex: number) {
if (adults < 6) { if (currentAdults < 6) {
increaseAdults(roomIndex) setValue(`rooms.${roomIndex}.adults`, currentAdults + 1)
setValue(`rooms.${roomIndex}.adults`, adults + 1)
} }
} }
function decreaseAdultsCount(roomIndex: number) { function decreaseAdultsCount(roomIndex: number) {
if (adults > 1) { if (currentAdults > 1) {
decreaseAdults(roomIndex) setValue(`rooms.${roomIndex}.adults`, currentAdults - 1)
setValue(`rooms.${roomIndex}.adults`, adults - 1) if (childrenInAdultsBed > currentAdults) {
if (childrenInAdultsBed > adults) { const toUpdateIndex = currentChildren.findIndex(
const toUpdateIndex = child.findIndex(
(child: Child) => child.bed == ChildBedMapEnum.IN_ADULTS_BED (child: Child) => child.bed == ChildBedMapEnum.IN_ADULTS_BED
) )
if (toUpdateIndex != -1) { if (toUpdateIndex != -1) {
setValue( setValue(
`rooms.${roomIndex}.children.${toUpdateIndex}.bed`, `rooms.${roomIndex}.children.${toUpdateIndex}.bed`,
child[toUpdateIndex].age < 3 currentChildren[toUpdateIndex].age < 3
? ChildBedMapEnum.IN_CRIB ? ChildBedMapEnum.IN_CRIB
: ChildBedMapEnum.IN_EXTRA_BED : ChildBedMapEnum.IN_EXTRA_BED
) )
@@ -60,15 +56,15 @@ export default function AdultSelector({ roomIndex = 0 }: AdultSelectorProps) {
{adultsLabel} {adultsLabel}
</Caption> </Caption>
<Counter <Counter
count={adults} count={currentAdults}
handleOnDecrease={() => { handleOnDecrease={() => {
decreaseAdultsCount(roomIndex) decreaseAdultsCount(roomIndex)
}} }}
handleOnIncrease={() => { handleOnIncrease={() => {
increaseAdultsCount(roomIndex) increaseAdultsCount(roomIndex)
}} }}
disableDecrease={adults == 1} disableDecrease={currentAdults == 1}
disableIncrease={adults == 6} disableIncrease={currentAdults == 6}
/> />
</section> </section>
) )

View File

@@ -3,8 +3,6 @@
import { useFormContext } from "react-hook-form" import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { useGuestsRoomsStore } from "@/stores/guests-rooms"
import { ErrorCircleIcon } from "@/components/Icons" import { ErrorCircleIcon } from "@/components/Icons"
import Select from "@/components/TempDesignSystem/Select" import Select from "@/components/TempDesignSystem/Select"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
@@ -19,6 +17,8 @@ import {
export default function ChildInfoSelector({ export default function ChildInfoSelector({
child = { age: -1, bed: -1 }, child = { age: -1, bed: -1 },
childrenInAdultsBed,
adults,
index = 0, index = 0,
roomIndex = 0, roomIndex = 0,
}: ChildInfoSelectorProps) { }: ChildInfoSelectorProps) {
@@ -26,23 +26,7 @@ export default function ChildInfoSelector({
const ageLabel = intl.formatMessage({ id: "Age" }) const ageLabel = intl.formatMessage({ id: "Age" })
const ageReqdErrMsg = intl.formatMessage({ id: "Child age is required" }) const ageReqdErrMsg = intl.formatMessage({ id: "Child age is required" })
const bedLabel = intl.formatMessage({ id: "Bed" }) const bedLabel = intl.formatMessage({ id: "Bed" })
const { setValue } = useFormContext() const { setValue, formState } = useFormContext()
const { adults, childrenInAdultsBed } = useGuestsRoomsStore(
(state) => state.rooms[roomIndex]
)
const {
isValidated,
updateChildAge,
updateChildBed,
increaseChildInAdultsBed,
decreaseChildInAdultsBed,
} = useGuestsRoomsStore((state) => ({
isValidated: state.isValidated,
updateChildAge: state.updateChildAge,
updateChildBed: state.updateChildBed,
increaseChildInAdultsBed: state.increaseChildInAdultsBed,
decreaseChildInAdultsBed: state.decreaseChildInAdultsBed,
}))
const ageList = Array.from(Array(13).keys()).map((age) => ({ const ageList = Array.from(Array(13).keys()).map((age) => ({
label: `${age}`, label: `${age}`,
@@ -50,7 +34,6 @@ export default function ChildInfoSelector({
})) }))
function updateSelectedAge(age: number) { function updateSelectedAge(age: number) {
updateChildAge(age, roomIndex, index)
setValue(`rooms.${roomIndex}.child.${index}.age`, age, { setValue(`rooms.${roomIndex}.child.${index}.age`, age, {
shouldValidate: true, shouldValidate: true,
}) })
@@ -59,12 +42,6 @@ export default function ChildInfoSelector({
} }
function updateSelectedBed(bed: number) { function updateSelectedBed(bed: number) {
if (bed == ChildBedMapEnum.IN_ADULTS_BED) {
increaseChildInAdultsBed(roomIndex)
} else if (child.bed == ChildBedMapEnum.IN_ADULTS_BED) {
decreaseChildInAdultsBed(roomIndex)
}
updateChildBed(bed, roomIndex, index)
setValue(`rooms.${roomIndex}.child.${index}.bed`, bed) setValue(`rooms.${roomIndex}.child.${index}.bed`, bed)
} }
@@ -97,6 +74,7 @@ export default function ChildInfoSelector({
return availableBedTypes return availableBedTypes
} }
console.log("ALL TJHE ERORRORS", formState.errors)
return ( return (
<> <>
<div key={index} className={styles.childInfoContainer}> <div key={index} className={styles.childInfoContainer}>
@@ -131,12 +109,13 @@ export default function ChildInfoSelector({
) : null} ) : null}
</div> </div>
</div> </div>
{isValidated && child.age < 0 ? (
{/* {isValidated && child.age < 0 ? (
<Caption color="red" className={styles.error}> <Caption color="red" className={styles.error}>
<ErrorCircleIcon color="red" /> <ErrorCircleIcon color="red" />
{ageReqdErrMsg} {ageReqdErrMsg}
</Caption> </Caption>
) : null} ) : null} */}
</> </>
) )
} }

View File

@@ -3,8 +3,6 @@
import { useFormContext } from "react-hook-form" import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { useGuestsRoomsStore } from "@/stores/guests-rooms"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import Counter from "../Counter" import Counter from "../Counter"
@@ -13,25 +11,22 @@ import ChildInfoSelector from "./ChildInfoSelector"
import styles from "./child-selector.module.css" import styles from "./child-selector.module.css"
import { BookingWidgetSchema } from "@/types/components/bookingWidget" import { BookingWidgetSchema } from "@/types/components/bookingWidget"
import { ChildSelectorProps } from "@/types/components/bookingWidget/guestsRoomsPicker" import { SelectorProps } from "@/types/components/bookingWidget/guestsRoomsPicker"
export default function ChildSelector({ roomIndex = 0 }: ChildSelectorProps) { export default function ChildSelector({
roomIndex = 0,
currentAdults,
childrenInAdultsBed,
currentChildren,
}: SelectorProps) {
const intl = useIntl() const intl = useIntl()
const childrenLabel = intl.formatMessage({ id: "Children" }) const childrenLabel = intl.formatMessage({ id: "Children" })
const { setValue, trigger } = useFormContext<BookingWidgetSchema>() const { setValue } = useFormContext<BookingWidgetSchema>()
const children = useGuestsRoomsStore((state) => state.rooms[roomIndex].child)
const increaseChildren = useGuestsRoomsStore(
(state) => state.increaseChildren
)
const decreaseChildren = useGuestsRoomsStore(
(state) => state.decreaseChildren
)
function increaseChildrenCount(roomIndex: number) { function increaseChildrenCount(roomIndex: number) {
if (children.length < 5) { if (currentChildren.length < 5) {
increaseChildren(roomIndex)
setValue( setValue(
`rooms.${roomIndex}.child.${children.length}`, `rooms.${roomIndex}.child.${currentChildren.length}`,
{ {
age: -1, age: -1,
bed: -1, bed: -1,
@@ -41,9 +36,9 @@ export default function ChildSelector({ roomIndex = 0 }: ChildSelectorProps) {
} }
} }
function decreaseChildrenCount(roomIndex: number) { function decreaseChildrenCount(roomIndex: number) {
if (children.length > 0) { if (currentChildren.length > 0) {
const newChildrenList = decreaseChildren(roomIndex) currentChildren.pop()
setValue(`rooms.${roomIndex}.child`, newChildrenList, { setValue(`rooms.${roomIndex}.child`, currentChildren, {
shouldValidate: true, shouldValidate: true,
}) })
} }
@@ -56,23 +51,25 @@ export default function ChildSelector({ roomIndex = 0 }: ChildSelectorProps) {
{childrenLabel} {childrenLabel}
</Caption> </Caption>
<Counter <Counter
count={children.length} count={currentChildren.length}
handleOnDecrease={() => { handleOnDecrease={() => {
decreaseChildrenCount(roomIndex) decreaseChildrenCount(roomIndex)
}} }}
handleOnIncrease={() => { handleOnIncrease={() => {
increaseChildrenCount(roomIndex) increaseChildrenCount(roomIndex)
}} }}
disableDecrease={children.length == 0} disableDecrease={currentChildren.length == 0}
disableIncrease={children.length == 5} disableIncrease={currentChildren.length == 5}
/> />
</section> </section>
{children.map((child, index) => ( {currentChildren.map((child, index) => (
<ChildInfoSelector <ChildInfoSelector
roomIndex={roomIndex} roomIndex={roomIndex}
index={index} index={index}
child={child} child={child}
adults={currentAdults}
key={"child_" + index} key={"child_" + index}
childrenInAdultsBed={childrenInAdultsBed}
/> />
))} ))}
</> </>

View File

@@ -1,130 +0,0 @@
"use client"
import { Dialog } from "react-aria-components"
import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import { useGuestsRoomsStore } from "@/stores/guests-rooms"
import { CloseLargeIcon, PlusCircleIcon, PlusIcon } from "../Icons"
import Button from "../TempDesignSystem/Button"
import Divider from "../TempDesignSystem/Divider"
import Subtitle from "../TempDesignSystem/Text/Subtitle"
import { Tooltip } from "../TempDesignSystem/Tooltip"
import AdultSelector from "./AdultSelector"
import ChildSelector from "./ChildSelector"
import styles from "./guests-rooms-picker.module.css"
import { BookingWidgetSchema } from "@/types/components/bookingWidget"
export default function GuestsRoomsPickerDialog() {
const intl = useIntl()
const doneLabel = intl.formatMessage({ id: "Done" })
const roomLabel = intl.formatMessage({ id: "Room" })
const disabledBookingOptionsHeader = intl.formatMessage({
id: "Disabled booking options header",
})
const disabledBookingOptionsText = intl.formatMessage({
id: "Disabled adding room",
})
const addRoomLabel = intl.formatMessage({ id: "Add Room" })
const { getFieldState } = useFormContext<BookingWidgetSchema>()
const rooms = useGuestsRoomsStore((state) => state.rooms)
return (
<Dialog className={styles.pickerContainer}>
{({ close }) => {
return (
<>
<header className={styles.header}>
<button type="button" className={styles.close}>
<CloseLargeIcon />
</button>
</header>
<div className={styles.contentContainer}>
{rooms.map((room, index) => (
<div className={styles.roomContainer} key={index}>
<section className={styles.roomDetailsContainer}>
<Subtitle type="two" className={styles.roomHeading}>
{roomLabel} {index + 1}
</Subtitle>
<AdultSelector roomIndex={index} />
<ChildSelector roomIndex={index} />
</section>
<Divider color="primaryLightSubtle" />
</div>
))}
<div className={styles.addRoomMobileContainer}>
<Tooltip
heading={disabledBookingOptionsHeader}
text={disabledBookingOptionsText}
position="top"
arrow="left"
>
{rooms.length < 4 ? (
<Button
intent="text"
variant="icon"
wrapping
disabled
theme="base"
fullWidth
>
<PlusIcon />
{addRoomLabel}
</Button>
) : null}
</Tooltip>
</div>
</div>
<footer className={styles.footer}>
<div className={styles.hideOnMobile}>
<Tooltip
heading={disabledBookingOptionsHeader}
text={disabledBookingOptionsText}
position="top"
arrow="left"
>
{rooms.length < 4 ? (
<Button
intent="text"
variant="icon"
wrapping
disabled
theme="base"
>
<PlusCircleIcon />
{addRoomLabel}
</Button>
) : null}
</Tooltip>
</div>
<Button
onPress={close}
disabled={getFieldState("rooms").invalid}
className={styles.hideOnMobile}
intent="tertiary"
theme="base"
size="small"
>
{doneLabel}
</Button>
<Button
onPress={close}
disabled={getFieldState("rooms").invalid}
className={styles.hideOnDesktop}
intent="tertiary"
theme="base"
size="large"
>
{doneLabel}
</Button>
</footer>
</>
)
}}
</Dialog>
)
}

View File

@@ -0,0 +1,146 @@
"use client"
import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import { CloseLargeIcon, PlusCircleIcon, PlusIcon } from "../Icons"
import Button from "../TempDesignSystem/Button"
import Divider from "../TempDesignSystem/Divider"
import Subtitle from "../TempDesignSystem/Text/Subtitle"
import { Tooltip } from "../TempDesignSystem/Tooltip"
import AdultSelector from "./AdultSelector"
import ChildSelector from "./ChildSelector"
import styles from "./guests-rooms-picker.module.css"
import { BookingWidgetSchema } from "@/types/components/bookingWidget"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
import { GuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker"
export default function GuestsRoomsPickerDialog({
rooms,
onClose,
}: {
rooms: GuestsRoom[]
onClose: () => void
}) {
const intl = useIntl()
const doneLabel = intl.formatMessage({ id: "Done" })
const roomLabel = intl.formatMessage({ id: "Room" })
const disabledBookingOptionsHeader = intl.formatMessage({
id: "Disabled booking options header",
})
const disabledBookingOptionsText = intl.formatMessage({
id: "Disabled adding room",
})
const addRoomLabel = intl.formatMessage({ id: "Add Room" })
const { getFieldState } = useFormContext<BookingWidgetSchema>()
return (
<>
<header className={styles.header}>
<button type="button" className={styles.close} onClick={onClose}>
<CloseLargeIcon />
</button>
</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
return (
<div className={styles.roomContainer} key={index}>
<section className={styles.roomDetailsContainer}>
<Subtitle type="two" className={styles.roomHeading}>
{roomLabel} {index + 1}
</Subtitle>
<AdultSelector
roomIndex={index}
currentAdults={currentAdults}
currentChildren={currentChildren}
childrenInAdultsBed={childrenInAdultsBed}
/>
<ChildSelector
roomIndex={index}
currentAdults={currentAdults}
currentChildren={currentChildren}
childrenInAdultsBed={childrenInAdultsBed}
/>
</section>
<Divider color="primaryLightSubtle" />
</div>
)
})}
<div className={styles.addRoomMobileContainer}>
<Tooltip
heading={disabledBookingOptionsHeader}
text={disabledBookingOptionsText}
position="top"
arrow="left"
>
{rooms.length < 4 ? (
<Button
intent="text"
variant="icon"
wrapping
disabled
theme="base"
fullWidth
>
<PlusIcon />
{addRoomLabel}
</Button>
) : null}
</Tooltip>
</div>
</div>
<footer className={styles.footer}>
<div className={styles.hideOnMobile}>
<Tooltip
heading={disabledBookingOptionsHeader}
text={disabledBookingOptionsText}
position="top"
arrow="left"
>
{rooms.length < 4 ? (
<Button
intent="text"
variant="icon"
wrapping
disabled
theme="base"
>
<PlusCircleIcon />
{addRoomLabel}
</Button>
) : null}
</Tooltip>
</div>
<Button
onPress={onClose}
disabled={getFieldState("rooms").invalid}
className={styles.hideOnMobile}
intent="tertiary"
theme="base"
size="small"
>
{doneLabel}
</Button>
<Button
onPress={onClose}
disabled={getFieldState("rooms").invalid}
className={styles.hideOnDesktop}
intent="tertiary"
theme="base"
size="large"
>
{doneLabel}
</Button>
</footer>
</>
)
}

View File

@@ -1,26 +0,0 @@
"use client"
import { PropsWithChildren, useRef } from "react"
import {
GuestsRoomsContext,
type GuestsRoomsStore,
initGuestsRoomsState,
} from "@/stores/guests-rooms"
import { GuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker"
export default function GuestsRoomsProvider({
selectedGuests,
children,
}: PropsWithChildren<{ selectedGuests?: GuestsRoom[] }>) {
const initialStore = useRef<GuestsRoomsStore>()
if (!initialStore.current) {
initialStore.current = initGuestsRoomsState(selectedGuests)
}
return (
<GuestsRoomsContext.Provider value={initialStore.current}>
{children}
</GuestsRoomsContext.Provider>
)
}

View File

@@ -1,7 +1,23 @@
.container { .triggerDesktop {
overflow: hidden; display: none;
position: relative;
} }
.pickerContainerMobile {
background-color: var(--Main-Grey-White);
border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0;
bottom: 0;
left: 0;
position: fixed;
right: 0;
top: 0;
transition: top 300ms ease;
z-index: 1000;
}
.pickerContainerDesktop {
display: none;
}
.roomContainer { .roomContainer {
display: grid; display: grid;
gap: var(--Spacing-x2); gap: var(--Spacing-x2);
@@ -23,9 +39,7 @@
width: 100%; width: 100%;
text-align: left; text-align: left;
} }
.body {
opacity: 0.8;
}
.footer { .footer {
display: grid; display: grid;
gap: var(--Spacing-x1); gap: var(--Spacing-x1);
@@ -33,25 +47,7 @@
margin-top: var(--Spacing-x2); margin-top: var(--Spacing-x2);
} }
.pickerContainer {
--header-height: 72px;
--sticky-button-height: 140px;
background-color: var(--Main-Grey-White);
display: grid;
border-radius: var(--Corner-radius-Large);
box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1);
max-width: calc(100vw - 20px);
padding: var(--Spacing-x2) var(--Spacing-x3);
width: 360px;
}
@media screen and (max-width: 1366px) { @media screen and (max-width: 1366px) {
.container[data-isopen="true"] .hideWrapper {
top: 20px;
}
.contentContainer { .contentContainer {
grid-area: content; grid-area: content;
overflow-y: scroll; overflow-y: scroll;
@@ -107,6 +103,32 @@
} }
@media screen and (min-width: 1367px) { @media screen and (min-width: 1367px) {
.pickerContainerMobisse {
display: none;
}
.triggerMobile {
display: none;
}
.triggerDesktop {
display: block;
}
.pickerContainerDesktop {
--header-height: 72px;
--sticky-button-height: 140px;
background-color: var(--Main-Grey-White);
display: grid;
border-radius: var(--Corner-radius-Large);
box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1);
max-width: calc(100vw - 20px);
padding: var(--Spacing-x2) var(--Spacing-x3);
width: 360px;
}
.header { .header {
display: none; display: none;
} }

View File

@@ -1,37 +1,64 @@
"use client" "use client"
import { useCallback, useEffect, useRef, useState } from "react" import {
import { Button, DialogTrigger, Popover } from "react-aria-components" Button,
Dialog,
DialogTrigger,
Modal,
Popover,
} from "react-aria-components"
import { useFormContext } from "react-hook-form" import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { useGuestsRoomsStore } from "@/stores/guests-rooms"
import { guestRoomsSchema } from "@/components/Forms/BookingWidget/schema"
import Body from "@/components/TempDesignSystem/Text/Body" import Body from "@/components/TempDesignSystem/Text/Body"
import Dialog from "./Dialog" import PickerForm from "./Form"
import styles from "./guests-rooms-picker.module.css" import styles from "./guests-rooms-picker.module.css"
export default function GuestsRoomsPickerForm({ import { GuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker"
name = "rooms",
export default function GuestsRoomsPickerForm() {
const { watch } = useFormContext()
const rooms = watch("rooms") as GuestsRoom[]
return (
<>
<DialogTrigger>
<Trigger rooms={rooms} className={styles.triggerMobile} />
<Modal className="my-modal">
<Dialog className={styles.pickerContainerMobile}>
{({ close }) => <PickerForm rooms={rooms} onClose={close} />}
</Dialog>
</Modal>
</DialogTrigger>
<DialogTrigger>
<Trigger rooms={rooms} className={styles.triggerDesktop} />
<Popover placement="bottom start" offset={22}>
<Dialog className={styles.pickerContainerDesktop}>
{({ close }) => <PickerForm rooms={rooms} onClose={close} />}
</Dialog>
</Popover>
</DialogTrigger>
</>
)
}
function Trigger({
rooms,
className,
}: { }: {
name: string rooms: GuestsRoom[]
className: string
}) { }) {
const intl = useIntl() const intl = useIntl()
const { rooms, adultCount, childCount } = useGuestsRoomsStore((state) => ({
rooms: state.rooms,
adultCount: state.adultCount,
childCount: state.childCount,
}))
return ( return (
<DialogTrigger> <Button className={`${className} ${styles.btn}`} type="button">
<Button className={styles.btn} type="button"> <Body>
<Body className={styles.body} asChild> {rooms.map((room, i) => (
<span> <span key={i}>
{intl.formatMessage( {intl.formatMessage(
{ id: "booking.rooms" }, { id: "booking.rooms" },
{ totalRooms: rooms.length } { totalRooms: rooms.length }
@@ -39,21 +66,18 @@ export default function GuestsRoomsPickerForm({
{", "} {", "}
{intl.formatMessage( {intl.formatMessage(
{ id: "booking.adults" }, { id: "booking.adults" },
{ totalAdults: adultCount } { totalAdults: room.adults }
)} )}
{childCount > 0 {room.child.length > 0
? ", " + ? ", " +
intl.formatMessage( intl.formatMessage(
{ id: "booking.children" }, { id: "booking.children" },
{ totalChildren: childCount } { totalChildren: room.child.length }
) )
: null} : null}
</span> </span>
</Body> ))}
</Button> </Body>
<Popover> </Button>
<Dialog />
</Popover>
</DialogTrigger>
) )
} }

View File

@@ -1,227 +0,0 @@
"use client"
import { produce } from "immer"
import { createContext, useContext } from "react"
import { create, useStore } from "zustand"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
import {
Child,
GuestsRoom,
} from "@/types/components/bookingWidget/guestsRoomsPicker"
const SESSION_STORAGE_KEY = "guests_rooms"
interface extendedGuestsRoom extends GuestsRoom {
childrenInAdultsBed: number
}
interface GuestsRoomsState {
rooms: extendedGuestsRoom[]
adultCount: number
childCount: number
isValidated: boolean
}
interface GuestsRoomsStoreState extends GuestsRoomsState {
increaseAdults: (roomIndex: number) => void
decreaseAdults: (roomIndex: number) => void
increaseChildren: (roomIndex: number) => void
decreaseChildren: (roomIndex: number) => Child[]
updateChildAge: (age: number, roomIndex: number, childIndex: number) => void
updateChildBed: (bed: number, roomIndex: number, childIndex: number) => void
increaseChildInAdultsBed: (roomIndex: number) => void
decreaseChildInAdultsBed: (roomIndex: number) => void
increaseRoom: () => void
decreaseRoom: (roomIndex: number) => void
setIsValidated: (isValidated: boolean) => void
}
export function validateBedTypes(data: extendedGuestsRoom[]) {
data.forEach((room) => {
room.child.forEach((child) => {
const allowedBedTypes: number[] = []
if (child.age <= 5 && room.adults >= room.childrenInAdultsBed) {
allowedBedTypes.push(ChildBedMapEnum.IN_ADULTS_BED)
} else if (child.age <= 5) {
room.childrenInAdultsBed = room.childrenInAdultsBed - 1
}
if (child.age < 3) {
allowedBedTypes.push(ChildBedMapEnum.IN_CRIB)
}
if (child.age > 2) {
allowedBedTypes.push(ChildBedMapEnum.IN_EXTRA_BED)
}
if (!allowedBedTypes.includes(child.bed)) {
child.bed = allowedBedTypes[0]
}
})
})
}
export function initGuestsRoomsState(initData?: GuestsRoom[]) {
const isBrowser = typeof window !== "undefined"
const sessionData = isBrowser
? sessionStorage.getItem(SESSION_STORAGE_KEY)
: null
const defaultGuestsData: extendedGuestsRoom = {
adults: 1,
child: [],
childrenInAdultsBed: 0,
}
const defaultData: GuestsRoomsState = {
rooms: [defaultGuestsData],
adultCount: 1,
childCount: 0,
isValidated: false,
}
let inputData: GuestsRoomsState = defaultData
if (sessionData) {
inputData = JSON.parse(sessionData)
}
if (initData) {
inputData.rooms = initData.map((room) => {
const childrenInAdultsBed = room.child
? room.child.reduce((acc, child) => {
acc = acc + (child.bed == ChildBedMapEnum.IN_ADULTS_BED ? 1 : 0)
return acc
}, 0)
: 0
return { ...defaultGuestsData, ...room, childrenInAdultsBed }
}) as extendedGuestsRoom[]
inputData.adultCount = initData.reduce((acc, room) => {
acc = acc + room.adults
return acc
}, 0)
inputData.childCount = initData.reduce((acc, room) => {
acc = acc + room.child?.length
return acc
}, 0)
validateBedTypes(inputData.rooms)
}
return create<GuestsRoomsStoreState>()((set, get) => ({
...inputData,
increaseAdults: (roomIndex) =>
set(
produce((state: GuestsRoomsState) => {
state.rooms[roomIndex].adults = state.rooms[roomIndex].adults + 1
state.adultCount = state.adultCount + 1
})
),
decreaseAdults: (roomIndex) =>
set(
produce((state: GuestsRoomsState) => {
state.rooms[roomIndex].adults = state.rooms[roomIndex].adults - 1
state.adultCount = state.adultCount - 1
if (
state.rooms[roomIndex].childrenInAdultsBed >
state.rooms[roomIndex].adults
) {
const toUpdateIndex = state.rooms[roomIndex].child.findIndex(
(child) => child.bed == ChildBedMapEnum.IN_ADULTS_BED
)
if (toUpdateIndex != -1) {
state.rooms[roomIndex].child[toUpdateIndex].bed =
state.rooms[roomIndex].child[toUpdateIndex].age < 3
? ChildBedMapEnum.IN_CRIB
: ChildBedMapEnum.IN_EXTRA_BED
state.rooms[roomIndex].childrenInAdultsBed =
state.rooms[roomIndex].adults
}
}
})
),
increaseChildren: (roomIndex) =>
set(
produce((state: GuestsRoomsState) => {
state.rooms[roomIndex].child.push({
age: -1,
bed: -1,
})
state.childCount = state.childCount + 1
})
),
decreaseChildren: (roomIndex) => {
set(
produce((state: GuestsRoomsState) => {
const roomChildren = state.rooms[roomIndex].child
if (
roomChildren.length &&
roomChildren[roomChildren.length - 1].bed ==
ChildBedMapEnum.IN_ADULTS_BED
) {
state.rooms[roomIndex].childrenInAdultsBed =
state.rooms[roomIndex].childrenInAdultsBed - 1
}
state.rooms[roomIndex].child.pop()
state.childCount = state.childCount - 1
})
)
return get().rooms[roomIndex].child
},
updateChildAge: (age, roomIndex, childIndex) =>
set(
produce((state: GuestsRoomsState) => {
state.rooms[roomIndex].child[childIndex].age = age
})
),
updateChildBed: (bed, roomIndex, childIndex) =>
set(
produce((state: GuestsRoomsState) => {
state.rooms[roomIndex].child[childIndex].bed = bed
})
),
increaseChildInAdultsBed: (roomIndex) =>
set(
produce((state: GuestsRoomsState) => {
state.rooms[roomIndex].childrenInAdultsBed =
state.rooms[roomIndex].childrenInAdultsBed + 1
})
),
decreaseChildInAdultsBed: (roomIndex) =>
set(
produce((state: GuestsRoomsState) => {
state.rooms[roomIndex].childrenInAdultsBed =
state.rooms[roomIndex].childrenInAdultsBed - 1
})
),
increaseRoom: () =>
set(
produce((state: GuestsRoomsState) => {
state.rooms.push({
adults: 1,
child: [],
childrenInAdultsBed: 0,
})
})
),
decreaseRoom: (roomIndex) =>
set(
produce((state: GuestsRoomsState) => {
state.rooms.splice(roomIndex, 1)
})
),
setIsValidated: (isValidated) => set(() => ({ isValidated })),
}))
}
export type GuestsRoomsStore = ReturnType<typeof initGuestsRoomsState>
export const GuestsRoomsContext = createContext<GuestsRoomsStore | null>(null)
export const useGuestsRoomsStore = <T>(
selector: (store: GuestsRoomsStoreState) => T
): T => {
const guestsRoomsContextStore = useContext(GuestsRoomsContext)
if (!guestsRoomsContextStore) {
throw new Error(
`guestsRoomsContextStore must be used within GuestsRoomsContextProvider`
)
}
return useStore(guestsRoomsContextStore, selector)
}

View File

@@ -3,7 +3,6 @@ import { create } from "zustand"
export enum StickyElementNameEnum { export enum StickyElementNameEnum {
SITEWIDE_ALERT = "SITEWIDE_ALERT", SITEWIDE_ALERT = "SITEWIDE_ALERT",
BOOKING_WIDGET = "BOOKING_WIDGET", BOOKING_WIDGET = "BOOKING_WIDGET",
BOOKING_WIDGET_MOBILE = "BOOKING_WIDGET_MOBILE",
HOTEL_TAB_NAVIGATION = "HOTEL_TAB_NAVIGATION", HOTEL_TAB_NAVIGATION = "HOTEL_TAB_NAVIGATION",
HOTEL_STATIC_MAP = "HOTEL_STATIC_MAP", HOTEL_STATIC_MAP = "HOTEL_STATIC_MAP",
} }
@@ -32,7 +31,6 @@ interface StickyStore {
const priorityMap: Record<StickyElementNameEnum, number> = { const priorityMap: Record<StickyElementNameEnum, number> = {
[StickyElementNameEnum.SITEWIDE_ALERT]: 1, [StickyElementNameEnum.SITEWIDE_ALERT]: 1,
[StickyElementNameEnum.BOOKING_WIDGET]: 2, [StickyElementNameEnum.BOOKING_WIDGET]: 2,
[StickyElementNameEnum.BOOKING_WIDGET_MOBILE]: 2,
[StickyElementNameEnum.HOTEL_TAB_NAVIGATION]: 3, [StickyElementNameEnum.HOTEL_TAB_NAVIGATION]: 3,
[StickyElementNameEnum.HOTEL_STATIC_MAP]: 3, [StickyElementNameEnum.HOTEL_STATIC_MAP]: 3,

View File

@@ -17,18 +17,19 @@ export type GuestsRoomPickerProps = {
index: number index: number
} }
export type AdultSelectorProps = { export type SelectorProps = {
roomIndex: number
}
export type ChildSelectorProps = {
roomIndex: number roomIndex: number
currentAdults: number
currentChildren: Child[]
childrenInAdultsBed: number
} }
export type ChildInfoSelectorProps = { export type ChildInfoSelectorProps = {
child: Child child: Child
adults: number
index: number index: number
roomIndex: number roomIndex: number
childrenInAdultsBed: number
} }
export interface CounterProps { export interface CounterProps {