Merged master into feat/SW-888-skeleton-loaders
This commit is contained in:
@@ -18,7 +18,7 @@ import { setLang } from "@/i18n/serverContext"
|
|||||||
|
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
import { LangParams, PageArgs } from "@/types/params"
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
|
|
||||||
export default async function SelectRatePage({
|
export default async function SelectRatePage({
|
||||||
params,
|
params,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -42,11 +42,11 @@ export default function BookingWidgetClient({
|
|||||||
|
|
||||||
const bookingWidgetSearchData: BookingWidgetSearchParams | undefined =
|
const bookingWidgetSearchData: BookingWidgetSearchParams | undefined =
|
||||||
searchParams
|
searchParams
|
||||||
? (getFormattedUrlQueryParams(new URLSearchParams(searchParams), {
|
? getFormattedUrlQueryParams(new URLSearchParams(searchParams), {
|
||||||
adults: "number",
|
adults: "number",
|
||||||
age: "number",
|
age: "number",
|
||||||
bed: "number",
|
bed: "number",
|
||||||
}) as BookingWidgetSearchParams)
|
})
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const getLocationObj = (destination: string): Location | undefined => {
|
const getLocationObj = (destination: string): Location | undefined => {
|
||||||
@@ -79,6 +79,16 @@ export default function BookingWidgetClient({
|
|||||||
)
|
)
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
|
const defaultRoomsData = bookingWidgetSearchData?.room?.map((room) => ({
|
||||||
|
adults: room.adults,
|
||||||
|
child: room.child ?? [],
|
||||||
|
})) ?? [
|
||||||
|
{
|
||||||
|
adults: 1,
|
||||||
|
child: [],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
const methods = useForm<BookingWidgetSchema>({
|
const methods = useForm<BookingWidgetSchema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
search: selectedLocation?.name ?? "",
|
search: selectedLocation?.name ?? "",
|
||||||
@@ -96,12 +106,7 @@ export default function BookingWidgetClient({
|
|||||||
bookingCode: "",
|
bookingCode: "",
|
||||||
redemption: false,
|
redemption: false,
|
||||||
voucher: false,
|
voucher: false,
|
||||||
rooms: bookingWidgetSearchData?.room ?? [
|
rooms: defaultRoomsData,
|
||||||
{
|
|
||||||
adults: 1,
|
|
||||||
child: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
shouldFocusError: false,
|
shouldFocusError: false,
|
||||||
mode: "all",
|
mode: "all",
|
||||||
@@ -158,10 +163,13 @@ export default function BookingWidgetClient({
|
|||||||
|
|
||||||
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}
|
||||||
|
>
|
||||||
|
<MobileToggleButton openMobileSearch={openMobileSearch} />
|
||||||
|
<div className={styles.formContainer}>
|
||||||
<button
|
<button
|
||||||
className={styles.close}
|
className={styles.close}
|
||||||
onClick={closeMobileSearch}
|
onClick={closeMobileSearch}
|
||||||
@@ -170,9 +178,9 @@ export default function BookingWidgetClient({
|
|||||||
<CloseLargeIcon />
|
<CloseLargeIcon />
|
||||||
</button>
|
</button>
|
||||||
<Form locations={locations} type={type} />
|
<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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -67,7 +61,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 && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
.containerDesktop,
|
.wrapper {
|
||||||
.containerMobile,
|
position: sticky;
|
||||||
.close {
|
z-index: var(--booking-widget-z-index);
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 767px) {
|
.formContainer {
|
||||||
.containerMobile {
|
|
||||||
background-color: var(--UI-Input-Controls-Surface-Normal);
|
|
||||||
bottom: -100%;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
background-color: var(--UI-Input-Controls-Surface-Normal);
|
||||||
|
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;
|
||||||
|
bottom: -100%;
|
||||||
transition: bottom 300ms ease;
|
transition: bottom 300ms ease;
|
||||||
width: 100%;
|
|
||||||
z-index: 10000;
|
|
||||||
border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.containerMobile[data-open="true"] {
|
.wrapper[data-open="true"] {
|
||||||
|
z-index: var(--booking-widget-open-z-index);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper[data-open="true"] .formContainer {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,32 +30,34 @@
|
|||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
justify-self: flex-end;
|
justify-self: flex-end;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.containerMobile[data-open="true"] + .backdrop {
|
.wrapper[data-open="true"] + .backdrop {
|
||||||
background-color: rgba(0, 0, 0, 0.4);
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 1000;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.container {
|
.container {
|
||||||
--header-height: 68px;
|
--header-height: 72px;
|
||||||
--sticky-button-height: 120px;
|
--sticky-button-height: 120px;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -11,12 +11,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
align-self: flex-start;
|
align-self: flex-end;
|
||||||
background-color: var(--Main-Grey-White);
|
background-color: var(--Main-Grey-White);
|
||||||
display: grid;
|
|
||||||
grid-area: header;
|
grid-area: header;
|
||||||
grid-template-columns: 1fr 24px;
|
padding: var(--Spacing-x3) var(--Spacing-x2);
|
||||||
padding: var(--Spacing-x3) var(--Spacing-x2) var(--Spacing-x2);
|
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
.hideWrapper {
|
.hideWrapper {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
|
|||||||
@@ -6,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 SkeletonShimmer from "@/components/SkeletonShimmer"
|
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
@@ -26,12 +25,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 +48,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}>
|
||||||
|
|||||||
@@ -1,15 +1,36 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||||
import type { Location } from "@/types/trpc/routers/hotel/locations"
|
import type { Location } from "@/types/trpc/routers/hotel/locations"
|
||||||
|
|
||||||
export const guestRoomSchema = z.object({
|
export const guestRoomSchema = z
|
||||||
|
.object({
|
||||||
adults: z.number().default(1),
|
adults: z.number().default(1),
|
||||||
child: z.array(
|
child: z
|
||||||
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
age: z.number().nonnegative(),
|
age: z.number().min(0, "Age is required"),
|
||||||
bed: z.number(),
|
bed: z.number().min(0, "Bed choice is required"),
|
||||||
})
|
})
|
||||||
),
|
)
|
||||||
|
.default([]),
|
||||||
|
})
|
||||||
|
.superRefine((value, ctx) => {
|
||||||
|
const childrenInAdultsBed = value.child.filter(
|
||||||
|
(c) => c.bed === ChildBedMapEnum.IN_ADULTS_BED
|
||||||
|
)
|
||||||
|
if (value.adults < childrenInAdultsBed.length) {
|
||||||
|
const lastAdultBedIndex = value.child
|
||||||
|
.map((c) => c.bed)
|
||||||
|
.lastIndexOf(ChildBedMapEnum.IN_ADULTS_BED)
|
||||||
|
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message:
|
||||||
|
"You cannot have more children in adults bed than adults in the room",
|
||||||
|
path: ["child", lastAdultBedIndex],
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const guestRoomsSchema = z.array(guestRoomSchema)
|
export const guestRoomsSchema = z.array(guestRoomSchema)
|
||||||
|
|||||||
@@ -3,54 +3,32 @@
|
|||||||
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"
|
||||||
|
|
||||||
import styles from "./adult-selector.module.css"
|
import styles from "./adult-selector.module.css"
|
||||||
|
|
||||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
import { SelectorProps } from "@/types/components/bookingWidget/guestsRoomsPicker"
|
||||||
import {
|
|
||||||
AdultSelectorProps,
|
|
||||||
Child,
|
|
||||||
} from "@/types/components/bookingWidget/guestsRoomsPicker"
|
|
||||||
|
|
||||||
export default function AdultSelector({ roomIndex = 0 }: AdultSelectorProps) {
|
export default function AdultSelector({
|
||||||
|
roomIndex = 0,
|
||||||
|
currentAdults,
|
||||||
|
}: SelectorProps) {
|
||||||
|
const name = `rooms.${roomIndex}.adults`
|
||||||
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() {
|
||||||
if (adults < 6) {
|
if (currentAdults < 6) {
|
||||||
increaseAdults(roomIndex)
|
setValue(name, currentAdults + 1)
|
||||||
setValue(`rooms.${roomIndex}.adults`, adults + 1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function decreaseAdultsCount(roomIndex: number) {
|
function decreaseAdultsCount() {
|
||||||
if (adults > 1) {
|
if (currentAdults > 1) {
|
||||||
decreaseAdults(roomIndex)
|
setValue(name, currentAdults - 1)
|
||||||
setValue(`rooms.${roomIndex}.adults`, adults - 1)
|
|
||||||
if (childrenInAdultsBed > adults) {
|
|
||||||
const toUpdateIndex = child.findIndex(
|
|
||||||
(child: Child) => child.bed == ChildBedMapEnum.IN_ADULTS_BED
|
|
||||||
)
|
|
||||||
if (toUpdateIndex != -1) {
|
|
||||||
setValue(
|
|
||||||
`rooms.${roomIndex}.children.${toUpdateIndex}.bed`,
|
|
||||||
child[toUpdateIndex].age < 3
|
|
||||||
? ChildBedMapEnum.IN_CRIB
|
|
||||||
: ChildBedMapEnum.IN_EXTRA_BED
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,15 +38,11 @@ export default function AdultSelector({ roomIndex = 0 }: AdultSelectorProps) {
|
|||||||
{adultsLabel}
|
{adultsLabel}
|
||||||
</Caption>
|
</Caption>
|
||||||
<Counter
|
<Counter
|
||||||
count={adults}
|
count={currentAdults}
|
||||||
handleOnDecrease={() => {
|
handleOnDecrease={decreaseAdultsCount}
|
||||||
decreaseAdultsCount(roomIndex)
|
handleOnIncrease={increaseAdultsCount}
|
||||||
}}
|
disableDecrease={currentAdults == 1}
|
||||||
handleOnIncrease={() => {
|
disableIncrease={currentAdults == 6}
|
||||||
increaseAdultsCount(roomIndex)
|
|
||||||
}}
|
|
||||||
disableDecrease={adults == 1}
|
|
||||||
disableIncrease={adults == 6}
|
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -17,55 +15,35 @@ import {
|
|||||||
ChildInfoSelectorProps,
|
ChildInfoSelectorProps,
|
||||||
} from "@/types/components/bookingWidget/guestsRoomsPicker"
|
} from "@/types/components/bookingWidget/guestsRoomsPicker"
|
||||||
|
|
||||||
|
const ageList = [...Array(13)].map((_, i) => ({
|
||||||
|
label: i.toString(),
|
||||||
|
value: i,
|
||||||
|
}))
|
||||||
|
|
||||||
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) {
|
||||||
|
const ageFieldName = `rooms.${roomIndex}.child.${index}.age`
|
||||||
|
const bedFieldName = `rooms.${roomIndex}.child.${index}.bed`
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const ageLabel = intl.formatMessage({ id: "Age" })
|
const ageLabel = intl.formatMessage({ id: "Age" })
|
||||||
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 errorMessage = intl.formatMessage({ id: "Child age is required" })
|
||||||
const { adults, childrenInAdultsBed } = useGuestsRoomsStore(
|
const { setValue, formState, register, trigger } = useFormContext()
|
||||||
(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) => ({
|
|
||||||
label: `${age}`,
|
|
||||||
value: age,
|
|
||||||
}))
|
|
||||||
|
|
||||||
function updateSelectedAge(age: number) {
|
|
||||||
updateChildAge(age, roomIndex, index)
|
|
||||||
setValue(`rooms.${roomIndex}.child.${index}.age`, age, {
|
|
||||||
shouldValidate: true,
|
|
||||||
})
|
|
||||||
const availableBedTypes = getAvailableBeds(age)
|
|
||||||
updateSelectedBed(availableBedTypes[0].value)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
||||||
|
trigger()
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelectedAge(age: number) {
|
||||||
|
setValue(`rooms.${roomIndex}.child.${index}.age`, age)
|
||||||
|
const availableBedTypes = getAvailableBeds(age)
|
||||||
|
updateSelectedBed(availableBedTypes[0].value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const allBedTypes: ChildBed[] = [
|
const allBedTypes: ChildBed[] = [
|
||||||
@@ -97,6 +75,12 @@ export default function ChildInfoSelector({
|
|||||||
return availableBedTypes
|
return availableBedTypes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//@ts-expect-error: formState is typed with FormValues
|
||||||
|
const roomErrors = formState.errors.rooms?.[roomIndex]?.child?.[index]
|
||||||
|
|
||||||
|
const ageError = roomErrors?.age
|
||||||
|
const bedError = roomErrors?.bed
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div key={index} className={styles.childInfoContainer}>
|
<div key={index} className={styles.childInfoContainer}>
|
||||||
@@ -110,13 +94,15 @@ export default function ChildInfoSelector({
|
|||||||
onSelect={(key) => {
|
onSelect={(key) => {
|
||||||
updateSelectedAge(key as number)
|
updateSelectedAge(key as number)
|
||||||
}}
|
}}
|
||||||
name={`rooms.${roomIndex}.child.${index}.age`}
|
|
||||||
placeholder={ageLabel}
|
placeholder={ageLabel}
|
||||||
maxHeight={150}
|
maxHeight={150}
|
||||||
|
{...register(ageFieldName, {
|
||||||
|
required: true,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{child.age !== -1 ? (
|
{child.age >= 0 ? (
|
||||||
<Select
|
<Select
|
||||||
items={getAvailableBeds(child.age)}
|
items={getAvailableBeds(child.age)}
|
||||||
label={bedLabel}
|
label={bedLabel}
|
||||||
@@ -125,16 +111,26 @@ export default function ChildInfoSelector({
|
|||||||
onSelect={(key) => {
|
onSelect={(key) => {
|
||||||
updateSelectedBed(key as number)
|
updateSelectedBed(key as number)
|
||||||
}}
|
}}
|
||||||
name={`rooms.${roomIndex}.child.${index}.age`}
|
|
||||||
placeholder={bedLabel}
|
placeholder={bedLabel}
|
||||||
|
{...register(bedFieldName, {
|
||||||
|
required: true,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isValidated && child.age < 0 ? (
|
|
||||||
|
{roomErrors && roomErrors.message ? (
|
||||||
<Caption color="red" className={styles.error}>
|
<Caption color="red" className={styles.error}>
|
||||||
<ErrorCircleIcon color="red" />
|
<ErrorCircleIcon color="red" />
|
||||||
{ageReqdErrMsg}
|
{roomErrors.message}
|
||||||
|
</Caption>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{ageError || bedError ? (
|
||||||
|
<Caption color="red" className={styles.error}>
|
||||||
|
<ErrorCircleIcon color="red" />
|
||||||
|
{errorMessage}
|
||||||
</Caption>
|
</Caption>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -12,40 +10,30 @@ 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 { SelectorProps } from "@/types/components/bookingWidget/guestsRoomsPicker"
|
||||||
import { ChildSelectorProps } 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()
|
||||||
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(`rooms.${roomIndex}.child.${currentChildren.length}`, {
|
||||||
setValue(
|
age: undefined,
|
||||||
`rooms.${roomIndex}.child.${children.length}`,
|
bed: undefined,
|
||||||
{
|
})
|
||||||
age: -1,
|
|
||||||
bed: -1,
|
|
||||||
},
|
|
||||||
{ shouldValidate: true }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,23 +44,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}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|||||||
167
components/GuestsRoomsPicker/Form.tsx
Normal file
167
components/GuestsRoomsPicker/Form.tsx
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect } from "react"
|
||||||
|
import { useFormContext, useWatch } 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, trigger } = useFormContext<BookingWidgetSchema>()
|
||||||
|
|
||||||
|
const roomsValue = useWatch({ name: "rooms" })
|
||||||
|
|
||||||
|
async function handleOnClose() {
|
||||||
|
const state = await trigger("rooms")
|
||||||
|
if (state) {
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldState = getFieldState("rooms")
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fieldState.invalid) {
|
||||||
|
trigger("rooms")
|
||||||
|
}
|
||||||
|
}, [roomsValue, fieldState.invalid, trigger])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className={styles.contentWrapper}>
|
||||||
|
<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 ?? 0
|
||||||
|
|
||||||
|
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>
|
||||||
|
</section>
|
||||||
|
<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={handleOnClose}
|
||||||
|
disabled={getFieldState("rooms").invalid}
|
||||||
|
className={styles.hideOnMobile}
|
||||||
|
intent="tertiary"
|
||||||
|
theme="base"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{doneLabel}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onPress={handleOnClose}
|
||||||
|
disabled={getFieldState("rooms").invalid}
|
||||||
|
className={styles.hideOnDesktop}
|
||||||
|
intent="tertiary"
|
||||||
|
theme="base"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
{doneLabel}
|
||||||
|
</Button>
|
||||||
|
</footer>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
"use client"
|
|
||||||
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"
|
|
||||||
import { GuestsRoomsPickerProps } from "@/types/components/bookingWidget/guestsRoomsPicker"
|
|
||||||
|
|
||||||
export default function GuestsRoomsPicker({
|
|
||||||
closePicker,
|
|
||||||
}: GuestsRoomsPickerProps) {
|
|
||||||
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)
|
|
||||||
|
|
||||||
// Not in MVP
|
|
||||||
// const increaseRoom = useGuestsRoomsStore.use.increaseRoom()
|
|
||||||
// const decreaseRoom = useGuestsRoomsStore.use.decreaseRoom()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.pickerContainer}>
|
|
||||||
<header className={styles.header}>
|
|
||||||
<button type="button" className={styles.close} onClick={closePicker}>
|
|
||||||
<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>
|
|
||||||
{/* Not in MVP
|
|
||||||
{index > 0 ? (
|
|
||||||
<Button intent="text" onClick={() => decreaseRoom(index)}>
|
|
||||||
Remove Room
|
|
||||||
</Button>
|
|
||||||
) : null} */}
|
|
||||||
<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
|
|
||||||
onClick={closePicker}
|
|
||||||
disabled={getFieldState("rooms").invalid}
|
|
||||||
className={styles.hideOnMobile}
|
|
||||||
intent="tertiary"
|
|
||||||
theme="base"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{doneLabel}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={closePicker}
|
|
||||||
disabled={getFieldState("rooms").invalid}
|
|
||||||
className={styles.hideOnDesktop}
|
|
||||||
intent="tertiary"
|
|
||||||
theme="base"
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
{doneLabel}
|
|
||||||
</Button>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,32 @@
|
|||||||
.container {
|
.triggerDesktop {
|
||||||
overflow: hidden;
|
display: none;
|
||||||
position: relative;
|
|
||||||
&[data-isopen="true"] {
|
|
||||||
overflow: visible;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pickerContainerMobile {
|
||||||
|
--header-height: 72px;
|
||||||
|
--sticky-button-height: 140px;
|
||||||
|
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: 20px;
|
||||||
|
transition: top 300ms ease;
|
||||||
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
.contentWrapper {
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas:
|
||||||
|
"header"
|
||||||
|
"content";
|
||||||
|
grid-template-rows: var(--header-height) calc(100dvh - var(--header-height));
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickerContainerDesktop {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.roomContainer {
|
.roomContainer {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Spacing-x2);
|
gap: var(--Spacing-x2);
|
||||||
@@ -14,9 +36,6 @@
|
|||||||
gap: var(--Spacing-x2);
|
gap: var(--Spacing-x2);
|
||||||
padding-bottom: var(--Spacing-x1);
|
padding-bottom: var(--Spacing-x1);
|
||||||
}
|
}
|
||||||
.hideWrapper {
|
|
||||||
background-color: var(--Main-Grey-White);
|
|
||||||
}
|
|
||||||
.roomHeading {
|
.roomHeading {
|
||||||
margin-bottom: var(--Spacing-x1);
|
margin-bottom: var(--Spacing-x1);
|
||||||
}
|
}
|
||||||
@@ -29,43 +48,14 @@
|
|||||||
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);
|
||||||
grid-template-columns: auto;
|
grid-template-columns: auto;
|
||||||
margin-top: var(--Spacing-x2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 1366px) {
|
@media screen and (max-width: 1366px) {
|
||||||
.hideWrapper {
|
|
||||||
border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
position: fixed;
|
|
||||||
right: 0;
|
|
||||||
top: 100%;
|
|
||||||
transition: top 300ms ease;
|
|
||||||
z-index: 10002;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container[data-isopen="true"] .hideWrapper {
|
|
||||||
top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pickerContainer {
|
|
||||||
--header-height: 72px;
|
|
||||||
--sticky-button-height: 140px;
|
|
||||||
display: grid;
|
|
||||||
grid-template-areas:
|
|
||||||
"header"
|
|
||||||
"content";
|
|
||||||
grid-template-rows: var(--header-height) calc(100dvh - var(--header-height));
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.contentContainer {
|
.contentContainer {
|
||||||
grid-area: content;
|
grid-area: content;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
@@ -73,7 +63,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
background-color: var(--Main-Grey-White);
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-area: header;
|
grid-area: header;
|
||||||
padding: var(--Spacing-x3) var(--Spacing-x2);
|
padding: var(--Spacing-x3) var(--Spacing-x2);
|
||||||
@@ -101,11 +90,10 @@
|
|||||||
rgba(255, 255, 255, 0) 7.5%,
|
rgba(255, 255, 255, 0) 7.5%,
|
||||||
#ffffff 82.5%
|
#ffffff 82.5%
|
||||||
);
|
);
|
||||||
padding: var(--Spacing-x1) var(--Spacing-x2) var(--Spacing-x7);
|
padding: var(--Spacing-x2) var(--Spacing-x2) var(--Spacing-x7);
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 10;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer .hideOnMobile {
|
.footer .hideOnMobile {
|
||||||
@@ -121,17 +109,40 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 1367px) {
|
@media screen and (min-width: 1367px) {
|
||||||
.hideWrapper {
|
.pickerContainerMobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.contentWrapper {
|
||||||
|
grid-template-rows: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentContainer {
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
border-radius: var(--Corner-radius-Large);
|
||||||
box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1);
|
box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1);
|
||||||
left: calc((var(--Spacing-x1) + var(--Spacing-x2)) * -1);
|
|
||||||
max-width: calc(100vw - 20px);
|
max-width: calc(100vw - 20px);
|
||||||
padding: var(--Spacing-x2) var(--Spacing-x3);
|
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||||
position: absolute;
|
|
||||||
top: calc(100% + var(--Spacing-x2) + 1px + var(--Spacing-x4));
|
|
||||||
width: 360px;
|
width: 360px;
|
||||||
max-height: calc(100dvh - 77px - var(--Spacing-x6));
|
}
|
||||||
overflow-y: auto;
|
|
||||||
|
.pickerContainerDesktop:focus-visible {
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
@@ -140,6 +151,7 @@
|
|||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
grid-template-columns: auto auto;
|
grid-template-columns: auto auto;
|
||||||
|
padding-top: var(--Spacing-x2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer .hideOnDesktop,
|
.footer .hideOnDesktop,
|
||||||
|
|||||||
@@ -1,67 +1,86 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
|
import {
|
||||||
|
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 { useMediaQuery } from "usehooks-ts"
|
||||||
|
|
||||||
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 GuestsRoomsPicker from "./GuestsRoomsPicker"
|
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() {
|
||||||
name: string
|
const { watch } = useFormContext()
|
||||||
}) {
|
const rooms = watch("rooms") as GuestsRoom[]
|
||||||
const intl = useIntl()
|
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const checkIsDesktop = useMediaQuery("(min-width: 1367px)")
|
||||||
const { setValue } = useFormContext()
|
const [isDesktop, setIsDesktop] = useState(true)
|
||||||
const { rooms, adultCount, childCount, setIsValidated } = useGuestsRoomsStore(
|
|
||||||
(state) => ({
|
const htmlElement =
|
||||||
rooms: state.rooms,
|
typeof window !== "undefined" ? document.querySelector("body") : null
|
||||||
adultCount: state.adultCount,
|
//isOpen is the 'old state', so isOpen === true means "The modal is open and WILL be closed".
|
||||||
childCount: state.childCount,
|
function setOverflowClip(isOpen: boolean) {
|
||||||
setIsValidated: state.setIsValidated,
|
if (htmlElement) {
|
||||||
})
|
if (isOpen) {
|
||||||
)
|
htmlElement.style.overflow = "visible"
|
||||||
const ref = useRef<HTMLDivElement | null>(null)
|
|
||||||
function handleOnClick() {
|
|
||||||
setIsOpen((prevIsOpen) => !prevIsOpen)
|
|
||||||
}
|
|
||||||
const closePicker = useCallback(() => {
|
|
||||||
const guestRoomsValidData = guestRoomsSchema.safeParse(rooms)
|
|
||||||
if (guestRoomsValidData.success) {
|
|
||||||
setIsOpen(false)
|
|
||||||
setIsValidated(false)
|
|
||||||
setValue(name, guestRoomsValidData.data, { shouldValidate: true })
|
|
||||||
} else {
|
} else {
|
||||||
setIsValidated(true)
|
// !important needed to override 'overflow: hidden' set by react-aria.
|
||||||
|
// 'overflow: hidden' does not work in combination with other sticky positioned elements, which clip does.
|
||||||
|
htmlElement.style.overflow = "clip !important"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [rooms, name, setValue, setIsValidated, setIsOpen])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleClickOutside(evt: Event) {
|
setIsDesktop(checkIsDesktop)
|
||||||
const target = evt.target as HTMLElement
|
}, [checkIsDesktop])
|
||||||
if (ref.current && target && !ref.current.contains(target)) {
|
|
||||||
closePicker()
|
return isDesktop ? (
|
||||||
|
<DialogTrigger onOpenChange={setOverflowClip}>
|
||||||
|
<Trigger rooms={rooms} className={styles.triggerDesktop} />
|
||||||
|
<Popover placement="bottom start" offset={36}>
|
||||||
|
<Dialog className={styles.pickerContainerDesktop}>
|
||||||
|
{({ close }) => <PickerForm rooms={rooms} onClose={close} />}
|
||||||
|
</Dialog>
|
||||||
|
</Popover>
|
||||||
|
</DialogTrigger>
|
||||||
|
) : (
|
||||||
|
<DialogTrigger>
|
||||||
|
<Trigger rooms={rooms} className={styles.triggerMobile} />
|
||||||
|
<Modal>
|
||||||
|
<Dialog className={styles.pickerContainerMobile}>
|
||||||
|
{({ close }) => <PickerForm rooms={rooms} onClose={close} />}
|
||||||
|
</Dialog>
|
||||||
|
</Modal>
|
||||||
|
</DialogTrigger>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
document.addEventListener("click", handleClickOutside)
|
function Trigger({
|
||||||
return () => {
|
rooms,
|
||||||
document.removeEventListener("click", handleClickOutside)
|
className,
|
||||||
}
|
}: {
|
||||||
}, [closePicker])
|
rooms: GuestsRoom[]
|
||||||
|
className: string
|
||||||
|
}) {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container} data-isopen={isOpen} ref={ref}>
|
<Button className={`${className} ${styles.btn}`} type="button">
|
||||||
<button className={styles.btn} onClick={handleOnClick} 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 }
|
||||||
@@ -69,21 +88,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>
|
</Body>
|
||||||
</button>
|
</Button>
|
||||||
<div aria-modal className={styles.hideWrapper} role="dialog">
|
|
||||||
<GuestsRoomsPicker closePicker={closePicker} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,24 +98,38 @@ export default function RoomFilter({
|
|||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form onSubmit={handleSubmit(submitFilter)}>
|
<form onSubmit={handleSubmit(submitFilter)}>
|
||||||
<div className={styles.roomsFilter}>
|
<div className={styles.roomsFilter}>
|
||||||
{filterOptions.map((option) => (
|
{filterOptions.map((option) => {
|
||||||
|
const { code, description } = option
|
||||||
|
const isPetRoom = code === RoomPackageCodeEnum.PET_ROOM
|
||||||
|
const isAllergyRoom = code === RoomPackageCodeEnum.ALLERGY_ROOM
|
||||||
|
const isDisabled =
|
||||||
|
(isAllergyRoom && petFriendly) || (isPetRoom && allergyFriendly)
|
||||||
|
|
||||||
|
const checkboxChip = (
|
||||||
<CheckboxChip
|
<CheckboxChip
|
||||||
name={option.code}
|
name={code}
|
||||||
key={option.code}
|
key={code}
|
||||||
label={intl.formatMessage({ id: option.description })}
|
label={intl.formatMessage({ id: description })}
|
||||||
disabled={
|
disabled={isDisabled}
|
||||||
(option.code === RoomPackageCodeEnum.ALLERGY_ROOM &&
|
selected={getValues(code)}
|
||||||
petFriendly) ||
|
Icon={getIconForFeatureCode(code)}
|
||||||
(option.code === RoomPackageCodeEnum.PET_ROOM &&
|
hasTooltip={isPetRoom}
|
||||||
allergyFriendly)
|
|
||||||
}
|
|
||||||
selected={getValues(option.code)}
|
|
||||||
Icon={getIconForFeatureCode(option.code)}
|
|
||||||
/>
|
/>
|
||||||
))}
|
)
|
||||||
<Tooltip text={tooltipText} position="bottom" arrow="right">
|
|
||||||
<InfoCircleIcon color="uiTextPlaceholder" />
|
return isPetRoom ? (
|
||||||
|
<Tooltip
|
||||||
|
key={option.code}
|
||||||
|
text={tooltipText}
|
||||||
|
position="bottom"
|
||||||
|
arrow="right"
|
||||||
|
>
|
||||||
|
{checkboxChip}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
checkboxChip
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
|||||||
@@ -11,30 +11,38 @@ export function calculatePricesPerNight({
|
|||||||
}: CalculatePricesPerNightProps) {
|
}: CalculatePricesPerNightProps) {
|
||||||
const totalPublicLocalPricePerNight = publicLocalPrice
|
const totalPublicLocalPricePerNight = publicLocalPrice
|
||||||
? petRoomLocalPrice
|
? petRoomLocalPrice
|
||||||
? Number(publicLocalPrice.pricePerNight) +
|
? Math.floor(
|
||||||
|
Number(publicLocalPrice.pricePerNight) +
|
||||||
Number(petRoomLocalPrice.price) / nights
|
Number(petRoomLocalPrice.price) / nights
|
||||||
: Number(publicLocalPrice.pricePerNight)
|
)
|
||||||
|
: Math.floor(Number(publicLocalPrice.pricePerNight))
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const totalMemberLocalPricePerNight = memberLocalPrice
|
const totalMemberLocalPricePerNight = memberLocalPrice
|
||||||
? petRoomLocalPrice
|
? petRoomLocalPrice
|
||||||
? Number(memberLocalPrice.pricePerNight) +
|
? Math.floor(
|
||||||
|
Number(memberLocalPrice.pricePerNight) +
|
||||||
Number(petRoomLocalPrice.price) / nights
|
Number(petRoomLocalPrice.price) / nights
|
||||||
: Number(memberLocalPrice.pricePerNight)
|
)
|
||||||
|
: Math.floor(Number(memberLocalPrice.pricePerNight))
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const totalPublicRequestedPricePerNight = publicRequestedPrice
|
const totalPublicRequestedPricePerNight = publicRequestedPrice
|
||||||
? petRoomRequestedPrice
|
? petRoomRequestedPrice
|
||||||
? Number(publicRequestedPrice.pricePerNight) +
|
? Math.floor(
|
||||||
|
Number(publicRequestedPrice.pricePerNight) +
|
||||||
Number(petRoomRequestedPrice.price) / nights
|
Number(petRoomRequestedPrice.price) / nights
|
||||||
: Number(publicRequestedPrice.pricePerNight)
|
)
|
||||||
|
: Math.floor(Number(publicRequestedPrice.pricePerNight))
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const totalMemberRequestedPricePerNight = memberRequestedPrice
|
const totalMemberRequestedPricePerNight = memberRequestedPrice
|
||||||
? petRoomRequestedPrice
|
? petRoomRequestedPrice
|
||||||
? Number(memberRequestedPrice.pricePerNight) +
|
? Math.floor(
|
||||||
|
Number(memberRequestedPrice.pricePerNight) +
|
||||||
Number(petRoomRequestedPrice.price) / nights
|
Number(petRoomRequestedPrice.price) / nights
|
||||||
: Number(memberRequestedPrice.pricePerNight)
|
)
|
||||||
|
: Math.floor(Number(memberRequestedPrice.pricePerNight))
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default function RoomCard({
|
|||||||
(room) => room.name === roomConfiguration.roomType
|
(room) => room.name === roomConfiguration.roomType
|
||||||
)
|
)
|
||||||
|
|
||||||
const { roomSize, occupancy, descriptions, images } = selectedRoom || {}
|
const { roomSize, occupancy, images } = selectedRoom || {}
|
||||||
const mainImage = images?.[0]
|
const mainImage = images?.[0]
|
||||||
|
|
||||||
const freeCancelation = intl.formatMessage({ id: "Free cancellation" })
|
const freeCancelation = intl.formatMessage({ id: "Free cancellation" })
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ export default function RoomSelection({
|
|||||||
user,
|
user,
|
||||||
packages,
|
packages,
|
||||||
selectedPackages,
|
selectedPackages,
|
||||||
|
setRateSummary,
|
||||||
|
rateSummary,
|
||||||
}: RoomSelectionProps) {
|
}: RoomSelectionProps) {
|
||||||
const [rateSummary, setRateSummary] = useState<Rate | null>(null)
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const isUserLoggedIn = !!user
|
const isUserLoggedIn = !!user
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import {
|
|||||||
RoomPackageCodeEnum,
|
RoomPackageCodeEnum,
|
||||||
type RoomPackageCodes,
|
type RoomPackageCodes,
|
||||||
} from "@/types/components/hotelReservation/selectRate/roomFilter"
|
} from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
import type { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
import type { SelectRateProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
||||||
|
import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
import type {
|
import type {
|
||||||
RoomConfiguration,
|
RoomConfiguration,
|
||||||
RoomsAvailability,
|
RoomsAvailability,
|
||||||
@@ -23,10 +24,10 @@ export default function Rooms({
|
|||||||
roomCategories = [],
|
roomCategories = [],
|
||||||
user,
|
user,
|
||||||
packages,
|
packages,
|
||||||
}: Omit<RoomSelectionProps, "selectedPackages">) {
|
}: SelectRateProps) {
|
||||||
const visibleRooms: RoomConfiguration[] =
|
const visibleRooms: RoomConfiguration[] =
|
||||||
filterDuplicateRoomTypesByLowestPrice(roomsAvailability.roomConfigurations)
|
filterDuplicateRoomTypesByLowestPrice(roomsAvailability.roomConfigurations)
|
||||||
|
const [rateSummary, setRateSummary] = useState<Rate | null>(null)
|
||||||
const [rooms, setRooms] = useState<RoomsAvailability>({
|
const [rooms, setRooms] = useState<RoomsAvailability>({
|
||||||
...roomsAvailability,
|
...roomsAvailability,
|
||||||
roomConfigurations: visibleRooms,
|
roomConfigurations: visibleRooms,
|
||||||
@@ -48,6 +49,14 @@ export default function Rooms({
|
|||||||
...roomsAvailability,
|
...roomsAvailability,
|
||||||
roomConfigurations: visibleRooms,
|
roomConfigurations: visibleRooms,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!!rateSummary) {
|
||||||
|
setRateSummary({
|
||||||
|
...rateSummary,
|
||||||
|
features: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,8 +66,26 @@ export default function Rooms({
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
setRooms({ ...roomsAvailability, roomConfigurations: filteredRooms })
|
setRooms({ ...roomsAvailability, roomConfigurations: filteredRooms })
|
||||||
|
|
||||||
|
const petRoomPackage =
|
||||||
|
(filteredPackages.includes(RoomPackageCodeEnum.PET_ROOM) &&
|
||||||
|
packages.find((pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM)) ||
|
||||||
|
undefined
|
||||||
|
|
||||||
|
const features = filteredRooms.find((room) =>
|
||||||
|
room.features.some(
|
||||||
|
(feature) => feature.code === RoomPackageCodeEnum.PET_ROOM
|
||||||
|
)
|
||||||
|
)?.features
|
||||||
|
|
||||||
|
if (!!rateSummary) {
|
||||||
|
setRateSummary({
|
||||||
|
...rateSummary,
|
||||||
|
features: petRoomPackage && features ? features : [],
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[roomsAvailability, visibleRooms]
|
[roomsAvailability, visibleRooms, rateSummary, packages]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -74,6 +101,8 @@ export default function Rooms({
|
|||||||
user={user}
|
user={user}
|
||||||
packages={packages}
|
packages={packages}
|
||||||
selectedPackages={selectedPackages}
|
selectedPackages={selectedPackages}
|
||||||
|
setRateSummary={setRateSummary}
|
||||||
|
rateSummary={rateSummary}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
29
components/Icons/BedSingle.tsx
Normal file
29
components/Icons/BedSingle.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { iconVariants } from "./variants"
|
||||||
|
|
||||||
|
import type { IconProps } from "@/types/components/icon"
|
||||||
|
|
||||||
|
export default function BedSingleIcon({
|
||||||
|
className,
|
||||||
|
color,
|
||||||
|
...props
|
||||||
|
}: IconProps) {
|
||||||
|
const classNames = iconVariants({ className, color })
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className={classNames}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M15.0427 14.0548H4.95718V14.9742C4.95718 15.2154 4.87884 15.4189 4.72214 15.5847C4.56545 15.7505 4.37314 15.8334 4.14522 15.8334C3.9173 15.8334 3.72499 15.7505 3.56829 15.5847C3.4116 15.4189 3.33325 15.2154 3.33325 14.9742V10.9484C3.33325 10.3564 3.48757 9.83679 3.79621 9.38962C4.10486 8.94244 4.51084 8.61837 5.01416 8.41739V6.48802C5.01416 5.83485 5.22546 5.28468 5.64807 4.83751C6.07067 4.39033 6.58947 4.16675 7.20448 4.16675H12.7954C13.4104 4.16675 13.9292 4.39033 14.3518 4.83751C14.7744 5.28468 14.9857 5.83485 14.9857 6.48802V8.41739C15.489 8.61837 15.895 8.94244 16.2036 9.38962C16.5123 9.83679 16.6666 10.3564 16.6666 10.9484V14.9742C16.6666 15.2154 16.5882 15.4189 16.4315 15.5847C16.2748 15.7505 16.0825 15.8334 15.8546 15.8334C15.6267 15.8334 15.4344 15.7505 15.2777 15.5847C15.121 15.4189 15.0427 15.2154 15.0427 14.9742V14.0548ZM6.6381 8.23652V5.85495H13.3617V8.23652H6.6381ZM4.95718 10.9497V12.3364H15.0427V10.9497C15.0427 10.6683 14.9524 10.4322 14.772 10.2413C14.5916 10.0503 14.37 9.95486 14.1075 9.95486H5.89237C5.62979 9.95486 5.40828 10.0503 5.22784 10.2413C5.0474 10.4322 4.95718 10.6683 4.95718 10.9497Z"
|
||||||
|
fill="#57514E"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
29
components/Icons/KingBedSmall.tsx
Normal file
29
components/Icons/KingBedSmall.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { iconVariants } from "./variants"
|
||||||
|
|
||||||
|
import type { IconProps } from "@/types/components/icon"
|
||||||
|
|
||||||
|
export default function KingBedSmallIcon({
|
||||||
|
className,
|
||||||
|
color,
|
||||||
|
...props
|
||||||
|
}: IconProps) {
|
||||||
|
const classNames = iconVariants({ className, color })
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={classNames}
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M1.04175 10.0104C1.04175 9.57819 1.19474 9.21146 1.50073 8.91021C1.80671 8.60896 2.17455 8.45833 2.60425 8.45833H2.68758V6C2.68758 5.57031 2.84057 5.20247 3.14656 4.89648C3.45255 4.59049 3.82039 4.4375 4.25008 4.4375H15.7501C16.1798 4.4375 16.5476 4.59049 16.8536 4.89648C17.1596 5.20247 17.3126 5.57031 17.3126 6V8.45833H17.3959C17.8256 8.45833 18.1934 8.61133 18.4994 8.91731C18.8054 9.2233 18.9584 9.59114 18.9584 10.0208V12.9688C18.9584 13.2674 18.8542 13.5208 18.6459 13.7292C18.4376 13.9375 18.1843 14.0417 17.8862 14.0417H17.6459V14.9063C17.6459 15.1215 17.5695 15.3056 17.4167 15.4583C17.264 15.6111 17.0799 15.6875 16.8647 15.6875C16.6494 15.6875 16.4654 15.6111 16.3126 15.4583C16.1598 15.3056 16.0834 15.1215 16.0834 14.9063V14.0417H3.91675V14.9063C3.91675 15.1215 3.84036 15.3056 3.68758 15.4583C3.5348 15.6111 3.35078 15.6875 3.1355 15.6875C2.92022 15.6875 2.73619 15.6111 2.58341 15.4583C2.43064 15.3056 2.35425 15.1215 2.35425 14.9063V14.0417H2.11398C1.81582 14.0417 1.56258 13.9375 1.35425 13.7292C1.14591 13.5208 1.04175 13.2674 1.04175 12.9688V10.0104ZM15.7501 8.45833H10.7813V6H15.7501V8.45833ZM9.21883 8.45833H4.25008V6H9.21883V8.45833ZM17.3959 12.4792H2.60425V10.0208H17.3959V12.4792Z"
|
||||||
|
fill="#57514E"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ export { default as ArrowRightIcon } from "./ArrowRight"
|
|||||||
export { default as BarIcon } from "./Bar"
|
export { default as BarIcon } from "./Bar"
|
||||||
export { default as BathtubIcon } from "./Bathtub"
|
export { default as BathtubIcon } from "./Bathtub"
|
||||||
export { default as BedDoubleIcon } from "./BedDouble"
|
export { default as BedDoubleIcon } from "./BedDouble"
|
||||||
|
export { default as BedSingleIcon } from "./BedSingle"
|
||||||
export { default as BikingIcon } from "./Biking"
|
export { default as BikingIcon } from "./Biking"
|
||||||
export { default as BreakfastIcon } from "./Breakfast"
|
export { default as BreakfastIcon } from "./Breakfast"
|
||||||
export { default as BusinessIcon } from "./Business"
|
export { default as BusinessIcon } from "./Business"
|
||||||
@@ -77,6 +78,7 @@ export { default as IronIcon } from "./Iron"
|
|||||||
export { default as KayakingIcon } from "./Kayaking"
|
export { default as KayakingIcon } from "./Kayaking"
|
||||||
export { default as KettleIcon } from "./Kettle"
|
export { default as KettleIcon } from "./Kettle"
|
||||||
export { default as KingBedIcon } from "./KingBed"
|
export { default as KingBedIcon } from "./KingBed"
|
||||||
|
export { default as KingBedSmallIcon } from "./KingBedSmall"
|
||||||
export { default as LampIcon } from "./Lamp"
|
export { default as LampIcon } from "./Lamp"
|
||||||
export { default as LaundryMachineIcon } from "./LaundryMachine"
|
export { default as LaundryMachineIcon } from "./LaundryMachine"
|
||||||
export { default as LocalBarIcon } from "./LocalBar"
|
export { default as LocalBarIcon } from "./LocalBar"
|
||||||
|
|||||||
@@ -27,16 +27,17 @@
|
|||||||
.imagePlaceholder {
|
.imagePlaceholder {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 190px;
|
min-height: 190px;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
background-image: linear-gradient(45deg, #000000 25%, transparent 25%),
|
background-image: linear-gradient(45deg, #000000 25%, transparent 25%),
|
||||||
linear-gradient(-45deg, #000000 25%, transparent 25%),
|
linear-gradient(-45deg, #000000 25%, transparent 25%),
|
||||||
linear-gradient(45deg, transparent 75%, #000000 75%),
|
linear-gradient(45deg, transparent 75%, #000000 75%),
|
||||||
linear-gradient(-45deg, transparent 75%, #000000 75%);
|
linear-gradient(-45deg, transparent 75%, #000000 75%);
|
||||||
background-size: 160px 160px;
|
background-size: 180px 180px;
|
||||||
background-position:
|
background-position:
|
||||||
0 0,
|
0 0,
|
||||||
0 80px,
|
0 90px,
|
||||||
80px -80px,
|
90px -90px,
|
||||||
-80px 0;
|
-90px 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ export default function ImageGallery({
|
|||||||
}: ImageGalleryProps) {
|
}: ImageGalleryProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const [lightboxIsOpen, setLightboxIsOpen] = useState(false)
|
const [lightboxIsOpen, setLightboxIsOpen] = useState(false)
|
||||||
|
const [imageError, setImageError] = useState(false)
|
||||||
const imageProps = fill ? { fill } : { height, width: height * 1.5 }
|
const imageProps = fill ? { fill } : { height, width: height * 1.5 }
|
||||||
|
|
||||||
if (!images || images.length === 0) {
|
if (!images || images.length === 0 || imageError) {
|
||||||
return <div className={styles.imagePlaceholder} />
|
return <div className={styles.imagePlaceholder} />
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ export default function ImageGallery({
|
|||||||
className={styles.image}
|
className={styles.image}
|
||||||
src={images[0].imageSizes.medium}
|
src={images[0].imageSizes.medium}
|
||||||
alt={images[0].metaData.altText}
|
alt={images[0].metaData.altText}
|
||||||
|
onError={() => setImageError(true)}
|
||||||
{...imageProps}
|
{...imageProps}
|
||||||
/>
|
/>
|
||||||
<div className={styles.imageCount}>
|
<div className={styles.imageCount}>
|
||||||
|
|||||||
37
components/SidePeeks/RoomSidePeek/bedIcon.ts
Normal file
37
components/SidePeeks/RoomSidePeek/bedIcon.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { FC } from "react"
|
||||||
|
|
||||||
|
import {
|
||||||
|
BedDoubleIcon,
|
||||||
|
BedSingleIcon,
|
||||||
|
KingBedSmallIcon,
|
||||||
|
} from "@/components/Icons"
|
||||||
|
|
||||||
|
import type { IconProps } from "@/types/components/icon"
|
||||||
|
|
||||||
|
export function getBedIcon(name: string): FC<IconProps> | null {
|
||||||
|
const iconMappings = [
|
||||||
|
{
|
||||||
|
icon: BedDoubleIcon,
|
||||||
|
texts: ["Queen"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: KingBedSmallIcon,
|
||||||
|
texts: ["King"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: KingBedSmallIcon,
|
||||||
|
texts: ["CustomOccupancy"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: BedSingleIcon,
|
||||||
|
texts: ["Twin"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: BedSingleIcon,
|
||||||
|
texts: ["Single"],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const icon = iconMappings.find((icon) => icon.texts.includes(name))
|
||||||
|
return icon ? icon.icon : BedSingleIcon
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import ImageGallery from "@/components/ImageGallery"
|
import ImageGallery from "@/components/ImageGallery"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
|
||||||
import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
|
|
||||||
|
import { getBedIcon } from "./bedIcon"
|
||||||
import { getFacilityIcon } from "./facilityIcon"
|
import { getFacilityIcon } from "./facilityIcon"
|
||||||
|
|
||||||
import styles from "./roomSidePeek.module.css"
|
import styles from "./roomSidePeek.module.css"
|
||||||
@@ -79,15 +79,27 @@ export default function RoomSidePeek({
|
|||||||
<Body color="grey">
|
<Body color="grey">
|
||||||
{intl.formatMessage({ id: "booking.basedOnAvailability" })}
|
{intl.formatMessage({ id: "booking.basedOnAvailability" })}
|
||||||
</Body>
|
</Body>
|
||||||
{/* TODO: Get data for bed options */}
|
<ul className={styles.bedOptions}>
|
||||||
|
{room.roomTypes.map((roomType) => {
|
||||||
|
const BedIcon = getBedIcon(roomType.mainBed.type)
|
||||||
|
return (
|
||||||
|
<li key={roomType.code}>
|
||||||
|
{BedIcon && (
|
||||||
|
<BedIcon
|
||||||
|
color="uiTextMediumContrast"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Body color="uiTextMediumContrast" asChild>
|
||||||
|
<span>{roomType.mainBed.description}</span>
|
||||||
|
</Body>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.buttonContainer}>
|
|
||||||
<Button fullWidth theme="base" intent="primary">
|
|
||||||
{intl.formatMessage({ id: "booking.selectRoom" })}
|
|
||||||
{/* TODO: Implement logic for select room */}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</SidePeek>
|
</SidePeek>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,12 @@
|
|||||||
margin-bottom: var(--Spacing-x-half);
|
margin-bottom: var(--Spacing-x-half);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bedOptions li {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
margin-bottom: var(--Spacing-x-half);
|
||||||
|
}
|
||||||
|
|
||||||
.noIcon {
|
.noIcon {
|
||||||
margin-left: var(--Spacing-x4);
|
margin-left: var(--Spacing-x4);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import { useFormContext } from "react-hook-form"
|
import { useFormContext } from "react-hook-form"
|
||||||
|
|
||||||
import { HeartIcon } from "@/components/Icons"
|
import { HeartIcon, InfoCircleIcon } from "@/components/Icons"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
|
||||||
import styles from "./chip.module.css"
|
import styles from "./chip.module.css"
|
||||||
@@ -19,6 +19,7 @@ export default function FilterChip({
|
|||||||
value,
|
value,
|
||||||
selected,
|
selected,
|
||||||
disabled,
|
disabled,
|
||||||
|
hasTooltip,
|
||||||
}: FilterChipProps) {
|
}: FilterChipProps) {
|
||||||
const { register } = useFormContext()
|
const { register } = useFormContext()
|
||||||
|
|
||||||
@@ -43,6 +44,11 @@ export default function FilterChip({
|
|||||||
<Caption type="bold" color={color} className={styles.caption}>
|
<Caption type="bold" color={color} className={styles.caption}>
|
||||||
{label}
|
{label}
|
||||||
</Caption>
|
</Caption>
|
||||||
|
|
||||||
|
{hasTooltip && (
|
||||||
|
<InfoCircleIcon color={color} height={iconHeight} width={iconWidth} />
|
||||||
|
)}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
aria-hidden
|
aria-hidden
|
||||||
id={id || name}
|
id={id || name}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.modal {
|
.modal {
|
||||||
--sidepeek-desktop-width: 600px;
|
--sidepeek-desktop-width: 560px;
|
||||||
}
|
}
|
||||||
@keyframes slide-in {
|
@keyframes slide-in {
|
||||||
from {
|
from {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.tooltipContainer {
|
.tooltipContainer {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip {
|
.tooltip {
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -13,26 +13,23 @@ export type GuestsRoom = {
|
|||||||
child: Child[]
|
child: Child[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GuestsRoomsPickerProps {
|
|
||||||
closePicker: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GuestsRoomPickerProps = {
|
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 {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface FilterChipProps {
|
|||||||
value?: string
|
value?: string
|
||||||
selected?: boolean
|
selected?: boolean
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
hasTooltip?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FilterChipCheckboxProps = Omit<FilterChipProps, "type">
|
export type FilterChipCheckboxProps = Omit<FilterChipProps, "type">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { RoomData } from "@/types/hotel"
|
|||||||
import type { SafeUser } from "@/types/user"
|
import type { SafeUser } from "@/types/user"
|
||||||
import type { RoomsAvailability } from "@/server/routers/hotels/output"
|
import type { RoomsAvailability } from "@/server/routers/hotels/output"
|
||||||
import type { RoomPackageCodes, RoomPackageData } from "./roomFilter"
|
import type { RoomPackageCodes, RoomPackageData } from "./roomFilter"
|
||||||
|
import type { Rate } from "./selectRate"
|
||||||
|
|
||||||
export interface RoomSelectionProps {
|
export interface RoomSelectionProps {
|
||||||
roomsAvailability: RoomsAvailability
|
roomsAvailability: RoomsAvailability
|
||||||
@@ -9,4 +10,13 @@ export interface RoomSelectionProps {
|
|||||||
user: SafeUser
|
user: SafeUser
|
||||||
packages: RoomPackageData
|
packages: RoomPackageData
|
||||||
selectedPackages: RoomPackageCodes[]
|
selectedPackages: RoomPackageCodes[]
|
||||||
|
setRateSummary: (rateSummary: Rate) => void
|
||||||
|
rateSummary: Rate | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectRateProps {
|
||||||
|
roomsAvailability: RoomsAvailability
|
||||||
|
roomCategories: RoomData[]
|
||||||
|
user: SafeUser
|
||||||
|
packages: RoomPackageData
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user