diff --git a/app/[lang]/(live-current)/layout.tsx b/app/[lang]/(live-current)/layout.tsx index ef9477a87..3867b76d1 100644 --- a/app/[lang]/(live-current)/layout.tsx +++ b/app/[lang]/(live-current)/layout.tsx @@ -4,6 +4,7 @@ import "@scandic-hotels/design-system/style.css" import Script from "next/script" import TokenRefresher from "@/components/Auth/TokenRefresher" +import BookingWidget from "@/components/BookingWidget" import CookieBotConsent from "@/components/CookieBot" import AdobeScript from "@/components/Current/AdobeScript" import Footer from "@/components/Current/Footer" diff --git a/components/DatePicker/index.tsx b/components/DatePicker/index.tsx index 9cf6b3446..d56aaba34 100644 --- a/components/DatePicker/index.tsx +++ b/components/DatePicker/index.tsx @@ -129,7 +129,7 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { onClick={() => setIsOpen(true)} type="button" > - + {selectedFromDate} - {selectedToDate} diff --git a/components/Forms/BookingWidget/FormContent/Input/index.tsx b/components/Forms/BookingWidget/FormContent/Input/index.tsx index dfa5e018a..9871d70f0 100644 --- a/components/Forms/BookingWidget/FormContent/Input/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Input/index.tsx @@ -10,7 +10,7 @@ const Input = forwardRef< InputHTMLAttributes >(function InputComponent(props, ref) { return ( - + ) diff --git a/components/GuestsRoomsPicker/Form.tsx b/components/GuestsRoomsPicker/Form.tsx index 97048bf4c..0204f414e 100644 --- a/components/GuestsRoomsPicker/Form.tsx +++ b/components/GuestsRoomsPicker/Form.tsx @@ -1,61 +1,77 @@ "use client" -import { useEffect } from "react" +import { useCallback, useEffect } from "react" import { useFormContext, useWatch } from "react-hook-form" import { useIntl } from "react-intl" +import { env } from "@/env/client" + 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 { GuestsRoom } from "./GuestsRoom" import styles from "./guests-rooms-picker.module.css" import type { BookingWidgetSchema } from "@/types/components/bookingWidget" -import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" -import type { GuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker" +import type { TGuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker" + +const MAX_ROOMS = 4 + +interface GuestsRoomsPickerDialogProps { + rooms: TGuestsRoom[] + onClose: () => void + isOverflowed?: boolean // ToDo Remove once Tooltip below is no longer required +} export default function GuestsRoomsPickerDialog({ rooms, onClose, isOverflowed = false, -}: { - rooms: GuestsRoom[] - onClose: () => void - isOverflowed?: boolean // ToDo Remove once Tooltip below is no longer required -}) { +}: GuestsRoomsPickerDialogProps) { const intl = useIntl() + const { getFieldState, trigger, setValue } = + useFormContext() + const roomsValue = useWatch({ name: "rooms" }) + const addRoomLabel = intl.formatMessage({ id: "Add Room" }) 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() + const handleClose = useCallback(async () => { + const isValid = await trigger("rooms") + if (isValid) onClose() + }, [trigger, onClose]) - const roomsValue = useWatch({ name: "rooms" }) + const handleAddRoom = useCallback(() => { + setValue("rooms", [...roomsValue, { adults: 1, child: [] }], { + shouldValidate: true, + }) + }, [roomsValue, setValue]) - async function handleOnClose() { - const state = await trigger("rooms") - if (state) { - onClose() - } - } - - const fieldState = getFieldState("rooms") + const handleRemoveRoom = useCallback( + (index: number) => { + setValue( + "rooms", + roomsValue.filter((_, i) => i !== index), + { shouldValidate: true } + ) + }, + [roomsValue, setValue] + ) + // Validate rooms when they change useEffect(() => { - if (fieldState.invalid) { - trigger("rooms") - } - }, [roomsValue, fieldState.invalid, trigger]) + const fieldState = getFieldState("rooms") + if (fieldState.invalid) trigger("rooms") + }, [roomsValue, getFieldState, trigger]) + + const isInvalid = getFieldState("rooms").invalid + const canAddRooms = rooms.length < MAX_ROOMS return ( <> @@ -65,97 +81,99 @@ export default function GuestsRoomsPickerDialog({ -
- {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 ( -
-
- - {roomLabel} {index + 1} - - - -
- -
- ) - })} -
- - {rooms.length < 4 ? ( +
+ {rooms.map((room, index) => ( + + ))} + + {env.NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE ? ( +
+ - ) : null} - -
+ +
+ ) : ( + canAddRooms && ( +
+ +
+ ) + )}
-
- - {rooms.length < 4 ? ( + {env.NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE ? ( +
+ - ) : null} - -
+
+
+ ) : ( + canAddRooms && ( +
+ +
+ ) + )} - +
) diff --git a/components/GuestsRoomsPicker/GuestsRoom/index.tsx b/components/GuestsRoomsPicker/GuestsRoom/index.tsx new file mode 100644 index 000000000..7122a0a31 --- /dev/null +++ b/components/GuestsRoomsPicker/GuestsRoom/index.tsx @@ -0,0 +1,72 @@ +import { useIntl } from "react-intl" + +import { DeleteIcon } from "@/components/Icons" +import Button from "@/components/TempDesignSystem/Button" +import Divider from "@/components/TempDesignSystem/Divider" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" + +import AdultSelector from "../AdultSelector" +import ChildSelector from "../ChildSelector" + +import styles from "../guests-rooms-picker.module.css" + +import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" +import type { TGuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker" + +export function GuestsRoom({ + room, + index, + onRemove, +}: { + room: TGuestsRoom + index: number + onRemove: (index: number) => void +}) { + const intl = useIntl() + const roomLabel = intl.formatMessage({ id: "Room" }) + + const childrenInAdultsBed = room.child.filter( + (child) => child.bed === ChildBedMapEnum.IN_ADULTS_BED + ).length + + return ( +
+
+ + {roomLabel} {index + 1} + + + + {index !== 0 && ( +
+ +
+ )} +
+ +
+ ) +} diff --git a/components/GuestsRoomsPicker/guests-rooms-picker.module.css b/components/GuestsRoomsPicker/guests-rooms-picker.module.css index dd43b2c32..281963d55 100644 --- a/components/GuestsRoomsPicker/guests-rooms-picker.module.css +++ b/components/GuestsRoomsPicker/guests-rooms-picker.module.css @@ -56,9 +56,25 @@ } .footer { - display: grid; + display: flex; + flex-direction: row; gap: var(--Spacing-x1); - grid-template-columns: auto; +} + +.roomContainer { + padding: var(--Spacing-x2); +} +.roomContainer:last-of-type { + padding-bottom: calc(var(--sticky-button-height) + 20px); +} + +.roomActionsButton { + margin-left: auto; + color: var(--Base-Text-Accent); +} + +.footer button { + width: 100%; } @media screen and (max-width: 1366px) { @@ -71,7 +87,7 @@ .header { display: grid; grid-area: header; - padding: var(--Spacing-x3) var(--Spacing-x2); + padding: var(--Spacing-x3) var(--Spacing-x2) 0; } .close { @@ -83,13 +99,6 @@ padding: 0; } - .roomContainer { - padding: 0 var(--Spacing-x2); - } - .roomContainer:last-of-type { - padding-bottom: calc(var(--sticky-button-height) + 20px); - } - .footer { background: linear-gradient( 180deg, @@ -125,6 +134,17 @@ grid-template-rows: auto; } + .roomContainer { + padding: var(--Spacing-x2) 0 0 0; + } + .roomContainer:first-of-type { + padding-top: 0; + } + + .roomContainer:last-of-type { + padding-bottom: 0; + } + .contentContainer { overflow-y: visible; } @@ -163,6 +183,11 @@ padding-top: var(--Spacing-x2); } + .footer button { + margin-left: auto; + width: 125px; + } + .footer .hideOnDesktop, .addRoomMobileContainer { display: none; diff --git a/components/GuestsRoomsPicker/index.tsx b/components/GuestsRoomsPicker/index.tsx index e45a671bd..d3bb9e68e 100644 --- a/components/GuestsRoomsPicker/index.tsx +++ b/components/GuestsRoomsPicker/index.tsx @@ -18,11 +18,11 @@ import PickerForm from "./Form" import styles from "./guests-rooms-picker.module.css" -import type { GuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker" +import type { TGuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker" export default function GuestsRoomsPickerForm() { const { watch, trigger } = useFormContext() - const rooms = watch("rooms") as GuestsRoom[] + const rooms = watch("rooms") as TGuestsRoom[] const checkIsDesktop = useMediaQuery("(min-width: 1367px)") const [isDesktop, setIsDesktop] = useState(true) @@ -83,10 +83,10 @@ export default function GuestsRoomsPickerForm() { }, [containerHeight]) useEffect(() => { - if (typeof window !== undefined && isDesktop) { + if (typeof window !== undefined && isDesktop && rooms.length > 0) { updateHeight() } - }, [childCount, isDesktop, updateHeight]) + }, [childCount, isDesktop, updateHeight, rooms]) return isDesktop ? ( @@ -104,13 +104,7 @@ export default function GuestsRoomsPickerForm() { style={containerHeight ? { overflow: "auto" } : {}} > - {({ close }) => ( - - )} + {({ close }) => } @@ -137,7 +131,7 @@ function Trigger({ className, triggerFn, }: { - rooms: GuestsRoom[] + rooms: TGuestsRoom[] className: string triggerFn?: () => void }) { @@ -149,27 +143,30 @@ function Trigger({ type="button" onPress={triggerFn} > - - {rooms.map((room, i) => ( - - {intl.formatMessage( - { id: "booking.rooms" }, - { totalRooms: rooms.length } - )} - {", "} - {intl.formatMessage( - { id: "booking.adults" }, - { totalAdults: room.adults } - )} - {room.child.length > 0 - ? ", " + - intl.formatMessage( - { id: "booking.children" }, - { totalChildren: room.child.length } - ) - : null} - - ))} + + + {intl.formatMessage( + { id: "booking.rooms" }, + { totalRooms: rooms.length } + )} + {", "} + {intl.formatMessage( + { id: "booking.adults" }, + { totalAdults: rooms.reduce((acc, room) => acc + room.adults, 0) } + )} + {rooms.some((room) => room.child.length > 0) + ? ", " + + intl.formatMessage( + { id: "booking.children" }, + { + totalChildren: rooms.reduce( + (acc, room) => acc + room.child.length, + 0 + ), + } + ) + : null} + ) diff --git a/types/components/bookingWidget/guestsRoomsPicker.ts b/types/components/bookingWidget/guestsRoomsPicker.ts index a075c9f8d..b672df7ea 100644 --- a/types/components/bookingWidget/guestsRoomsPicker.ts +++ b/types/components/bookingWidget/guestsRoomsPicker.ts @@ -8,7 +8,7 @@ export type Child = { bed: number } -export type GuestsRoom = { +export type TGuestsRoom = { adults: number child: Child[] } diff --git a/types/components/bookingWidget/index.ts b/types/components/bookingWidget/index.ts index 7e9c79d16..e8eda85c8 100644 --- a/types/components/bookingWidget/index.ts +++ b/types/components/bookingWidget/index.ts @@ -4,7 +4,7 @@ import type { z } from "zod" import type { Locations } from "@/types/trpc/routers/hotel/locations" import type { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema" import type { bookingWidgetVariants } from "@/components/Forms/BookingWidget/variants" -import type { GuestsRoom } from "./guestsRoomsPicker" +import type { TGuestsRoom } from "./guestsRoomsPicker" export type BookingWidgetSchema = z.output @@ -13,7 +13,7 @@ export type BookingWidgetSearchParams = { hotel?: string fromDate?: string toDate?: string - room?: GuestsRoom[] + room?: TGuestsRoom[] } export type BookingWidgetType = VariantProps<