diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx index 1e4ae867e..5aed6b8b3 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx @@ -184,6 +184,8 @@ export default async function DetailsPage({ roomType={room.roomType} roomTypeCode={booking.rooms[idx].roomTypeCode} rateDescription={room.cancellationText} + roomIndex={idx} + searchParamsStr={selectRoomParams.toString()} /> {room.bedTypes ? ( diff --git a/components/HotelReservation/EnterDetails/SelectedRoom/index.tsx b/components/HotelReservation/EnterDetails/SelectedRoom/index.tsx index 8944d4baf..76ca0f10d 100644 --- a/components/HotelReservation/EnterDetails/SelectedRoom/index.tsx +++ b/components/HotelReservation/EnterDetails/SelectedRoom/index.tsx @@ -1,11 +1,14 @@ "use client" +import { useRouter } from "next/navigation" +import { useTransition } from "react" import { useIntl } from "react-intl" import { selectRate } from "@/constants/routes/hotelReservation" +import { useRateSelectionStore } from "@/stores/select-rate/rate-selection" import { CheckIcon, EditIcon } from "@/components/Icons" -import Link from "@/components/TempDesignSystem/Link" +import Button from "@/components/TempDesignSystem/Button" import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import useLang from "@/hooks/useLang" @@ -21,9 +24,23 @@ export default function SelectedRoom({ roomType, roomTypeCode, rateDescription, + roomIndex, + searchParamsStr, }: SelectedRoomProps) { const intl = useIntl() const lang = useLang() + const router = useRouter() + const [isPending, startTransition] = useTransition() + const { modifyRate } = useRateSelectionStore() + + function changeRoom() { + modifyRate(roomIndex) + startTransition(() => { + const newSearchParams = new URLSearchParams(searchParamsStr) + newSearchParams.set("modifyRateIndex", roomIndex.toString()) + router.push(`${selectRate(lang)}?${newSearchParams.toString()}`) + }) + } return (
@@ -59,17 +76,16 @@ export default function SelectedRoom({ } )} - - {intl.formatMessage({ id: "Change room" })}{" "} - + {intl.formatMessage({ id: "Change room" })} +
{roomTypeCode && (
diff --git a/components/HotelReservation/SelectRate/RoomTypeFilter/roomFilter.module.css b/components/HotelReservation/SelectRate/RoomTypeFilter/roomFilter.module.css index 188bca200..cc256b1f4 100644 --- a/components/HotelReservation/SelectRate/RoomTypeFilter/roomFilter.module.css +++ b/components/HotelReservation/SelectRate/RoomTypeFilter/roomFilter.module.css @@ -2,7 +2,7 @@ display: flex; flex-direction: row; justify-content: space-between; - align-items: center; + align-items: flex-start; } .roomsFilter { diff --git a/components/HotelReservation/SelectRate/RoomTypeList/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomTypeList/RoomCard/index.tsx index a43c00960..6ead0edfc 100644 --- a/components/HotelReservation/SelectRate/RoomTypeList/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomTypeList/RoomCard/index.tsx @@ -72,10 +72,13 @@ export default function RoomCard({ const isUserLoggedIn = !!session const intl = useIntl() const searchParams = useSearchParams() - const { selectRate, selectedRates } = useRateSelectionStore((state) => ({ - selectRate: state.selectRate, - selectedRates: state.selectedRates, - })) + const { selectRate, selectedRates, closeModifyRate } = useRateSelectionStore( + (state) => ({ + selectRate: state.selectRate, + selectedRates: state.selectedRates, + closeModifyRate: state.closeModifyRate, + }) + ) const selectedRate = useRateSelectionStore( (state) => state.selectedRates[roomListIndex] @@ -389,7 +392,10 @@ export default function RoomCard({ return ( { + handleRateSelection(rateCode, rateName, paymentTerm) + closeModifyRate() + }} isSelected={ selectedRate?.publicRateCode === product.productType.public.rateCode && diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx index a9300f6ac..c68b31bda 100644 --- a/components/HotelReservation/SelectRate/Rooms/index.tsx +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -1,12 +1,14 @@ "use client" -import { usePathname, useRouter, useSearchParams } from "next/navigation" -import { useEffect, useMemo } from "react" +import { useRouter, useSearchParams } from "next/navigation" +import { useCallback, useEffect, useMemo, useTransition } from "react" import { useIntl } from "react-intl" import { useRateSelectionStore } from "@/stores/select-rate/rate-selection" import { useRoomFilteringStore } from "@/stores/select-rate/room-filtering" +import { ChevronDownSmallIcon } from "@/components/Icons" +import Button from "@/components/TempDesignSystem/Button" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import { trackLowestRoomPrice } from "@/utils/tracking" import { convertObjToSearchParams, convertSearchParamsToObj } from "@/utils/url" @@ -34,9 +36,9 @@ export default function Rooms({ vat, }: SelectRateProps) { const router = useRouter() - const pathname = usePathname() const searchParams = useSearchParams() const intl = useIntl() + const [isPending, startTransition] = useTransition() const hotelId = searchParams.get("hotel") const arrivalDate = searchParams.get("fromDate") @@ -48,6 +50,8 @@ export default function Rooms({ calculateRateSummary, initializeRates, setGuestsInRooms, + modifyRateIndex, + closeModifyRate, } = useRateSelectionStore() const { @@ -133,13 +137,6 @@ export default function Rooms({ calculateRateSummary, ]) - useEffect(() => { - if (!rateSummary?.some((rate) => rate === null)) return - - const hasAnySelection = selectedRates.some((rate) => rate !== undefined) - if (!hasAnySelection) return - }, [rateSummary, selectedRates]) - useEffect(() => { const pricesWithCurrencies = visibleRooms.flatMap((room) => room.products.map((product) => ({ @@ -163,35 +160,35 @@ export default function Rooms({ }) }, [arrivalDate, departureDate, hotelId, visibleRooms]) - const queryParams = useMemo(() => { - const rooms = rateSummary.map((rate, index) => ({ - roomTypeCode: rate?.roomTypeCode, - rateCode: rate?.public.rateCode, - counterRateCode: rate?.member?.rateCode, - packages: selectedPackagesByRoom[index] || [], - })) - - const newSearchParams = convertObjToSearchParams({ rooms }, searchParams) - - return newSearchParams - }, [searchParams, rateSummary, selectedPackagesByRoom]) - function handleSubmit(e: React.FormEvent) { e.preventDefault() + startTransition(() => { + const rooms = rateSummary.map((rate, index) => ({ + roomTypeCode: rate?.roomTypeCode, + rateCode: rate?.public.rateCode, + counterRateCode: rate?.member?.rateCode, + packages: selectedPackagesByRoom[index] || [], + })) - window.history.pushState(null, "", `${pathname}?${queryParams.toString()}`) - router.push(`details?${queryParams}`) + const newSearchParams = convertObjToSearchParams({ rooms }, searchParams) + router.push(`details?${newSearchParams}`) + }) } useEffect(() => { requestAnimationFrame(() => { const SCROLL_OFFSET = 100 const roomElements = document.querySelectorAll(`.${styles.roomContainer}`) - const index = selectedRates.findIndex((rate) => rate === undefined) - const targetIndex = index === -1 ? selectedRates.length - 1 : index - 1 + let targetIndex: number + if (modifyRateIndex !== null) { + targetIndex = modifyRateIndex + } else { + const index = selectedRates.findIndex((rate) => rate === undefined) + targetIndex = index === -1 ? selectedRates.length - 1 : index - 1 + } + const selectedRoom = roomElements[targetIndex] - if (selectedRoom) { const elementPosition = selectedRoom.getBoundingClientRect().top const offsetPosition = elementPosition + window.scrollY - SCROLL_OFFSET @@ -202,40 +199,79 @@ export default function Rooms({ }) } }) - }, [selectedRates]) + }, [selectedRates, modifyRateIndex]) + + const getRoomState = useCallback( + (index: number) => { + const isFirstRoom = index === 0 + const hasPrevRoomBeenSelected = selectedRates[index - 1] !== undefined + const isCurrentRoomSelected = selectedRates[index] !== undefined + const isModifyRoom = modifyRateIndex === index + + if (isModifyRoom && isCurrentRoomSelected) { + return { active: true, selected: false } + } + + if (isCurrentRoomSelected) { + return { active: false, selected: true } + } + + if ( + (isFirstRoom || hasPrevRoomBeenSelected) && + modifyRateIndex === null + ) { + return { active: true, selected: false } + } + + return { active: false, selected: false } + }, + [modifyRateIndex, selectedRates] + ) return (
{isMultipleRooms ? ( bookingWidgetSearchData.rooms.map((room, index) => { - const classNames = roomSelectionPanelVariants({ - active: - (index === 0 || selectedRates[index - 1] !== undefined) && - selectedRates[index] === undefined, - selected: selectedRates[index] !== undefined, - }) + const roomState = getRoomState(index) + const classNames = roomSelectionPanelVariants(roomState) + return (
- {selectedRates[index] === undefined && ( - - {intl.formatMessage( - { id: "Room {roomIndex}" }, - { roomIndex: index + 1 } - )} - ,{" "} - {intl.formatMessage( - { - id: room.childrenInRoom?.length - ? "{adults} adults, {children} children" - : "{adults} adults", - }, - { - adults: room.adults, - children: room.childrenInRoom?.length, - } - )} - + {!roomState.selected && ( +
+ + {intl.formatMessage( + { id: "Room {roomIndex}" }, + { roomIndex: index + 1 } + )} + ,{" "} + {intl.formatMessage( + { + id: room.childrenInRoom?.length + ? "{adults} adults, {children} children" + : "{adults} adults", + }, + { + adults: room.adults, + children: room.childrenInRoom?.length, + } + )} + + {modifyRateIndex === index ? ( + + ) : null} +
)} +
* { @@ -47,6 +57,7 @@ grid-template-rows: 1fr; opacity: 1; height: auto; + padding-top: var(--Spacing-x1); } .hotelAlert { @@ -54,3 +65,9 @@ margin: 0 auto; padding: var(--Spacing-x-one-and-half); } + +@media (max-width: 768px) { + .roomContainer { + padding: var(--Spacing-x2); + } +} diff --git a/components/HotelReservation/SelectRate/SelectedRoomPanel/index.tsx b/components/HotelReservation/SelectRate/SelectedRoomPanel/index.tsx index e083c14a4..af467cb7c 100644 --- a/components/HotelReservation/SelectRate/SelectedRoomPanel/index.tsx +++ b/components/HotelReservation/SelectRate/SelectedRoomPanel/index.tsx @@ -7,6 +7,7 @@ import { useRateSelectionStore } from "@/stores/select-rate/rate-selection" import { EditIcon } from "@/components/Icons" import Image from "@/components/Image" import Button from "@/components/TempDesignSystem/Button" +import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" @@ -42,41 +43,45 @@ export default function SelectedRoomPanel({ return (
-
- - {intl.formatMessage( - { - id: "Room {roomIndex}", - }, - { - roomIndex: roomIndex + 1, - } - )} - - - {selectedRate?.roomType},{" "} - {intl.formatMessage( - { - id: room.childrenInRoom?.length - ? "{adults} adults, {children} children" - : "{adults} adults", - }, - { - adults: room.adults, - children: room.childrenInRoom?.length, - } - )} - - - {selectedRate?.priceName}, {selectedRate?.priceTerm} - - - {selectedRate?.public.localPrice.pricePerNight}{" "} - {selectedRate?.public.localPrice.currency}/ - {intl.formatMessage({ - id: "night", - })} - +
+
+ + {intl.formatMessage( + { + id: "Room {roomIndex}", + }, + { + roomIndex: roomIndex + 1, + } + )} + + + {selectedRate?.roomType},{" "} + {intl.formatMessage( + { + id: room.childrenInRoom?.length + ? "{adults} adults, {children} children" + : "{adults} adults", + }, + { + adults: room.adults, + children: room.childrenInRoom?.length, + } + )} + +
+
+ + {selectedRate?.priceName}, {selectedRate?.priceTerm} + + + {selectedRate?.public.localPrice.pricePerNight}{" "} + {selectedRate?.public.localPrice.currency}/ + {intl.formatMessage({ + id: "night", + })} + +
{images?.[0]?.imageSizes?.tiny && ( diff --git a/components/HotelReservation/SelectRate/SelectedRoomPanel/selectedRoomPanel.module.css b/components/HotelReservation/SelectRate/SelectedRoomPanel/selectedRoomPanel.module.css index 8c4adce07..04fae65bd 100644 --- a/components/HotelReservation/SelectRate/SelectedRoomPanel/selectedRoomPanel.module.css +++ b/components/HotelReservation/SelectRate/SelectedRoomPanel/selectedRoomPanel.module.css @@ -12,9 +12,17 @@ } .imageContainer { - width: 240px; - height: 160px; + width: 187px; + height: 105px; position: relative; + border-radius: var(--Corner-radius-Small); + overflow: hidden; +} + +.titleContainer { + display: flex; + flex-direction: column; + gap: var(--Spacing-x1); } @media (max-width: 768px) { diff --git a/stores/select-rate/rate-selection.ts b/stores/select-rate/rate-selection.ts index ae7e2407b..e136526f8 100644 --- a/stores/select-rate/rate-selection.ts +++ b/stores/select-rate/rate-selection.ts @@ -26,7 +26,9 @@ interface RateSelectionState { isPriceDetailsModalOpen: boolean isSummaryOpen: boolean guestsInRooms: { adults: number; children?: Child[] }[] + modifyRateIndex: number | null modifyRate: (index: number) => void + closeModifyRate: () => void selectRate: (index: number, rate: RateCode | undefined) => void initializeRates: (count: number) => void calculateRateSummary: ({ @@ -46,18 +48,18 @@ export const useRateSelectionStore = create((set, get) => ({ isPriceDetailsModalOpen: false, isSummaryOpen: false, guestsInRooms: [{ adults: 1 }], - modifyRate: (index) => - set((state) => { - const newRates = [...state.selectedRates] - newRates[index] = undefined - return { selectedRates: newRates } - }), - selectRate: (index, rate) => + modifyRateIndex: null, + modifyRate: (index) => set({ modifyRateIndex: index }), + closeModifyRate: () => set({ modifyRateIndex: null }), + selectRate: (index, rate) => { set((state) => { const newRates = [...state.selectedRates] newRates[index] = rate - return { selectedRates: newRates } - }), + return { + selectedRates: newRates, + } + }) + }, initializeRates: (count) => set({ selectedRates: new Array(count).fill(undefined) }), calculateRateSummary: (params) => { diff --git a/types/components/hotelReservation/enterDetails/room.ts b/types/components/hotelReservation/enterDetails/room.ts index b133a7d34..87bbf459d 100644 --- a/types/components/hotelReservation/enterDetails/room.ts +++ b/types/components/hotelReservation/enterDetails/room.ts @@ -1,8 +1,8 @@ -import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability" - export interface SelectedRoomProps { hotelId: string roomType: string roomTypeCode: string rateDescription: string + roomIndex: number + searchParamsStr: string }