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 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({
|
||||
params,
|
||||
|
||||
@@ -114,6 +114,8 @@
|
||||
/* Z-INDEX */
|
||||
--header-z-index: 11;
|
||||
--menu-overlay-z-index: 11;
|
||||
--booking-widget-z-index: 10;
|
||||
--booking-widget-open-z-index: 100;
|
||||
--dialog-z-index: 9;
|
||||
--sidepeek-z-index: 100;
|
||||
--lightbox-z-index: 150;
|
||||
|
||||
@@ -42,11 +42,11 @@ export default function BookingWidgetClient({
|
||||
|
||||
const bookingWidgetSearchData: BookingWidgetSearchParams | undefined =
|
||||
searchParams
|
||||
? (getFormattedUrlQueryParams(new URLSearchParams(searchParams), {
|
||||
? getFormattedUrlQueryParams(new URLSearchParams(searchParams), {
|
||||
adults: "number",
|
||||
age: "number",
|
||||
bed: "number",
|
||||
}) as BookingWidgetSearchParams)
|
||||
})
|
||||
: undefined
|
||||
|
||||
const getLocationObj = (destination: string): Location | undefined => {
|
||||
@@ -79,6 +79,16 @@ export default function BookingWidgetClient({
|
||||
)
|
||||
: undefined
|
||||
|
||||
const defaultRoomsData = bookingWidgetSearchData?.room?.map((room) => ({
|
||||
adults: room.adults,
|
||||
child: room.child ?? [],
|
||||
})) ?? [
|
||||
{
|
||||
adults: 1,
|
||||
child: [],
|
||||
},
|
||||
]
|
||||
|
||||
const methods = useForm<BookingWidgetSchema>({
|
||||
defaultValues: {
|
||||
search: selectedLocation?.name ?? "",
|
||||
@@ -96,12 +106,7 @@ export default function BookingWidgetClient({
|
||||
bookingCode: "",
|
||||
redemption: false,
|
||||
voucher: false,
|
||||
rooms: bookingWidgetSearchData?.room ?? [
|
||||
{
|
||||
adults: 1,
|
||||
child: [],
|
||||
},
|
||||
],
|
||||
rooms: defaultRoomsData,
|
||||
},
|
||||
shouldFocusError: false,
|
||||
mode: "all",
|
||||
@@ -158,21 +163,24 @@ export default function BookingWidgetClient({
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<section ref={bookingWidgetRef} className={styles.containerDesktop}>
|
||||
<Form locations={locations} type={type} />
|
||||
</section>
|
||||
<section className={styles.containerMobile} data-open={isOpen}>
|
||||
<button
|
||||
className={styles.close}
|
||||
onClick={closeMobileSearch}
|
||||
type="button"
|
||||
>
|
||||
<CloseLargeIcon />
|
||||
</button>
|
||||
<Form locations={locations} type={type} />
|
||||
<section
|
||||
ref={bookingWidgetRef}
|
||||
className={styles.wrapper}
|
||||
data-open={isOpen}
|
||||
>
|
||||
<MobileToggleButton openMobileSearch={openMobileSearch} />
|
||||
<div className={styles.formContainer}>
|
||||
<button
|
||||
className={styles.close}
|
||||
onClick={closeMobileSearch}
|
||||
type="button"
|
||||
>
|
||||
<CloseLargeIcon />
|
||||
</button>
|
||||
<Form locations={locations} type={type} />
|
||||
</div>
|
||||
</section>
|
||||
<div className={styles.backdrop} onClick={closeMobileSearch} />
|
||||
<MobileToggleButton openMobileSearch={openMobileSearch} />
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
display: grid;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
padding: var(--Spacing-x2);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
}
|
||||
|
||||
@@ -31,12 +31,6 @@ export default function MobileToggleButton({
|
||||
const location = useWatch({ name: "location" })
|
||||
const rooms: BookingWidgetSchema["rooms"] = useWatch({ name: "rooms" })
|
||||
|
||||
const bookingWidgetMobileRef = useRef(null)
|
||||
useStickyPosition({
|
||||
ref: bookingWidgetMobileRef,
|
||||
name: StickyElementNameEnum.BOOKING_WIDGET_MOBILE,
|
||||
})
|
||||
|
||||
const parsedLocation: Location | null = location
|
||||
? JSON.parse(decodeURIComponent(location))
|
||||
: null
|
||||
@@ -67,7 +61,6 @@ export default function MobileToggleButton({
|
||||
className={locationAndDateIsSet ? styles.complete : styles.partial}
|
||||
onClick={openMobileSearch}
|
||||
role="button"
|
||||
ref={bookingWidgetMobileRef}
|
||||
>
|
||||
{!locationAndDateIsSet && (
|
||||
<>
|
||||
|
||||
@@ -1,60 +1,63 @@
|
||||
.containerDesktop,
|
||||
.containerMobile,
|
||||
.close {
|
||||
display: none;
|
||||
.wrapper {
|
||||
position: sticky;
|
||||
z-index: var(--booking-widget-z-index);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.containerMobile {
|
||||
background-color: var(--UI-Input-Controls-Surface-Normal);
|
||||
bottom: -100%;
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3);
|
||||
grid-template-rows: 36px 1fr;
|
||||
height: calc(100dvh - 20px);
|
||||
padding: var(--Spacing-x3) var(--Spacing-x2) var(--Spacing-x7);
|
||||
position: fixed;
|
||||
transition: bottom 300ms ease;
|
||||
width: 100%;
|
||||
z-index: 10000;
|
||||
border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0;
|
||||
}
|
||||
.formContainer {
|
||||
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);
|
||||
height: calc(100dvh - 20px);
|
||||
width: 100%;
|
||||
padding: var(--Spacing-x3) var(--Spacing-x2) var(--Spacing-x7);
|
||||
position: fixed;
|
||||
bottom: -100%;
|
||||
transition: bottom 300ms ease;
|
||||
}
|
||||
|
||||
.containerMobile[data-open="true"] {
|
||||
bottom: 0;
|
||||
}
|
||||
.wrapper[data-open="true"] {
|
||||
z-index: var(--booking-widget-open-z-index);
|
||||
}
|
||||
|
||||
.close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
justify-self: flex-end;
|
||||
}
|
||||
.wrapper[data-open="true"] .formContainer {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.containerMobile[data-open="true"] + .backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
.close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
justify-self: flex-end;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.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) {
|
||||
.containerDesktop {
|
||||
display: block;
|
||||
box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.05);
|
||||
position: sticky;
|
||||
.wrapper {
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.container {
|
||||
z-index: 9;
|
||||
.formContainer {
|
||||
display: block;
|
||||
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 {
|
||||
--header-height: 68px;
|
||||
--header-height: 72px;
|
||||
--sticky-button-height: 120px;
|
||||
|
||||
display: grid;
|
||||
@@ -11,12 +11,10 @@
|
||||
}
|
||||
|
||||
.header {
|
||||
align-self: flex-start;
|
||||
align-self: flex-end;
|
||||
background-color: var(--Main-Grey-White);
|
||||
display: grid;
|
||||
grid-area: header;
|
||||
grid-template-columns: 1fr 24px;
|
||||
padding: var(--Spacing-x3) var(--Spacing-x2) var(--Spacing-x2);
|
||||
padding: var(--Spacing-x3) var(--Spacing-x2);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
.hideWrapper {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
|
||||
@@ -6,7 +6,6 @@ import { dt } from "@/lib/dt"
|
||||
|
||||
import DatePicker from "@/components/DatePicker"
|
||||
import GuestsRoomsPickerForm from "@/components/GuestsRoomsPicker"
|
||||
import GuestsRoomsProvider from "@/components/GuestsRoomsPicker/Provider/GuestsRoomsProvider"
|
||||
import { SearchIcon } from "@/components/Icons"
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
@@ -26,12 +25,10 @@ export default function FormContent({
|
||||
const intl = useIntl()
|
||||
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 selectedGuests = useWatch({ name: "rooms" })
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.input}>
|
||||
@@ -51,12 +48,10 @@ export default function FormContent({
|
||||
<div className={styles.rooms}>
|
||||
<label>
|
||||
<Caption color="red" type="bold" asChild>
|
||||
<span>{rooms}</span>
|
||||
<span>{roomsLabel}</span>
|
||||
</Caption>
|
||||
</label>
|
||||
<GuestsRoomsProvider selectedGuests={selectedGuests}>
|
||||
<GuestsRoomsPickerForm name="rooms" />
|
||||
</GuestsRoomsProvider>
|
||||
<GuestsRoomsPickerForm />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.voucherContainer}>
|
||||
|
||||
@@ -1,16 +1,37 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||
import type { Location } from "@/types/trpc/routers/hotel/locations"
|
||||
|
||||
export const guestRoomSchema = z.object({
|
||||
adults: z.number().default(1),
|
||||
child: z.array(
|
||||
z.object({
|
||||
age: z.number().nonnegative(),
|
||||
bed: z.number(),
|
||||
})
|
||||
),
|
||||
})
|
||||
export const guestRoomSchema = z
|
||||
.object({
|
||||
adults: z.number().default(1),
|
||||
child: z
|
||||
.array(
|
||||
z.object({
|
||||
age: z.number().min(0, "Age is required"),
|
||||
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)
|
||||
|
||||
|
||||
@@ -3,54 +3,32 @@
|
||||
import { useFormContext } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useGuestsRoomsStore } from "@/stores/guests-rooms"
|
||||
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import Counter from "../Counter"
|
||||
|
||||
import styles from "./adult-selector.module.css"
|
||||
|
||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||
import {
|
||||
AdultSelectorProps,
|
||||
Child,
|
||||
} from "@/types/components/bookingWidget/guestsRoomsPicker"
|
||||
import { SelectorProps } 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 adultsLabel = intl.formatMessage({ id: "Adults" })
|
||||
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) {
|
||||
if (adults < 6) {
|
||||
increaseAdults(roomIndex)
|
||||
setValue(`rooms.${roomIndex}.adults`, adults + 1)
|
||||
function increaseAdultsCount() {
|
||||
if (currentAdults < 6) {
|
||||
setValue(name, currentAdults + 1)
|
||||
}
|
||||
}
|
||||
|
||||
function decreaseAdultsCount(roomIndex: number) {
|
||||
if (adults > 1) {
|
||||
decreaseAdults(roomIndex)
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
function decreaseAdultsCount() {
|
||||
if (currentAdults > 1) {
|
||||
setValue(name, currentAdults - 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,15 +38,11 @@ export default function AdultSelector({ roomIndex = 0 }: AdultSelectorProps) {
|
||||
{adultsLabel}
|
||||
</Caption>
|
||||
<Counter
|
||||
count={adults}
|
||||
handleOnDecrease={() => {
|
||||
decreaseAdultsCount(roomIndex)
|
||||
}}
|
||||
handleOnIncrease={() => {
|
||||
increaseAdultsCount(roomIndex)
|
||||
}}
|
||||
disableDecrease={adults == 1}
|
||||
disableIncrease={adults == 6}
|
||||
count={currentAdults}
|
||||
handleOnDecrease={decreaseAdultsCount}
|
||||
handleOnIncrease={increaseAdultsCount}
|
||||
disableDecrease={currentAdults == 1}
|
||||
disableIncrease={currentAdults == 6}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
import { useFormContext } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useGuestsRoomsStore } from "@/stores/guests-rooms"
|
||||
|
||||
import { ErrorCircleIcon } from "@/components/Icons"
|
||||
import Select from "@/components/TempDesignSystem/Select"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
@@ -17,55 +15,35 @@ import {
|
||||
ChildInfoSelectorProps,
|
||||
} from "@/types/components/bookingWidget/guestsRoomsPicker"
|
||||
|
||||
const ageList = [...Array(13)].map((_, i) => ({
|
||||
label: i.toString(),
|
||||
value: i,
|
||||
}))
|
||||
|
||||
export default function ChildInfoSelector({
|
||||
child = { age: -1, bed: -1 },
|
||||
childrenInAdultsBed,
|
||||
adults,
|
||||
index = 0,
|
||||
roomIndex = 0,
|
||||
}: ChildInfoSelectorProps) {
|
||||
const ageFieldName = `rooms.${roomIndex}.child.${index}.age`
|
||||
const bedFieldName = `rooms.${roomIndex}.child.${index}.bed`
|
||||
const intl = useIntl()
|
||||
const ageLabel = intl.formatMessage({ id: "Age" })
|
||||
const ageReqdErrMsg = intl.formatMessage({ id: "Child age is required" })
|
||||
const bedLabel = intl.formatMessage({ id: "Bed" })
|
||||
const { setValue } = 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) => ({
|
||||
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)
|
||||
}
|
||||
const errorMessage = intl.formatMessage({ id: "Child age is required" })
|
||||
const { setValue, formState, register, trigger } = useFormContext()
|
||||
|
||||
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)
|
||||
trigger()
|
||||
}
|
||||
|
||||
function updateSelectedAge(age: number) {
|
||||
setValue(`rooms.${roomIndex}.child.${index}.age`, age)
|
||||
const availableBedTypes = getAvailableBeds(age)
|
||||
updateSelectedBed(availableBedTypes[0].value)
|
||||
}
|
||||
|
||||
const allBedTypes: ChildBed[] = [
|
||||
@@ -97,6 +75,12 @@ export default function ChildInfoSelector({
|
||||
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 (
|
||||
<>
|
||||
<div key={index} className={styles.childInfoContainer}>
|
||||
@@ -110,13 +94,15 @@ export default function ChildInfoSelector({
|
||||
onSelect={(key) => {
|
||||
updateSelectedAge(key as number)
|
||||
}}
|
||||
name={`rooms.${roomIndex}.child.${index}.age`}
|
||||
placeholder={ageLabel}
|
||||
maxHeight={150}
|
||||
{...register(ageFieldName, {
|
||||
required: true,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{child.age !== -1 ? (
|
||||
{child.age >= 0 ? (
|
||||
<Select
|
||||
items={getAvailableBeds(child.age)}
|
||||
label={bedLabel}
|
||||
@@ -125,16 +111,26 @@ export default function ChildInfoSelector({
|
||||
onSelect={(key) => {
|
||||
updateSelectedBed(key as number)
|
||||
}}
|
||||
name={`rooms.${roomIndex}.child.${index}.age`}
|
||||
placeholder={bedLabel}
|
||||
{...register(bedFieldName, {
|
||||
required: true,
|
||||
})}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{isValidated && child.age < 0 ? (
|
||||
|
||||
{roomErrors && roomErrors.message ? (
|
||||
<Caption color="red" className={styles.error}>
|
||||
<ErrorCircleIcon color="red" />
|
||||
{ageReqdErrMsg}
|
||||
{roomErrors.message}
|
||||
</Caption>
|
||||
) : null}
|
||||
|
||||
{ageError || bedError ? (
|
||||
<Caption color="red" className={styles.error}>
|
||||
<ErrorCircleIcon color="red" />
|
||||
{errorMessage}
|
||||
</Caption>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
import { useFormContext } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useGuestsRoomsStore } from "@/stores/guests-rooms"
|
||||
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import Counter from "../Counter"
|
||||
@@ -12,40 +10,30 @@ import ChildInfoSelector from "./ChildInfoSelector"
|
||||
|
||||
import styles from "./child-selector.module.css"
|
||||
|
||||
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 childrenLabel = intl.formatMessage({ id: "Children" })
|
||||
const { setValue, trigger } = useFormContext<BookingWidgetSchema>()
|
||||
const children = useGuestsRoomsStore((state) => state.rooms[roomIndex].child)
|
||||
const increaseChildren = useGuestsRoomsStore(
|
||||
(state) => state.increaseChildren
|
||||
)
|
||||
const decreaseChildren = useGuestsRoomsStore(
|
||||
(state) => state.decreaseChildren
|
||||
)
|
||||
const { setValue } = useFormContext()
|
||||
|
||||
function increaseChildrenCount(roomIndex: number) {
|
||||
if (children.length < 5) {
|
||||
increaseChildren(roomIndex)
|
||||
setValue(
|
||||
`rooms.${roomIndex}.child.${children.length}`,
|
||||
{
|
||||
age: -1,
|
||||
bed: -1,
|
||||
},
|
||||
{ shouldValidate: true }
|
||||
)
|
||||
if (currentChildren.length < 5) {
|
||||
setValue(`rooms.${roomIndex}.child.${currentChildren.length}`, {
|
||||
age: undefined,
|
||||
bed: undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
function decreaseChildrenCount(roomIndex: number) {
|
||||
if (children.length > 0) {
|
||||
const newChildrenList = decreaseChildren(roomIndex)
|
||||
setValue(`rooms.${roomIndex}.child`, newChildrenList, {
|
||||
shouldValidate: true,
|
||||
})
|
||||
if (currentChildren.length > 0) {
|
||||
currentChildren.pop()
|
||||
setValue(`rooms.${roomIndex}.child`, currentChildren)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,23 +44,25 @@ export default function ChildSelector({ roomIndex = 0 }: ChildSelectorProps) {
|
||||
{childrenLabel}
|
||||
</Caption>
|
||||
<Counter
|
||||
count={children.length}
|
||||
count={currentChildren.length}
|
||||
handleOnDecrease={() => {
|
||||
decreaseChildrenCount(roomIndex)
|
||||
}}
|
||||
handleOnIncrease={() => {
|
||||
increaseChildrenCount(roomIndex)
|
||||
}}
|
||||
disableDecrease={children.length == 0}
|
||||
disableIncrease={children.length == 5}
|
||||
disableDecrease={currentChildren.length == 0}
|
||||
disableIncrease={currentChildren.length == 5}
|
||||
/>
|
||||
</section>
|
||||
{children.map((child, index) => (
|
||||
{currentChildren.map((child, index) => (
|
||||
<ChildInfoSelector
|
||||
roomIndex={roomIndex}
|
||||
index={index}
|
||||
child={child}
|
||||
adults={currentAdults}
|
||||
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 {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
&[data-isopen="true"] {
|
||||
overflow: visible;
|
||||
}
|
||||
.triggerDesktop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
@@ -14,9 +36,6 @@
|
||||
gap: var(--Spacing-x2);
|
||||
padding-bottom: var(--Spacing-x1);
|
||||
}
|
||||
.hideWrapper {
|
||||
background-color: var(--Main-Grey-White);
|
||||
}
|
||||
.roomHeading {
|
||||
margin-bottom: var(--Spacing-x1);
|
||||
}
|
||||
@@ -29,43 +48,14 @@
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
.body {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x1);
|
||||
grid-template-columns: auto;
|
||||
margin-top: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
@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 {
|
||||
grid-area: content;
|
||||
overflow-y: scroll;
|
||||
@@ -73,7 +63,6 @@
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: var(--Main-Grey-White);
|
||||
display: grid;
|
||||
grid-area: header;
|
||||
padding: var(--Spacing-x3) var(--Spacing-x2);
|
||||
@@ -101,11 +90,10 @@
|
||||
rgba(255, 255, 255, 0) 7.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;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.footer .hideOnMobile {
|
||||
@@ -121,17 +109,40 @@
|
||||
}
|
||||
|
||||
@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);
|
||||
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);
|
||||
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||
position: absolute;
|
||||
top: calc(100% + var(--Spacing-x2) + 1px + var(--Spacing-x4));
|
||||
width: 360px;
|
||||
max-height: calc(100dvh - 77px - var(--Spacing-x6));
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.pickerContainerDesktop:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
@@ -140,6 +151,7 @@
|
||||
|
||||
.footer {
|
||||
grid-template-columns: auto auto;
|
||||
padding-top: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.footer .hideOnDesktop,
|
||||
|
||||
@@ -1,67 +1,86 @@
|
||||
"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 { 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 GuestsRoomsPicker from "./GuestsRoomsPicker"
|
||||
import PickerForm from "./Form"
|
||||
|
||||
import styles from "./guests-rooms-picker.module.css"
|
||||
|
||||
export default function GuestsRoomsPickerForm({
|
||||
name = "rooms",
|
||||
}: {
|
||||
name: string
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const { setValue } = useFormContext()
|
||||
const { rooms, adultCount, childCount, setIsValidated } = useGuestsRoomsStore(
|
||||
(state) => ({
|
||||
rooms: state.rooms,
|
||||
adultCount: state.adultCount,
|
||||
childCount: state.childCount,
|
||||
setIsValidated: state.setIsValidated,
|
||||
})
|
||||
)
|
||||
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 {
|
||||
setIsValidated(true)
|
||||
}
|
||||
}, [rooms, name, setValue, setIsValidated, setIsOpen])
|
||||
import { GuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker"
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(evt: Event) {
|
||||
const target = evt.target as HTMLElement
|
||||
if (ref.current && target && !ref.current.contains(target)) {
|
||||
closePicker()
|
||||
export default function GuestsRoomsPickerForm() {
|
||||
const { watch } = useFormContext()
|
||||
const rooms = watch("rooms") as GuestsRoom[]
|
||||
|
||||
const checkIsDesktop = useMediaQuery("(min-width: 1367px)")
|
||||
const [isDesktop, setIsDesktop] = useState(true)
|
||||
|
||||
const htmlElement =
|
||||
typeof window !== "undefined" ? document.querySelector("body") : null
|
||||
//isOpen is the 'old state', so isOpen === true means "The modal is open and WILL be closed".
|
||||
function setOverflowClip(isOpen: boolean) {
|
||||
if (htmlElement) {
|
||||
if (isOpen) {
|
||||
htmlElement.style.overflow = "visible"
|
||||
} else {
|
||||
// !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"
|
||||
}
|
||||
}
|
||||
document.addEventListener("click", handleClickOutside)
|
||||
return () => {
|
||||
document.removeEventListener("click", handleClickOutside)
|
||||
}
|
||||
}, [closePicker])
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setIsDesktop(checkIsDesktop)
|
||||
}, [checkIsDesktop])
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
function Trigger({
|
||||
rooms,
|
||||
className,
|
||||
}: {
|
||||
rooms: GuestsRoom[]
|
||||
className: string
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<div className={styles.container} data-isopen={isOpen} ref={ref}>
|
||||
<button className={styles.btn} onClick={handleOnClick} type="button">
|
||||
<Body className={styles.body} asChild>
|
||||
<span>
|
||||
<Button className={`${className} ${styles.btn}`} type="button">
|
||||
<Body>
|
||||
{rooms.map((room, i) => (
|
||||
<span key={i}>
|
||||
{intl.formatMessage(
|
||||
{ id: "booking.rooms" },
|
||||
{ totalRooms: rooms.length }
|
||||
@@ -69,21 +88,18 @@ export default function GuestsRoomsPickerForm({
|
||||
{", "}
|
||||
{intl.formatMessage(
|
||||
{ id: "booking.adults" },
|
||||
{ totalAdults: adultCount }
|
||||
{ totalAdults: room.adults }
|
||||
)}
|
||||
{childCount > 0
|
||||
{room.child.length > 0
|
||||
? ", " +
|
||||
intl.formatMessage(
|
||||
{ id: "booking.children" },
|
||||
{ totalChildren: childCount }
|
||||
{ totalChildren: room.child.length }
|
||||
)
|
||||
: null}
|
||||
</span>
|
||||
</Body>
|
||||
</button>
|
||||
<div aria-modal className={styles.hideWrapper} role="dialog">
|
||||
<GuestsRoomsPicker closePicker={closePicker} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Body>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -98,24 +98,38 @@ export default function RoomFilter({
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={handleSubmit(submitFilter)}>
|
||||
<div className={styles.roomsFilter}>
|
||||
{filterOptions.map((option) => (
|
||||
<CheckboxChip
|
||||
name={option.code}
|
||||
key={option.code}
|
||||
label={intl.formatMessage({ id: option.description })}
|
||||
disabled={
|
||||
(option.code === RoomPackageCodeEnum.ALLERGY_ROOM &&
|
||||
petFriendly) ||
|
||||
(option.code === RoomPackageCodeEnum.PET_ROOM &&
|
||||
allergyFriendly)
|
||||
}
|
||||
selected={getValues(option.code)}
|
||||
Icon={getIconForFeatureCode(option.code)}
|
||||
/>
|
||||
))}
|
||||
<Tooltip text={tooltipText} position="bottom" arrow="right">
|
||||
<InfoCircleIcon color="uiTextPlaceholder" />
|
||||
</Tooltip>
|
||||
{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
|
||||
name={code}
|
||||
key={code}
|
||||
label={intl.formatMessage({ id: description })}
|
||||
disabled={isDisabled}
|
||||
selected={getValues(code)}
|
||||
Icon={getIconForFeatureCode(code)}
|
||||
hasTooltip={isPetRoom}
|
||||
/>
|
||||
)
|
||||
|
||||
return isPetRoom ? (
|
||||
<Tooltip
|
||||
key={option.code}
|
||||
text={tooltipText}
|
||||
position="bottom"
|
||||
arrow="right"
|
||||
>
|
||||
{checkboxChip}
|
||||
</Tooltip>
|
||||
) : (
|
||||
checkboxChip
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
|
||||
@@ -11,30 +11,38 @@ export function calculatePricesPerNight({
|
||||
}: CalculatePricesPerNightProps) {
|
||||
const totalPublicLocalPricePerNight = publicLocalPrice
|
||||
? petRoomLocalPrice
|
||||
? Number(publicLocalPrice.pricePerNight) +
|
||||
Number(petRoomLocalPrice.price) / nights
|
||||
: Number(publicLocalPrice.pricePerNight)
|
||||
? Math.floor(
|
||||
Number(publicLocalPrice.pricePerNight) +
|
||||
Number(petRoomLocalPrice.price) / nights
|
||||
)
|
||||
: Math.floor(Number(publicLocalPrice.pricePerNight))
|
||||
: undefined
|
||||
|
||||
const totalMemberLocalPricePerNight = memberLocalPrice
|
||||
? petRoomLocalPrice
|
||||
? Number(memberLocalPrice.pricePerNight) +
|
||||
Number(petRoomLocalPrice.price) / nights
|
||||
: Number(memberLocalPrice.pricePerNight)
|
||||
? Math.floor(
|
||||
Number(memberLocalPrice.pricePerNight) +
|
||||
Number(petRoomLocalPrice.price) / nights
|
||||
)
|
||||
: Math.floor(Number(memberLocalPrice.pricePerNight))
|
||||
: undefined
|
||||
|
||||
const totalPublicRequestedPricePerNight = publicRequestedPrice
|
||||
? petRoomRequestedPrice
|
||||
? Number(publicRequestedPrice.pricePerNight) +
|
||||
Number(petRoomRequestedPrice.price) / nights
|
||||
: Number(publicRequestedPrice.pricePerNight)
|
||||
? Math.floor(
|
||||
Number(publicRequestedPrice.pricePerNight) +
|
||||
Number(petRoomRequestedPrice.price) / nights
|
||||
)
|
||||
: Math.floor(Number(publicRequestedPrice.pricePerNight))
|
||||
: undefined
|
||||
|
||||
const totalMemberRequestedPricePerNight = memberRequestedPrice
|
||||
? petRoomRequestedPrice
|
||||
? Number(memberRequestedPrice.pricePerNight) +
|
||||
Number(petRoomRequestedPrice.price) / nights
|
||||
: Number(memberRequestedPrice.pricePerNight)
|
||||
? Math.floor(
|
||||
Number(memberRequestedPrice.pricePerNight) +
|
||||
Number(petRoomRequestedPrice.price) / nights
|
||||
)
|
||||
: Math.floor(Number(memberRequestedPrice.pricePerNight))
|
||||
: undefined
|
||||
|
||||
return {
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function RoomCard({
|
||||
(room) => room.name === roomConfiguration.roomType
|
||||
)
|
||||
|
||||
const { roomSize, occupancy, descriptions, images } = selectedRoom || {}
|
||||
const { roomSize, occupancy, images } = selectedRoom || {}
|
||||
const mainImage = images?.[0]
|
||||
|
||||
const freeCancelation = intl.formatMessage({ id: "Free cancellation" })
|
||||
|
||||
@@ -17,9 +17,9 @@ export default function RoomSelection({
|
||||
user,
|
||||
packages,
|
||||
selectedPackages,
|
||||
setRateSummary,
|
||||
rateSummary,
|
||||
}: RoomSelectionProps) {
|
||||
const [rateSummary, setRateSummary] = useState<Rate | null>(null)
|
||||
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const isUserLoggedIn = !!user
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
RoomPackageCodeEnum,
|
||||
type RoomPackageCodes,
|
||||
} 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 {
|
||||
RoomConfiguration,
|
||||
RoomsAvailability,
|
||||
@@ -23,10 +24,10 @@ export default function Rooms({
|
||||
roomCategories = [],
|
||||
user,
|
||||
packages,
|
||||
}: Omit<RoomSelectionProps, "selectedPackages">) {
|
||||
}: SelectRateProps) {
|
||||
const visibleRooms: RoomConfiguration[] =
|
||||
filterDuplicateRoomTypesByLowestPrice(roomsAvailability.roomConfigurations)
|
||||
|
||||
const [rateSummary, setRateSummary] = useState<Rate | null>(null)
|
||||
const [rooms, setRooms] = useState<RoomsAvailability>({
|
||||
...roomsAvailability,
|
||||
roomConfigurations: visibleRooms,
|
||||
@@ -48,6 +49,14 @@ export default function Rooms({
|
||||
...roomsAvailability,
|
||||
roomConfigurations: visibleRooms,
|
||||
})
|
||||
|
||||
if (!!rateSummary) {
|
||||
setRateSummary({
|
||||
...rateSummary,
|
||||
features: [],
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -57,8 +66,26 @@ export default function Rooms({
|
||||
)
|
||||
)
|
||||
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 (
|
||||
@@ -74,6 +101,8 @@ export default function Rooms({
|
||||
user={user}
|
||||
packages={packages}
|
||||
selectedPackages={selectedPackages}
|
||||
setRateSummary={setRateSummary}
|
||||
rateSummary={rateSummary}
|
||||
/>
|
||||
</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 BathtubIcon } from "./Bathtub"
|
||||
export { default as BedDoubleIcon } from "./BedDouble"
|
||||
export { default as BedSingleIcon } from "./BedSingle"
|
||||
export { default as BikingIcon } from "./Biking"
|
||||
export { default as BreakfastIcon } from "./Breakfast"
|
||||
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 KettleIcon } from "./Kettle"
|
||||
export { default as KingBedIcon } from "./KingBed"
|
||||
export { default as KingBedSmallIcon } from "./KingBedSmall"
|
||||
export { default as LampIcon } from "./Lamp"
|
||||
export { default as LaundryMachineIcon } from "./LaundryMachine"
|
||||
export { default as LocalBarIcon } from "./LocalBar"
|
||||
|
||||
@@ -27,16 +27,17 @@
|
||||
.imagePlaceholder {
|
||||
height: 100%;
|
||||
min-height: 190px;
|
||||
aspect-ratio: 16/9;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
background-image: 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%);
|
||||
background-size: 160px 160px;
|
||||
background-size: 180px 180px;
|
||||
background-position:
|
||||
0 0,
|
||||
0 80px,
|
||||
80px -80px,
|
||||
-80px 0;
|
||||
0 90px,
|
||||
90px -90px,
|
||||
-90px 0;
|
||||
}
|
||||
|
||||
@@ -20,9 +20,10 @@ export default function ImageGallery({
|
||||
}: ImageGalleryProps) {
|
||||
const intl = useIntl()
|
||||
const [lightboxIsOpen, setLightboxIsOpen] = useState(false)
|
||||
const [imageError, setImageError] = useState(false)
|
||||
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} />
|
||||
}
|
||||
|
||||
@@ -38,6 +39,7 @@ export default function ImageGallery({
|
||||
className={styles.image}
|
||||
src={images[0].imageSizes.medium}
|
||||
alt={images[0].metaData.altText}
|
||||
onError={() => setImageError(true)}
|
||||
{...imageProps}
|
||||
/>
|
||||
<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 ImageGallery from "@/components/ImageGallery"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
|
||||
import { getBedIcon } from "./bedIcon"
|
||||
import { getFacilityIcon } from "./facilityIcon"
|
||||
|
||||
import styles from "./roomSidePeek.module.css"
|
||||
@@ -79,15 +79,27 @@ export default function RoomSidePeek({
|
||||
<Body color="grey">
|
||||
{intl.formatMessage({ id: "booking.basedOnAvailability" })}
|
||||
</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 className={styles.buttonContainer}>
|
||||
<Button fullWidth theme="base" intent="primary">
|
||||
{intl.formatMessage({ id: "booking.selectRoom" })}
|
||||
{/* TODO: Implement logic for select room */}
|
||||
</Button>
|
||||
</div>
|
||||
</SidePeek>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -44,6 +44,12 @@
|
||||
margin-bottom: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.bedOptions li {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
margin-bottom: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.noIcon {
|
||||
margin-left: var(--Spacing-x4);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo } from "react"
|
||||
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 styles from "./chip.module.css"
|
||||
@@ -19,6 +19,7 @@ export default function FilterChip({
|
||||
value,
|
||||
selected,
|
||||
disabled,
|
||||
hasTooltip,
|
||||
}: FilterChipProps) {
|
||||
const { register } = useFormContext()
|
||||
|
||||
@@ -43,6 +44,11 @@ export default function FilterChip({
|
||||
<Caption type="bold" color={color} className={styles.caption}>
|
||||
{label}
|
||||
</Caption>
|
||||
|
||||
{hasTooltip && (
|
||||
<InfoCircleIcon color={color} height={iconHeight} width={iconWidth} />
|
||||
)}
|
||||
|
||||
<input
|
||||
aria-hidden
|
||||
id={id || name}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.modal {
|
||||
--sidepeek-desktop-width: 600px;
|
||||
--sidepeek-desktop-width: 560px;
|
||||
}
|
||||
@keyframes slide-in {
|
||||
from {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.tooltipContainer {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.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 {
|
||||
SITEWIDE_ALERT = "SITEWIDE_ALERT",
|
||||
BOOKING_WIDGET = "BOOKING_WIDGET",
|
||||
BOOKING_WIDGET_MOBILE = "BOOKING_WIDGET_MOBILE",
|
||||
HOTEL_TAB_NAVIGATION = "HOTEL_TAB_NAVIGATION",
|
||||
HOTEL_STATIC_MAP = "HOTEL_STATIC_MAP",
|
||||
}
|
||||
@@ -32,7 +31,6 @@ interface StickyStore {
|
||||
const priorityMap: Record<StickyElementNameEnum, number> = {
|
||||
[StickyElementNameEnum.SITEWIDE_ALERT]: 1,
|
||||
[StickyElementNameEnum.BOOKING_WIDGET]: 2,
|
||||
[StickyElementNameEnum.BOOKING_WIDGET_MOBILE]: 2,
|
||||
|
||||
[StickyElementNameEnum.HOTEL_TAB_NAVIGATION]: 3,
|
||||
[StickyElementNameEnum.HOTEL_STATIC_MAP]: 3,
|
||||
|
||||
@@ -13,26 +13,23 @@ export type GuestsRoom = {
|
||||
child: Child[]
|
||||
}
|
||||
|
||||
export interface GuestsRoomsPickerProps {
|
||||
closePicker: () => void
|
||||
}
|
||||
|
||||
export type GuestsRoomPickerProps = {
|
||||
index: number
|
||||
}
|
||||
|
||||
export type AdultSelectorProps = {
|
||||
roomIndex: number
|
||||
}
|
||||
|
||||
export type ChildSelectorProps = {
|
||||
export type SelectorProps = {
|
||||
roomIndex: number
|
||||
currentAdults: number
|
||||
currentChildren: Child[]
|
||||
childrenInAdultsBed: number
|
||||
}
|
||||
|
||||
export type ChildInfoSelectorProps = {
|
||||
child: Child
|
||||
adults: number
|
||||
index: number
|
||||
roomIndex: number
|
||||
childrenInAdultsBed: number
|
||||
}
|
||||
|
||||
export interface CounterProps {
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface FilterChipProps {
|
||||
value?: string
|
||||
selected?: boolean
|
||||
disabled?: boolean
|
||||
hasTooltip?: boolean
|
||||
}
|
||||
|
||||
export type FilterChipCheckboxProps = Omit<FilterChipProps, "type">
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { RoomData } from "@/types/hotel"
|
||||
import type { SafeUser } from "@/types/user"
|
||||
import type { RoomsAvailability } from "@/server/routers/hotels/output"
|
||||
import type { RoomPackageCodes, RoomPackageData } from "./roomFilter"
|
||||
import type { Rate } from "./selectRate"
|
||||
|
||||
export interface RoomSelectionProps {
|
||||
roomsAvailability: RoomsAvailability
|
||||
@@ -9,4 +10,13 @@ export interface RoomSelectionProps {
|
||||
user: SafeUser
|
||||
packages: RoomPackageData
|
||||
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