diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index 18001c9d7..fb352f05c 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -7,8 +7,11 @@ import { getHotel } from "@/lib/trpc/memoizedRequests" import HotelInfoCard, { HotelInfoCardSkeleton, } from "@/components/HotelReservation/SelectRate/HotelInfoCard" -import { RoomsContainer } from "@/components/HotelReservation/SelectRate/Rooms/RoomsContainer" -import { RoomsContainerSkeleton } from "@/components/HotelReservation/SelectRate/Rooms/RoomsContainerSkeleton" +import { + preload, + RoomsContainer, +} from "@/components/HotelReservation/SelectRate/RoomsContainer" +import { RoomsContainerSkeleton } from "@/components/HotelReservation/SelectRate/RoomsContainer/RoomsContainerSkeleton" import TrackingSDK from "@/components/TrackingSDK" import { setLang } from "@/i18n/serverContext" import { convertSearchParamsToObj } from "@/utils/url" @@ -37,17 +40,26 @@ export default async function SelectRatePage({ const { hotel, adultsInRoom, childrenInRoom, selectHotelParams } = searchDetails + const { fromDate, toDate } = getValidDates( + selectHotelParams.fromDate, + selectHotelParams.toDate + ) + + preload( + hotel.id, + params.lang, + fromDate.format("YYYY-MM-DD"), + toDate.format("YYYY-MM-DD"), + adultsInRoom, + childrenInRoom + ) + const hotelData = await getHotel({ hotelId: hotel.id, isCardOnlyPayment: false, language: params.lang, }) - const { fromDate, toDate } = getValidDates( - selectHotelParams.fromDate, - selectHotelParams.toDate - ) - const arrivalDate = fromDate.toDate() const departureDate = toDate.toDate() @@ -96,13 +108,13 @@ export default async function SelectRatePage({ fallback={} > diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/enterDetailsTracking.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/enterDetailsTracking.tsx deleted file mode 100644 index 4cb53d551..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/enterDetailsTracking.tsx +++ /dev/null @@ -1,174 +0,0 @@ -"use client" -import { usePathname } from "next/navigation" -import { useEffect, useMemo, useRef } from "react" - -import { useEnterDetailsStore } from "@/stores/enter-details" -import { selectRoom } from "@/stores/enter-details/helpers" - -import { useSessionId } from "@/hooks/useSessionId" -import { createSDKPageObject, trackPageView } from "@/utils/tracking" - -import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" -import { - type Ancillary, - TrackingChannelEnum, - type TrackingSDKHotelInfo, - type TrackingSDKPageData, - type TrackingSDKUserData, -} from "@/types/components/tracking" -import type { Packages } from "@/types/requests/packages" -import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability" -import type { Lang } from "@/constants/languages" - -type Props = { - initialHotelsTrackingData: TrackingSDKHotelInfo - userTrackingData: TrackingSDKUserData - lang: Lang - selectedRoom: RoomConfiguration - cancellationRule: string -} - -export default function EnterDetailsTracking(props: Props) { - const { - initialHotelsTrackingData, - userTrackingData, - lang, - selectedRoom, - cancellationRule, - } = props - - const { bedType, breakfast, roomPrice, roomRate, roomFeatures } = - useEnterDetailsStore(selectRoom) - - const totalPrice = useEnterDetailsStore((state) => state.totalPrice) - - const pathName = usePathname() - const sessionId = useSessionId() - - const previousPathname = useRef(null) - - const getSpecialRoomType = (packages: Packages | null) => { - if (!packages) { - return "" - } - - const specialRoom = packages.find((p) => - [ - RoomPackageCodeEnum.PET_ROOM, - RoomPackageCodeEnum.ALLERGY_ROOM, - RoomPackageCodeEnum.ACCESSIBILITY_ROOM, - ].includes(p.code) - ) - - switch (specialRoom?.code) { - case RoomPackageCodeEnum.PET_ROOM: - return "pet-friendly" - case RoomPackageCodeEnum.ALLERGY_ROOM: - return "allergy room" - case RoomPackageCodeEnum.ACCESSIBILITY_ROOM: - return "accessibility room" - default: - return "" - } - } - - const getAnalyticsRateCode = (rateCodeName: string | undefined) => { - switch (rateCodeName) { - case "FLEXEU": - return "flex" - case "CHANGEEU": - return "change" - case "SAVEEU": - return "save" - default: - return rateCodeName - } - } - - const pageObject = useMemo(() => { - const stepByPathname = pathName.split("/").pop()! - const pageTrackingData: TrackingSDKPageData = { - pageId: stepByPathname, - domainLanguage: lang, - channel: TrackingChannelEnum["hotelreservation"], - pageName: `hotelreservation|${stepByPathname}`, - siteSections: `hotelreservation|${stepByPathname}`, - pageType: stepByPathname, - siteVersion: "new-web", - } - - const trackingData = { - ...pageTrackingData, - pathName, - sessionId, - pageLoadTime: 0, // Yes, this is instant - } - const pageObject = createSDKPageObject(trackingData) - - return pageObject - }, [lang, sessionId, pathName]) - - const hotelDetailsData = useMemo(() => { - const isMember = true - const rate = isMember ? roomRate.memberRate : roomRate.publicRate - - const breakfastAncillary = breakfast && { - hotelid: initialHotelsTrackingData.hotelID, - productName: "BreakfastAdult", - productCategory: "", // TODO: Add category - productId: breakfast.code, - productPrice: +breakfast.localPrice.price, - productUnits: initialHotelsTrackingData.noOfAdults, - productPoints: 0, - productType: "food", - } - - const data: TrackingSDKHotelInfo = { - ...initialHotelsTrackingData, - rateCode: rate?.rateCode, - rateCodeType: roomRate.publicRate.rateType, - rateCodeName: "", //TODO: this should be ratedefinition.title and should be fixed when tracking is implemented for multiroom. - rateCodeCancellationRule: cancellationRule, - revenueCurrencyCode: totalPrice.local?.currency, - breakfastOption: breakfast ? "breakfast buffet" : "no breakfast", - totalPrice: totalPrice.local?.price, - specialRoomType: getSpecialRoomType(roomFeatures), - roomTypeName: selectedRoom.roomType, - bedType: bedType?.description, - roomTypeCode: bedType?.roomTypeCode, - roomPrice: roomPrice.perStay.local.price, - discount: roomRate.memberRate - ? roomRate.publicRate.localPrice.pricePerStay - - roomRate.memberRate.localPrice.pricePerStay - : 0, - analyticsrateCode: getAnalyticsRateCode(roomRate.publicRate.rateCode), - ancillaries: breakfastAncillary ? [breakfastAncillary] : [], - } - - return data - }, [ - bedType, - breakfast, - totalPrice, - roomPrice, - roomRate, - roomFeatures, - initialHotelsTrackingData, - cancellationRule, - selectedRoom.roomType, - ]) - - useEffect(() => { - if (previousPathname.current !== pathName) { - trackPageView({ - event: "pageView", - pageInfo: pageObject, - userInfo: userTrackingData, - hotelInfo: hotelDetailsData, - }) - } - previousPathname.current = pathName // Update for next render - }, [userTrackingData, pageObject, hotelDetailsData, pathName]) - - return null -} diff --git a/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx b/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx index e642e1844..d41262981 100644 --- a/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx +++ b/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx @@ -42,8 +42,8 @@ import { type PaymentFormData, paymentSchema } from "./schema" import styles from "./payment.module.css" +import type { PaymentClientProps } from "@/types/components/hotelReservation/enterDetails/payment" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" -import type { PaymentClientProps } from "@/types/components/hotelReservation/selectRate/section" const maxRetries = 15 const retryInterval = 2000 diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx index 3a8f16f61..6a6cf6532 100644 --- a/components/HotelReservation/EnterDetails/Payment/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -2,7 +2,7 @@ import { getSavedPaymentCardsSafely } from "@/lib/trpc/memoizedRequests" import PaymentClient from "./PaymentClient" -import type { PaymentProps } from "@/types/components/hotelReservation/selectRate/section" +import type { PaymentProps } from "@/types/components/hotelReservation/enterDetails/payment" export default async function Payment({ user, diff --git a/components/HotelReservation/EnterDetails/Summary/Desktop.tsx b/components/HotelReservation/EnterDetails/Summary/Desktop.tsx index d89e3c64b..b04da9660 100644 --- a/components/HotelReservation/EnterDetails/Summary/Desktop.tsx +++ b/components/HotelReservation/EnterDetails/Summary/Desktop.tsx @@ -11,7 +11,7 @@ import type { SummaryProps } from "@/types/components/hotelReservation/summary" export default function DesktopSummary(props: SummaryProps) { const { booking, - actions: { toggleSummaryOpen, togglePriceDetailsModalOpen }, + actions: { toggleSummaryOpen }, totalPrice, vat, } = useEnterDetailsStore((state) => state) @@ -28,7 +28,6 @@ export default function DesktopSummary(props: SummaryProps) { totalPrice={totalPrice} vat={vat} toggleSummaryOpen={toggleSummaryOpen} - togglePriceDetailsModalOpen={togglePriceDetailsModalOpen} /> ) diff --git a/components/HotelReservation/EnterDetails/Summary/Mobile/index.tsx b/components/HotelReservation/EnterDetails/Summary/Mobile/index.tsx index e4011af56..03a3c88b5 100644 --- a/components/HotelReservation/EnterDetails/Summary/Mobile/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/Mobile/index.tsx @@ -14,7 +14,7 @@ import type { SummaryProps } from "@/types/components/hotelReservation/summary" export default function MobileSummary(props: SummaryProps) { const { booking, - actions: { toggleSummaryOpen, togglePriceDetailsModalOpen }, + actions: { toggleSummaryOpen }, totalPrice, vat, } = useEnterDetailsStore((state) => state) @@ -40,7 +40,6 @@ export default function MobileSummary(props: SummaryProps) { totalPrice={totalPrice} vat={vat} toggleSummaryOpen={toggleSummaryOpen} - togglePriceDetailsModalOpen={togglePriceDetailsModalOpen} /> diff --git a/components/HotelReservation/EnterDetails/Summary/UI/index.tsx b/components/HotelReservation/EnterDetails/Summary/UI/index.tsx index 37edb7399..d9251ff2a 100644 --- a/components/HotelReservation/EnterDetails/Summary/UI/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/UI/index.tsx @@ -35,7 +35,6 @@ export default function SummaryUI({ breakfastIncluded, vat, toggleSummaryOpen, - togglePriceDetailsModalOpen, }: EnterDetailsSummaryProps) { const intl = useIntl() const lang = useLang() @@ -53,12 +52,6 @@ export default function SummaryUI({ } } - function handleTogglePriceDetailsModal() { - if (togglePriceDetailsModalOpen) { - togglePriceDetailsModalOpen() - } - } - function getMemberPrice(roomRate: RoomRate) { return roomRate?.memberRate ? { @@ -369,7 +362,6 @@ export default function SummaryUI({ }))} totalPrice={totalPrice} vat={vat} - toggleModal={handleTogglePriceDetailsModal} />
diff --git a/components/HotelReservation/EnterDetails/Summary/summary.test.tsx b/components/HotelReservation/EnterDetails/Summary/summary.test.tsx index de1000252..1febae6f4 100644 --- a/components/HotelReservation/EnterDetails/Summary/summary.test.tsx +++ b/components/HotelReservation/EnterDetails/Summary/summary.test.tsx @@ -102,7 +102,6 @@ describe("EnterDetails Summary", () => { }} vat={12} toggleSummaryOpen={jest.fn()} - togglePriceDetailsModalOpen={jest.fn()} />, { wrapper: createWrapper(intl), @@ -141,7 +140,6 @@ describe("EnterDetails Summary", () => { }} vat={12} toggleSummaryOpen={jest.fn()} - togglePriceDetailsModalOpen={jest.fn()} />, { wrapper: createWrapper(intl), diff --git a/components/HotelReservation/HotelCardDialog/ListingHotelCardDialog/index.tsx b/components/HotelReservation/HotelCardDialog/ListingHotelCardDialog/index.tsx index 12cce2039..8054f8066 100644 --- a/components/HotelReservation/HotelCardDialog/ListingHotelCardDialog/index.tsx +++ b/components/HotelReservation/HotelCardDialog/ListingHotelCardDialog/index.tsx @@ -9,6 +9,7 @@ import Button from "@/components/TempDesignSystem/Button" import Link from "@/components/TempDesignSystem/Link" import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import { isValidClientSession } from "@/utils/clientSession" import NoPriceAvailableCard from "../../HotelCard/NoPriceAvailableCard" import HotelCardDialogImage from "../HotelCardDialogImage" @@ -33,7 +34,7 @@ export default function ListingHotelCardDialog({ }: ListingHotelCardProps) { const intl = useIntl() const { data: session } = useSession() - const isUserLoggedIn = !!session + const isUserLoggedIn = isValidClientSession(session) const { name, publicPrice, diff --git a/components/HotelReservation/HotelCardDialog/StandaloneHotelCardDialog/index.tsx b/components/HotelReservation/HotelCardDialog/StandaloneHotelCardDialog/index.tsx index 36cabfc12..298777abf 100644 --- a/components/HotelReservation/HotelCardDialog/StandaloneHotelCardDialog/index.tsx +++ b/components/HotelReservation/HotelCardDialog/StandaloneHotelCardDialog/index.tsx @@ -11,6 +11,7 @@ import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import { isValidClientSession } from "@/utils/clientSession" import NoPriceAvailableCard from "../../HotelCard/NoPriceAvailableCard" import HotelCardDialogImage from "../HotelCardDialogImage" @@ -35,7 +36,7 @@ export default function StandaloneHotelCardDialog({ }: StandaloneHotelCardProps) { const intl = useIntl() const { data: session } = useSession() - const isUserLoggedIn = !!session + const isUserLoggedIn = isValidClientSession(session) const { name, publicPrice, diff --git a/components/HotelReservation/HotelCardListing/index.tsx b/components/HotelReservation/HotelCardListing/index.tsx index bcd5f7b75..1b80b5a98 100644 --- a/components/HotelReservation/HotelCardListing/index.tsx +++ b/components/HotelReservation/HotelCardListing/index.tsx @@ -11,6 +11,7 @@ import { useHotelsMapStore } from "@/stores/hotels-map" import Alert from "@/components/TempDesignSystem/Alert" import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton" import { useScrollToTop } from "@/hooks/useScrollToTop" +import { isValidClientSession } from "@/utils/clientSession" import HotelCard from "../HotelCard" import { DEFAULT_SORT } from "../SelectHotel/HotelSorter" @@ -29,7 +30,7 @@ export default function HotelCardListing({ type = HotelCardListingTypeEnum.PageListing, }: HotelCardListingProps) { const { data: session } = useSession() - const isUserLoggedIn = !!session + const isUserLoggedIn = isValidClientSession(session) const searchParams = useSearchParams() const activeFilters = useHotelFilterStore((state) => state.activeFilters) const setResultCount = useHotelFilterStore((state) => state.setResultCount) diff --git a/components/HotelReservation/PriceDetailsModal/index.tsx b/components/HotelReservation/PriceDetailsModal/index.tsx index dfa2d7bcf..1fe296edc 100644 --- a/components/HotelReservation/PriceDetailsModal/index.tsx +++ b/components/HotelReservation/PriceDetailsModal/index.tsx @@ -26,7 +26,6 @@ interface PriceDetailsModalProps { }[] totalPrice: Price vat: number - toggleModal: () => void } export default function PriceDetailsModal({ @@ -35,7 +34,6 @@ export default function PriceDetailsModal({ rooms, totalPrice, vat, - toggleModal, }: PriceDetailsModalProps) { const intl = useIntl() @@ -43,7 +41,7 @@ export default function PriceDetailsModal({ + - -
- ) -} diff --git a/components/HotelReservation/SelectRate/BreakfastSelection/breakfastSelection.module.css b/components/HotelReservation/SelectRate/BreakfastSelection/breakfastSelection.module.css deleted file mode 100644 index b98c3f937..000000000 --- a/components/HotelReservation/SelectRate/BreakfastSelection/breakfastSelection.module.css +++ /dev/null @@ -1,28 +0,0 @@ -.wrapper { - border-bottom: 1px solid rgba(17, 17, 17, 0.2); - padding-bottom: var(--Spacing-x3); -} - -.header { - margin-top: var(--Spacing-x2); - margin-bottom: var(--Spacing-x2); -} - -.list { - margin-top: var(--Spacing-x4); - list-style: none; - display: grid; - grid-template-columns: 1fr 1fr; - column-gap: var(--Spacing-x2); - row-gap: var(--Spacing-x4); -} - -.list > li { - width: 100%; -} - -.list input[type="radio"] { - opacity: 0; - position: fixed; - width: 0; -} diff --git a/components/HotelReservation/SelectRate/BreakfastSelection/index.tsx b/components/HotelReservation/SelectRate/BreakfastSelection/index.tsx deleted file mode 100644 index c20a46fee..000000000 --- a/components/HotelReservation/SelectRate/BreakfastSelection/index.tsx +++ /dev/null @@ -1,60 +0,0 @@ -"use client" - -import { useRouter, useSearchParams } from "next/navigation" -import { useIntl } from "react-intl" - -import SelectionCard from "../SelectionCard" - -import styles from "./breakfastSelection.module.css" - -import type { BreakfastSelectionProps } from "@/types/components/hotelReservation/selectRate/section" - -export default function BreakfastSelection({ - alternatives, - nextPath, -}: BreakfastSelectionProps) { - const intl = useIntl() - const router = useRouter() - const searchParams = useSearchParams() - - function handleSubmit(e: React.FormEvent) { - e.preventDefault() - const queryParams = new URLSearchParams(searchParams) - queryParams.set("breakfast", e.currentTarget.breakfast?.value) - router.push(`${nextPath}?${queryParams}`) - } - - return ( -
-
-
    - {alternatives.map((alternative) => ( -
  • - -
  • - ))} -
- - -
-
- ) -} diff --git a/components/HotelReservation/SelectRate/Details/details.module.css b/components/HotelReservation/SelectRate/Details/details.module.css deleted file mode 100644 index ec81ef8e9..000000000 --- a/components/HotelReservation/SelectRate/Details/details.module.css +++ /dev/null @@ -1,2 +0,0 @@ -.wrapper { -} diff --git a/components/HotelReservation/SelectRate/Details/index.tsx b/components/HotelReservation/SelectRate/Details/index.tsx deleted file mode 100644 index bf4b5e0c4..000000000 --- a/components/HotelReservation/SelectRate/Details/index.tsx +++ /dev/null @@ -1,23 +0,0 @@ -"use client" - -import { useSearchParams } from "next/navigation" -import { useIntl } from "react-intl" - -import Button from "@/components/TempDesignSystem/Button" - -import styles from "./details.module.css" - -import type { DetailsProps } from "@/types/components/hotelReservation/selectRate/section" - -export default function Details({ nextPath }: DetailsProps) { - const intl = useIntl() - const searchParams = useSearchParams() - - return ( -
-
- -
-
- ) -} diff --git a/components/HotelReservation/SelectRate/RateSummary/index.tsx b/components/HotelReservation/SelectRate/RateSummary/index.tsx deleted file mode 100644 index 152938a13..000000000 --- a/components/HotelReservation/SelectRate/RateSummary/index.tsx +++ /dev/null @@ -1,210 +0,0 @@ -"use client" - -import { useEffect, useState } from "react" -import { useIntl } from "react-intl" - -import { dt } from "@/lib/dt" -import { useRateSelectionStore } from "@/stores/select-rate/rate-selection" - -import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop" -import SignupPromoMobile from "@/components/HotelReservation/SignupPromo/Mobile" -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" -import { formatPrice } from "@/utils/numberFormatting" - -import MobileSummary from "./MobileSummary" -import { calculateTotalPrice } from "./utils" - -import styles from "./rateSummary.module.css" - -import type { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary" -import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" - -export default function RateSummary({ - isUserLoggedIn, - packages, - roomsAvailability, - booking, - vat, -}: RateSummaryProps) { - const intl = useIntl() - const [isVisible, setIsVisible] = useState(false) - - const { getSelectedRateSummary } = useRateSelectionStore() - - const { rooms } = booking - - useEffect(() => { - const timer = setTimeout(() => setIsVisible(true), 0) - return () => clearTimeout(timer) - }, []) - - const selectedRateSummary = getSelectedRateSummary() - const totalRoomsRequired = rooms?.length || 1 - if (selectedRateSummary.length === 0) return null - - const petRoomPackage = packages?.find( - (pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM - ) - - const totalPriceToShow = calculateTotalPrice( - selectedRateSummary, - isUserLoggedIn, - petRoomPackage - ) - const isAllRoomsSelected = selectedRateSummary.length === totalRoomsRequired - - const checkInDate = new Date(roomsAvailability.checkInDate) - const checkOutDate = new Date(roomsAvailability.checkOutDate) - const nights = dt(checkOutDate).diff(dt(checkInDate), "days") - - const hasMemberRates = selectedRateSummary.some((room) => room.member) - - const showMemberDiscountBanner = hasMemberRates && !isUserLoggedIn - - const summaryPriceText = `${intl.formatMessage( - { id: "{totalNights, plural, one {# night} other {# nights}}" }, - { totalNights: nights } - )}, ${intl.formatMessage( - { id: "{totalAdults, plural, one {# adult} other {# adults}}" }, - { totalAdults: rooms.reduce((acc, room) => acc + room.adults, 0) } - )}${ - rooms.some((room) => room.childrenInRoom?.length) - ? `, ${intl.formatMessage( - { id: "{totalChildren, plural, one {# child} other {# children}}" }, - { - totalChildren: rooms.reduce( - (acc, room) => acc + (room.childrenInRoom?.length ?? 0), - 0 - ), - } - )}` - : "" - }, ${intl.formatMessage( - { id: "{totalRooms, plural, one {# room} other {# rooms}}" }, - { - totalRooms: rooms.length, - } - )}` - - return ( -
-
-
- {selectedRateSummary.map((room, index) => ( -
- - {intl.formatMessage( - { id: "Room {roomIndex}" }, - { roomIndex: index + 1 } - )} - - {room.roomType} - {`${room.priceName}, ${room.priceTerm}`} -
- ))} - {/* Render unselected rooms */} - {Array.from({ - length: totalRoomsRequired - selectedRateSummary.length, - }).map((_, index) => ( -
- - {intl.formatMessage( - { id: "Room {roomIndex}" }, - { roomIndex: selectedRateSummary.length + index + 1 } - )} - - - {intl.formatMessage({ id: "Select room" })} - -
- ))} -
-
- {showMemberDiscountBanner && ( -
- { - const memberPrice = - room.member?.localPrice.pricePerStay ?? 0 - const isPetRoom = room.features.some( - (feature) => feature.code === RoomPackageCodeEnum.PET_ROOM - ) - const petRoomPrice = - isPetRoom && petRoomPackage - ? Number(petRoomPackage.localPrice.totalPrice || 0) - : 0 - return total + memberPrice + petRoomPrice - }, 0), - currency: - selectedRateSummary[0].member?.localPrice.currency ?? - selectedRateSummary[0].public.localPrice.currency, - }} - /> -
- )} -
- - {intl.formatMessage( - { id: "Total price (incl VAT)" }, - { b: (str) => {str} } - )} - - {summaryPriceText} -
-
-
- - {formatPrice( - intl, - totalPriceToShow.local.price, - totalPriceToShow.local.currency - )} - - {totalPriceToShow?.requested ? ( - - {intl.formatMessage( - { id: "Approx. {value}" }, - { - value: formatPrice( - intl, - totalPriceToShow.requested.price, - totalPriceToShow.requested.currency - ), - } - )} - - ) : null} -
-
- -
-
-
- {showMemberDiscountBanner ? : null} - -
-
- ) -} diff --git a/components/HotelReservation/SelectRate/RoomSelectionPanel/index.tsx b/components/HotelReservation/SelectRate/RoomSelectionPanel/index.tsx deleted file mode 100644 index ffaf168fd..000000000 --- a/components/HotelReservation/SelectRate/RoomSelectionPanel/index.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { useSearchParams } from "next/navigation" -import { useMemo } from "react" -import { useIntl } from "react-intl" - -import { alternativeHotels } from "@/constants/routes/hotelReservation" -import { useRoomFilteringStore } from "@/stores/select-rate/room-filtering" - -import Alert from "@/components/TempDesignSystem/Alert" -import useLang from "@/hooks/useLang" - -import RoomTypeFilter from "../RoomTypeFilter" -import RoomTypeList from "../RoomTypeList" - -import styles from "../Rooms/rooms.module.css" - -import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" -import type { FilterValues } from "@/types/components/hotelReservation/selectRate/roomFilter" -import type { RoomSelectionPanelProps } from "@/types/components/hotelReservation/selectRate/roomSelection" -import { AlertTypeEnum } from "@/types/enums/alert" - -export function RoomSelectionPanel({ - availablePackages, - defaultPackages, - hotelType, - roomCategories, - roomListIndex, - selectedPackages, -}: RoomSelectionPanelProps) { - const searchParams = useSearchParams() - const intl = useIntl() - const lang = useLang() - - const { getRooms } = useRoomFilteringStore() - - const rooms = getRooms(roomListIndex) - - const initialFilterValues = useMemo(() => { - const packagesFromSearchParams = - searchParams.get(`room[${roomListIndex}].packages`)?.split(",") ?? [] - - return defaultPackages.reduce((acc, option) => { - acc[option.code] = packagesFromSearchParams.includes(option.code) - return acc - }, {}) - }, [defaultPackages, searchParams, roomListIndex]) - - return ( - <> - {rooms?.roomConfigurations.every( - (roomConfig) => roomConfig.status === AvailabilityEnum.NotAvailable - ) && ( -
- -
- )} - - - {rooms && ( - - )} - - ) -} diff --git a/components/HotelReservation/SelectRate/RoomTypeFilter/index.tsx b/components/HotelReservation/SelectRate/RoomTypeFilter/index.tsx deleted file mode 100644 index 10de5d1fd..000000000 --- a/components/HotelReservation/SelectRate/RoomTypeFilter/index.tsx +++ /dev/null @@ -1,168 +0,0 @@ -"use client" - -import { zodResolver } from "@hookform/resolvers/zod" -import { useEffect, useState } from "react" -import { FormProvider, useForm } from "react-hook-form" -import { useIntl } from "react-intl" -import { useMediaQuery } from "usehooks-ts" -import { z } from "zod" - -import { useRoomFilteringStore } from "@/stores/select-rate/room-filtering" - -import { getIconForFeatureCode } from "@/components/HotelReservation/utils" -import { InfoCircleIcon } from "@/components/Icons" -import CheckboxChip from "@/components/TempDesignSystem/Form/FilterChip/Checkbox" -import Body from "@/components/TempDesignSystem/Text/Body" -import Caption from "@/components/TempDesignSystem/Text/Caption" -import { Tooltip } from "@/components/TempDesignSystem/Tooltip" - -import styles from "./roomFilter.module.css" - -import { - type FilterValues, - type RoomFilterProps, - RoomPackageCodeEnum, -} from "@/types/components/hotelReservation/selectRate/roomFilter" - -export default function RoomFilter({ - numberOfRooms, - filterOptions, - initialFilterValues, - roomListIndex, -}: RoomFilterProps) { - const isTabletAndUp = useMediaQuery("(min-width: 768px)") - const [isAboveMobile, setIsAboveMobile] = useState(false) - - const intl = useIntl() - const methods = useForm({ - defaultValues: initialFilterValues, - mode: "all", - reValidateMode: "onChange", - resolver: zodResolver(z.object({})), - }) - - const { handleFilter } = useRoomFilteringStore() - - const { watch, getValues } = methods - const petFriendly = watch(RoomPackageCodeEnum.PET_ROOM) - const allergyFriendly = watch(RoomPackageCodeEnum.ALLERGY_ROOM) - - const selectedFilters = getValues() - - const tooltipText = intl.formatMessage({ - id: "Pet-friendly rooms have an additional fee of 20 EUR per stay", - }) - - useEffect(() => { - if (!initialFilterValues) return - - handleFilter(initialFilterValues, roomListIndex) - }, [initialFilterValues, handleFilter, roomListIndex]) - - // Watch for filter changes - useEffect(() => { - const subscription = watch((value, { name }) => { - if (name) handleFilter(getValues(), roomListIndex) - }) - return () => subscription.unsubscribe() - }, [watch, getValues, handleFilter, roomListIndex]) - - useEffect(() => { - setIsAboveMobile(isTabletAndUp) - }, [isTabletAndUp]) - - return ( -
-
- - {intl.formatMessage( - { - id: "{numberOfRooms, plural, one {# room type} other {# room types}} available", - }, - { - numberOfRooms, - } - )} - -
-
-
- - -
- - {intl.formatMessage({ id: "Filter" })} - - - {Object.entries(selectedFilters) - .filter(([_, value]) => value) - .map(([key]) => intl.formatMessage({ id: key })) - .join(", ")} - -
-
-
- - {intl.formatMessage( - { - id: "{numberOfRooms, plural, one {# room type} other {# room types}} available", - }, - { - numberOfRooms, - } - )} - -
- -
-
- {filterOptions.map((option) => { - const { code, description, itemCode } = option - const isPetRoom = code === RoomPackageCodeEnum.PET_ROOM - const isAllergyRoom = code === RoomPackageCodeEnum.ALLERGY_ROOM - const isDisabled = - (isAllergyRoom && petFriendly) || - (isPetRoom && allergyFriendly) || - !itemCode - - const checkboxChip = ( - - ) - - return isPetRoom && isAboveMobile ? ( - - {checkboxChip} - - ) : ( - checkboxChip - ) - })} -
-
-
-
- ) -} diff --git a/components/HotelReservation/SelectRate/RoomTypeFilter/roomFilter.module.css b/components/HotelReservation/SelectRate/RoomTypeFilter/roomFilter.module.css deleted file mode 100644 index cc256b1f4..000000000 --- a/components/HotelReservation/SelectRate/RoomTypeFilter/roomFilter.module.css +++ /dev/null @@ -1,46 +0,0 @@ -.container { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: flex-start; -} - -.roomsFilter { - display: flex; - flex-direction: row; - gap: var(--Spacing-x1); - align-items: center; -} - -.filter { - display: flex; - gap: var(--Spacing-x-half); - margin-left: var(--Spacing-x-half); - align-items: baseline; -} - -.filterInfo { - display: flex; - flex-direction: row; - gap: var(--Spacing-x-half); - flex-wrap: wrap; - margin-right: var(--Spacing-x1); -} - -.infoDesktop { - display: none; -} - -.infoMobile { - display: block; -} - -@media (min-width: 768px) { - .infoDesktop { - display: block; - } - - .infoMobile { - display: none; - } -} diff --git a/components/HotelReservation/SelectRate/RoomTypeList/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomTypeList/RoomCard/index.tsx deleted file mode 100644 index 6ead0edfc..000000000 --- a/components/HotelReservation/SelectRate/RoomTypeList/RoomCard/index.tsx +++ /dev/null @@ -1,417 +0,0 @@ -"use client" - -import { useSearchParams } from "next/navigation" -import { useSession } from "next-auth/react" -import { createElement, useCallback, useEffect, useMemo } from "react" -import { useIntl } from "react-intl" - -import { useRateSelectionStore } from "@/stores/select-rate/rate-selection" - -import ToggleSidePeek from "@/components/HotelReservation/EnterDetails/SelectedRoom/ToggleSidePeek" -import { getIconForFeatureCode } from "@/components/HotelReservation/utils" -import { ErrorCircleIcon } from "@/components/Icons" -import ImageGallery from "@/components/ImageGallery" -import Caption from "@/components/TempDesignSystem/Text/Caption" -import Footnote from "@/components/TempDesignSystem/Text/Footnote" -import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import { mapApiImagesToGalleryImages } from "@/utils/imageGallery" - -import FlexibilityOption from "../FlexibilityOption" -import { cardVariants } from "./cardVariants" - -import styles from "./roomCard.module.css" - -import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" -import type { RoomCardProps } from "@/types/components/hotelReservation/selectRate/roomCard" -import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" -import { HotelTypeEnum } from "@/types/enums/hotelType" -import type { Product } from "@/types/trpc/routers/hotel/roomAvailability" - -function getBreakfastMessage( - publicBreakfastIncluded: boolean, - memberBreakfastIncluded: boolean, - hotelType: string | undefined, - userIsLoggedIn: boolean, - msgs: Record<"included" | "noSelection" | "scandicgo" | "notIncluded", string> -) { - if (hotelType === HotelTypeEnum.ScandicGo) { - return msgs.scandicgo - } - - if (userIsLoggedIn && memberBreakfastIncluded) { - return msgs.included - } - - if (publicBreakfastIncluded && memberBreakfastIncluded) { - return msgs.included - } - - /** selected and rate does not include breakfast */ - if (false) { - return msgs.notIncluded - } - - if (!publicBreakfastIncluded && !memberBreakfastIncluded) { - return msgs.notIncluded - } - - return msgs.noSelection -} - -export default function RoomCard({ - hotelId, - hotelType, - rateDefinitions, - roomConfiguration, - roomCategories, - selectedPackages, - packages, - roomListIndex, -}: RoomCardProps) { - const { data: session } = useSession() - const isUserLoggedIn = !!session - const intl = useIntl() - const searchParams = useSearchParams() - const { selectRate, selectedRates, closeModifyRate } = useRateSelectionStore( - (state) => ({ - selectRate: state.selectRate, - selectedRates: state.selectedRates, - closeModifyRate: state.closeModifyRate, - }) - ) - - const selectedRate = useRateSelectionStore( - (state) => state.selectedRates[roomListIndex] - ) - - const classNames = cardVariants({ - availability: - roomConfiguration.status === AvailabilityEnum.NotAvailable - ? "noAvailability" - : "default", - }) - - const breakfastMessages = { - included: intl.formatMessage({ id: "Breakfast is included." }), - notIncluded: intl.formatMessage({ - id: "Breakfast selection in next step.", - }), - noSelection: intl.formatMessage({ id: "Select a rate" }), - scandicgo: intl.formatMessage({ - id: "Breakfast deal can be purchased at the hotel.", - }), - } - const breakfastMessage = getBreakfastMessage( - roomConfiguration.breakfastIncludedInAllRatesPublic, - roomConfiguration.breakfastIncludedInAllRatesMember, - hotelType, - isUserLoggedIn, - breakfastMessages - ) - - const rates = useMemo( - () => ({ - change: rateDefinitions.filter( - (rate) => rate.cancellationRule === "Changeable" - ), - flex: rateDefinitions.filter( - (rate) => rate.cancellationRule === "CancellableBefore6PM" - ), - save: rateDefinitions.filter( - (rate) => rate.cancellationRule === "NotCancellable" - ), - }), - [rateDefinitions] - ) - - const petRoomPackage = - (selectedPackages?.includes(RoomPackageCodeEnum.PET_ROOM) && - packages?.find((pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM)) || - undefined - - const selectedRoom = roomCategories.find((roomCategory) => - roomCategory.roomTypes.find( - (roomType) => roomType.code === roomConfiguration.roomTypeCode - ) - ) - - const { name, roomSize, totalOccupancy, images } = selectedRoom || {} - const galleryImages = mapApiImagesToGalleryImages(images || []) - - const freeCancelation = intl.formatMessage({ id: "Free cancellation" }) - const nonRefundable = intl.formatMessage({ id: "Non-refundable" }) - const freeBooking = intl.formatMessage({ id: "Free rebooking" }) - const payLater = intl.formatMessage({ id: "Pay later" }) - const payNow = intl.formatMessage({ id: "Pay now" }) - - function handleRateSelection( - rateCode: string, - rateName: string, - paymentTerm: string - ) { - if ( - selectedRates[roomListIndex]?.publicRateCode === rateCode && - selectedRates[roomListIndex]?.roomTypeCode === - roomConfiguration.roomTypeCode - ) { - selectRate(roomListIndex, undefined) - } else { - selectRate(roomListIndex, { - publicRateCode: rateCode, - roomTypeCode: roomConfiguration.roomTypeCode, - name: rateName, - paymentTerm: paymentTerm, - }) - } - } - - const getRate = useCallback( - (rateCode: string) => { - switch (rateCode) { - case "change": - return { - isFlex: false, - notAvailable: false, - title: freeBooking, - } - case "flex": - return { - isFlex: true, - notAvailable: false, - title: freeCancelation, - } - case "save": - return { - isFlex: false, - notAvailable: false, - title: nonRefundable, - } - default: - throw new Error( - `Unknown key for rate, should be "change", "flex" or "save", but got ${rateCode}` - ) - } - }, - [freeBooking, freeCancelation, nonRefundable] - ) - - const getRateInfo = useCallback( - (product: Product) => { - if ( - !product.productType.public.rateCode && - !product.productType.member?.rateCode - ) { - const possibleRate = getRate(product.productType.public.rate) - if (possibleRate) { - return { - ...possibleRate, - notAvailable: true, - } - } - return { - isFlex: false, - notAvailable: true, - title: "", - } - } - const publicRate = Object.keys(rates).find((k) => - rates[k as keyof typeof rates].find( - (a) => a.rateCode === product.productType.public.rateCode - ) - ) - let memberRate - if (product.productType.member) { - memberRate = Object.keys(rates).find((k) => - rates[k as keyof typeof rates].find( - (a) => a.rateCode === product.productType.member!.rateCode - ) - ) - } - - if (!publicRate || !memberRate) { - throw new Error("We should never make it here without rateCodes") - } - - const key = isUserLoggedIn ? memberRate : publicRate - return getRate(key) - }, - [getRate, isUserLoggedIn, rates] - ) - - // Handle URL-based preselection - useEffect(() => { - const ratecodeSearchParam = searchParams.get( - `room[${roomListIndex}].ratecode` - ) - const roomtypeSearchParam = searchParams.get( - `room[${roomListIndex}].roomtype` - ) - - if (!ratecodeSearchParam || !roomtypeSearchParam) return - - // Check if there's already a selection for this room index - const existingSelection = selectedRates[roomListIndex] - if (existingSelection) return - - const matchingRate = roomConfiguration.products.find( - (product) => - product.productType.public.rateCode === ratecodeSearchParam && - roomConfiguration.roomTypeCode === roomtypeSearchParam - ) - - if (matchingRate) { - const rateInfo = getRateInfo(matchingRate) - selectRate(roomListIndex, { - publicRateCode: matchingRate.productType.public.rateCode, - roomTypeCode: roomConfiguration.roomTypeCode, - name: rateInfo.title, - paymentTerm: rateInfo.isFlex ? payLater : payNow, - }) - } - }, [ - searchParams, - roomListIndex, - rates, - roomConfiguration.products, - roomConfiguration.roomTypeCode, - payLater, - payNow, - selectRate, - selectedRates, - getRateInfo, - ]) - - return ( -
  • -
    -
    -
    - {roomConfiguration.roomsLeft > 0 && - roomConfiguration.roomsLeft < 5 && ( - - - {intl.formatMessage( - { id: "{amount, number} left" }, - { amount: roomConfiguration.roomsLeft } - )} - - - )} - {roomConfiguration.features - .filter((feature) => selectedPackages?.includes(feature.code)) - .map((feature) => ( - - {createElement(getIconForFeatureCode(feature.code), { - width: 16, - height: 16, - color: "burgundy", - })} - - ))} -
    - -
    - -
    - {totalOccupancy && ( - - {intl.formatMessage( - { - id: "Max. {max, plural, one {{range} guest} other {{range} guests}}", - }, - { - max: totalOccupancy.max, - range: totalOccupancy.range, - } - )} - - )} - {roomSize && ( - - {roomSize.min === roomSize.max - ? intl.formatMessage( - { id: "{roomSize} m²" }, - { - roomSize: roomSize.min, - } - ) - : intl.formatMessage( - { - id: "{roomSizeMin}–{roomSizeMax} m²", - }, - { - roomSizeMin: roomSize.min, - roomSizeMax: roomSize.max, - } - )} - - )} -
    - {roomConfiguration.roomTypeCode && ( - - )} -
    -
    -
    - - {name} - - {/* Out of scope for now - {descriptions?.short} - */} -
    -
    - -
    - {roomConfiguration.status === AvailabilityEnum.Available ? ( - - {breakfastMessage} - - ) : null} - {roomConfiguration.status === AvailabilityEnum.NotAvailable ? ( -
    -
    - - - {intl.formatMessage({ - id: "This room is not available", - })} - -
    -
    - ) : ( - roomConfiguration.products.map((product) => { - const rate = getRateInfo(product) - return ( - { - handleRateSelection(rateCode, rateName, paymentTerm) - closeModifyRate() - }} - isSelected={ - selectedRate?.publicRateCode === - product.productType.public.rateCode && - selectedRate?.roomTypeCode === roomConfiguration.roomTypeCode - } - isUserLoggedIn={isUserLoggedIn} - paymentTerm={rate.isFlex ? payLater : payNow} - petRoomPackage={petRoomPackage} - product={rate?.notAvailable ? undefined : product} - roomTypeCode={roomConfiguration.roomTypeCode} - title={rate.title} - /> - ) - }) - )} -
    -
  • - ) -} diff --git a/components/HotelReservation/SelectRate/RoomTypeList/index.tsx b/components/HotelReservation/SelectRate/RoomTypeList/index.tsx deleted file mode 100644 index 9db58c411..000000000 --- a/components/HotelReservation/SelectRate/RoomTypeList/index.tsx +++ /dev/null @@ -1,38 +0,0 @@ -"use client" - -import RoomCard from "./RoomCard" - -import styles from "./roomSelection.module.css" - -import type { RoomTypeListProps } from "@/types/components/hotelReservation/selectRate/roomSelection" - -export default function RoomTypeList({ - availablePackages, - hotelType, - roomCategories, - roomListIndex, - roomsAvailability, - selectedPackages, -}: RoomTypeListProps) { - const { roomConfigurations, rateDefinitions } = roomsAvailability - - return ( -
    -
      - {roomConfigurations.map((roomConfiguration) => ( - - ))} -
    -
    - ) -} diff --git a/components/HotelReservation/SelectRate/Rooms/RoomsContainer.tsx b/components/HotelReservation/SelectRate/Rooms/RoomsContainer.tsx deleted file mode 100644 index 58f1206e3..000000000 --- a/components/HotelReservation/SelectRate/Rooms/RoomsContainer.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { dt } from "@/lib/dt" -import { - getHotel, - getPackages, - getRoomsAvailability, -} from "@/lib/trpc/memoizedRequests" - -import { auth } from "@/auth" -import { getIntl } from "@/i18n" -import { safeTry } from "@/utils/safeTry" -import { isValidSession } from "@/utils/session" - -import { generateChildrenString } from "../../utils" -import { combineRoomAvailabilities } from "../utils" -import Rooms from "." - -import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" -import type { RoomsContainerProps } from "@/types/components/hotelReservation/selectRate/roomsContainer" - -export async function RoomsContainer({ - adultArray, - childArray, - fromDate, - hotelId, - lang, - toDate, -}: RoomsContainerProps) { - const session = await auth() - const isUserLoggedIn = isValidSession(session) - - const fromDateString = dt(fromDate).format("YYYY-MM-DD") - const toDateString = dt(toDate).format("YYYY-MM-DD") - - const hotelDataPromise = safeTry( - getHotel({ - hotelId: hotelId.toString(), - isCardOnlyPayment: false, - language: lang, - }) - ) - - const packagesPromise = safeTry( - getPackages({ - hotelId: hotelId.toString(), - startDate: fromDateString, - endDate: toDateString, - adults: adultArray[0], - children: childArray ? childArray.length : undefined, - packageCodes: [ - RoomPackageCodeEnum.ACCESSIBILITY_ROOM, - RoomPackageCodeEnum.PET_ROOM, - RoomPackageCodeEnum.ALLERGY_ROOM, - ], - }) - ) - - const uniqueAdultCounts = [...new Set(adultArray)] - const roomsAvailabilityPromises = uniqueAdultCounts.map((adultCount) => { - return safeTry( - getRoomsAvailability({ - hotelId: hotelId, - roomStayStartDate: fromDateString, - roomStayEndDate: toDateString, - adults: adultCount, - children: - childArray && childArray.length > 0 - ? generateChildrenString(childArray) - : undefined, - }) - ) - }) - - const [hotelData, hotelDataError] = await hotelDataPromise - const [packages, packagesError] = await packagesPromise - const roomsAvailabilityResults = await Promise.all(roomsAvailabilityPromises) - - const roomsAvailability = combineRoomAvailabilities({ - availabilityResults: roomsAvailabilityResults, - }) - - const intl = await getIntl(lang) - - if (packagesError) { - // TODO: Log packages error - console.error("[RoomsContainer] unable to fetch packages") - } - - if (!hotelData) { - // TODO: Log hotel data error - console.error("[RoomsContainer] unable to fetch hotel data") - return null - } - - return ( - - ) -} diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx deleted file mode 100644 index c68b31bda..000000000 --- a/components/HotelReservation/SelectRate/Rooms/index.tsx +++ /dev/null @@ -1,325 +0,0 @@ -"use client" - -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" - -import RateSummary from "../RateSummary" -import { RoomSelectionPanel } from "../RoomSelectionPanel" -import SelectedRoomPanel from "../SelectedRoomPanel" -import { roomSelectionPanelVariants } from "./variants" - -import styles from "./rooms.module.css" - -import { - type DefaultFilterOptions, - RoomPackageCodeEnum, -} from "@/types/components/hotelReservation/selectRate/roomFilter" -import type { SelectRateProps } from "@/types/components/hotelReservation/selectRate/roomSelection" -import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" - -export default function Rooms({ - availablePackages, - hotelType, - isUserLoggedIn, - roomsAvailability, - roomCategories = [], - vat, -}: SelectRateProps) { - const router = useRouter() - const searchParams = useSearchParams() - const intl = useIntl() - const [isPending, startTransition] = useTransition() - - const hotelId = searchParams.get("hotel") - const arrivalDate = searchParams.get("fromDate") - const departureDate = searchParams.get("toDate") - - const { - selectedRates, - rateSummary, - calculateRateSummary, - initializeRates, - setGuestsInRooms, - modifyRateIndex, - closeModifyRate, - } = useRateSelectionStore() - - const { - selectedPackagesByRoom, - visibleRooms, - setVisibleRooms, - setRoomsAvailability, - getFilteredRooms, - } = useRoomFilteringStore() - - const bookingWidgetSearchData = useMemo( - () => - convertSearchParamsToObj( - Object.fromEntries(searchParams) - ), - [searchParams] - ) - - useEffect(() => { - bookingWidgetSearchData.rooms.forEach((room, index) => { - setGuestsInRooms(index, room.adults, room.childrenInRoom) - }) - }, [bookingWidgetSearchData.rooms, setGuestsInRooms]) - - const isMultipleRooms = bookingWidgetSearchData.rooms.length > 1 - - useEffect(() => { - initializeRates(bookingWidgetSearchData.rooms.length) - }, [initializeRates, bookingWidgetSearchData.rooms.length]) - - const defaultPackages: DefaultFilterOptions[] = useMemo( - () => [ - { - code: RoomPackageCodeEnum.ACCESSIBILITY_ROOM, - description: intl.formatMessage({ id: "Accessible room" }), - itemCode: availablePackages.find( - (pkg) => pkg.code === RoomPackageCodeEnum.ACCESSIBILITY_ROOM - )?.itemCode, - }, - { - code: RoomPackageCodeEnum.ALLERGY_ROOM, - description: intl.formatMessage({ id: "Allergy-friendly room" }), - itemCode: availablePackages.find( - (pkg) => pkg.code === RoomPackageCodeEnum.ALLERGY_ROOM - )?.itemCode, - }, - { - code: RoomPackageCodeEnum.PET_ROOM, - description: intl.formatMessage({ id: "Pet room" }), - itemCode: availablePackages.find( - (pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM - )?.itemCode, - }, - ], - [availablePackages, intl] - ) - - useEffect(() => { - if (roomsAvailability) { - setRoomsAvailability(roomsAvailability) - } - setVisibleRooms() - }, [roomsAvailability, setRoomsAvailability, setVisibleRooms]) - - useEffect(() => { - if ( - selectedRates.length > 0 && - selectedRates.some((rate) => rate !== undefined) - ) { - calculateRateSummary({ - getFilteredRooms, - availablePackages, - roomCategories, - selectedPackagesByRoom, - }) - } - }, [ - selectedRates, - getFilteredRooms, - availablePackages, - roomCategories, - selectedPackagesByRoom, - calculateRateSummary, - ]) - - useEffect(() => { - const pricesWithCurrencies = visibleRooms.flatMap((room) => - room.products.map((product) => ({ - price: product.productType.public.localPrice.pricePerNight, - currency: product.productType.public.localPrice.currency, - })) - ) - const lowestPrice = pricesWithCurrencies.reduce( - (minPrice, { price }) => Math.min(minPrice, price), - Infinity - ) - - const currency = pricesWithCurrencies[0]?.currency - - trackLowestRoomPrice({ - hotelId, - arrivalDate, - departureDate, - lowestPrice: lowestPrice, - currency: currency, - }) - }, [arrivalDate, departureDate, hotelId, visibleRooms]) - - 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] || [], - })) - - const newSearchParams = convertObjToSearchParams({ rooms }, searchParams) - router.push(`details?${newSearchParams}`) - }) - } - - useEffect(() => { - requestAnimationFrame(() => { - const SCROLL_OFFSET = 100 - const roomElements = document.querySelectorAll(`.${styles.roomContainer}`) - - 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 - - window.scrollTo({ - top: offsetPosition, - behavior: "smooth", - }) - } - }) - }, [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 roomState = getRoomState(index) - const classNames = roomSelectionPanelVariants(roomState) - - return ( -
    - {!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} -
    - )} - -
    -
    - -
    -
    - -
    -
    -
    - ) - }) - ) : ( - - )} - - {rateSummary && roomsAvailability && ( -
    - - - )} -
    - ) -} diff --git a/components/HotelReservation/SelectRate/Rooms/utils.ts b/components/HotelReservation/SelectRate/Rooms/utils.ts deleted file mode 100644 index fdedec453..000000000 --- a/components/HotelReservation/SelectRate/Rooms/utils.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" -import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability" - -/** - * Get the lowest priced room for each room type that appears more than once. - */ - -export function filterDuplicateRoomTypesByLowestPrice( - roomConfigurations: RoomConfiguration[] -): RoomConfiguration[] { - const roomTypeCount = roomConfigurations.reduce>( - (roomTypeTally, currentRoom) => { - const currentRoomType = currentRoom.roomType - const currentCount = roomTypeTally[currentRoomType] || 0 - - return { - ...roomTypeTally, - [currentRoomType]: currentCount + 1, - } - }, - {} - ) - - const duplicateRoomTypes = new Set( - Object.keys(roomTypeCount).filter((roomType) => roomTypeCount[roomType] > 1) - ) - - const roomMap = new Map() - - roomConfigurations.forEach((room) => { - const { roomType, products, status } = room - - if (!duplicateRoomTypes.has(roomType)) { - roomMap.set(roomType, room) - return - } - - const previousRoom = roomMap.get(roomType) - - // Prioritize 'Available' status - if ( - status === AvailabilityEnum.Available && - previousRoom?.status === AvailabilityEnum.NotAvailable - ) { - roomMap.set(roomType, room) - return - } - - if ( - status === AvailabilityEnum.NotAvailable && - previousRoom?.status === AvailabilityEnum.Available - ) { - return - } - - if (previousRoom) { - products.forEach((product) => { - const { productType } = product - const publicProduct = productType.public || { - requestedPrice: null, - localPrice: null, - } - const memberProduct = productType.member || { - requestedPrice: null, - localPrice: null, - } - - const { - requestedPrice: publicRequestedPrice, - localPrice: publicLocalPrice, - } = publicProduct - const { - requestedPrice: memberRequestedPrice, - localPrice: memberLocalPrice, - } = memberProduct - - const previousLowest = roomMap.get(roomType) - - const currentRequestedPrice = Math.min( - Number(publicRequestedPrice?.pricePerNight) ?? Infinity, - Number(memberRequestedPrice?.pricePerNight) ?? Infinity - ) - const currentLocalPrice = Math.min( - Number(publicLocalPrice?.pricePerNight) ?? Infinity, - Number(memberLocalPrice?.pricePerNight) ?? Infinity - ) - - if ( - !previousLowest || - currentRequestedPrice < - Math.min( - Number( - previousLowest.products[0].productType.public.requestedPrice - ?.pricePerNight - ) ?? Infinity, - Number( - previousLowest.products[0].productType.member?.requestedPrice - ?.pricePerNight - ) ?? Infinity - ) || - (currentRequestedPrice === - Math.min( - Number( - previousLowest.products[0].productType.public.requestedPrice - ?.pricePerNight - ) ?? Infinity, - Number( - previousLowest.products[0].productType.member?.requestedPrice - ?.pricePerNight - ) ?? Infinity - ) && - currentLocalPrice < - Math.min( - Number( - previousLowest.products[0].productType.public.localPrice - ?.pricePerNight - ) ?? Infinity, - Number( - previousLowest.products[0].productType.member?.localPrice - ?.pricePerNight - ) ?? Infinity - )) - ) { - roomMap.set(roomType, room) - } - }) - } else { - roomMap.set(roomType, room) - } - }) - - return Array.from(roomMap.values()) -} diff --git a/components/HotelReservation/SelectRate/RateSummary/MobileSummary/Summary.tsx b/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/Summary.tsx similarity index 95% rename from components/HotelReservation/SelectRate/RateSummary/MobileSummary/Summary.tsx rename to components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/Summary.tsx index 1f5bae966..14105c32e 100644 --- a/components/HotelReservation/SelectRate/RateSummary/MobileSummary/Summary.tsx +++ b/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/Summary.tsx @@ -1,6 +1,5 @@ "use client" - -import React from "react" +import { Fragment } from "react" import { useIntl } from "react-intl" import { dt } from "@/lib/dt" @@ -34,7 +33,6 @@ export default function Summary({ isMember, vat, toggleSummaryOpen, - togglePriceDetailsModalOpen, }: SelectRateSummaryProps) { const intl = useIntl() const lang = useLang() @@ -46,18 +44,6 @@ export default function Summary({ { totalNights: diff } ) - function handleToggleSummary() { - if (toggleSummaryOpen) { - toggleSummaryOpen() - } - } - - function handleTogglePriceDetailsModal() { - if (togglePriceDetailsModalOpen) { - togglePriceDetailsModalOpen() - } - } - function getMemberPrice(roomRate: RoomRate) { return roomRate?.memberRate ? { @@ -85,7 +71,7 @@ export default function Summary({ intent="text" size="small" className={styles.chevronButton} - onClick={handleToggleSummary} + onClick={toggleSummaryOpen} > @@ -135,7 +121,7 @@ export default function Summary({ } return ( - +
    - + ) })}
    @@ -260,7 +246,6 @@ export default function Summary({ }))} totalPrice={totalPrice} vat={vat} - toggleModal={handleTogglePriceDetailsModal} />
    diff --git a/components/HotelReservation/SelectRate/RateSummary/MobileSummary/index.tsx b/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/index.tsx similarity index 78% rename from components/HotelReservation/SelectRate/RateSummary/MobileSummary/index.tsx rename to components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/index.tsx index 3636b54aa..203b1af88 100644 --- a/components/HotelReservation/SelectRate/RateSummary/MobileSummary/index.tsx +++ b/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/index.tsx @@ -1,7 +1,8 @@ -import { useEffect, useRef } from "react" +"use client" +import { useEffect, useRef, useState } from "react" import { useIntl } from "react-intl" -import { useRateSelectionStore } from "@/stores/select-rate/rate-selection" +import { useRatesStore } from "@/stores/select-rate" import Button from "@/components/TempDesignSystem/Button" import Caption from "@/components/TempDesignSystem/Text/Caption" @@ -15,60 +16,26 @@ import styles from "./mobileSummary.module.css" import type { MobileSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary" export default function MobileSummary({ - totalPriceToShow, isAllRoomsSelected, - booking, isUserLoggedIn, - vat, - roomsAvailability, + totalPriceToShow, }: MobileSummaryProps) { const intl = useIntl() const scrollY = useRef(0) + const [isSummaryOpen, setIsSummaryOpen] = useState(false) - const { - guestsInRooms, - isSummaryOpen, - getSelectedRateSummary, - toggleSummaryOpen, - togglePriceDetailsModalOpen, - } = useRateSelectionStore() + const { booking, bookingRooms, rateDefinitions, rateSummary, vat } = + useRatesStore((state) => ({ + booking: state.booking, + bookingRooms: state.booking.rooms, + rateDefinitions: state.roomsAvailability?.rateDefinitions, + rateSummary: state.rateSummary, + vat: state.vat, + })) - const selectedRateSummary = getSelectedRateSummary() - - const rooms = selectedRateSummary.map((room, index) => ({ - adults: guestsInRooms[index].adults, - childrenInRoom: guestsInRooms[index].children, - roomType: room.roomType, - roomPrice: { - perNight: { - local: { - price: room.public.localPrice.pricePerNight, - currency: room.public.localPrice.currency, - }, - requested: undefined, - }, - perStay: { - local: { - price: room.public.localPrice.pricePerStay, - currency: room.public.localPrice.currency, - }, - requested: undefined, - }, - currency: room.public.localPrice.currency, - }, - roomRate: { - ...room.public, - memberRate: room.member, - publicRate: room.public, - }, - rateDetails: roomsAvailability.rateDefinitions.find( - (rate) => rate.rateCode === room.public.rateCode - )?.generalTerms, - cancellationText: - roomsAvailability.rateDefinitions.find( - (rate) => rate.rateCode === room.public.rateCode - )?.cancellationText ?? "", - })) + function toggleSummaryOpen() { + setIsSummaryOpen(!isSummaryOpen) + } useEffect(() => { if (isSummaryOpen) { @@ -93,6 +60,44 @@ export default function MobileSummary({ } }, [isSummaryOpen]) + if (!rateDefinitions) { + return null + } + + const rooms = rateSummary.map((room, index) => ({ + adults: bookingRooms[index].adults, + childrenInRoom: bookingRooms[index].childrenInRoom ?? undefined, + roomType: room.roomType, + roomPrice: { + perNight: { + local: { + price: room.public.localPrice.pricePerNight, + currency: room.public.localPrice.currency, + }, + requested: undefined, + }, + perStay: { + local: { + price: room.public.localPrice.pricePerStay, + currency: room.public.localPrice.currency, + }, + requested: undefined, + }, + currency: room.public.localPrice.currency, + }, + roomRate: { + ...room.public, + memberRate: room.member, + publicRate: room.public, + }, + rateDetails: rateDefinitions.find( + (rate) => rate.rateCode === room.public.rateCode + )?.generalTerms, + cancellationText: + rateDefinitions.find((rate) => rate.rateCode === room.public.rateCode) + ?.cancellationText ?? "", + })) + return (
    @@ -104,7 +109,6 @@ export default function MobileSummary({ totalPrice={totalPriceToShow} vat={vat} toggleSummaryOpen={toggleSummaryOpen} - togglePriceDetailsModalOpen={togglePriceDetailsModalOpen} />
    diff --git a/components/HotelReservation/SelectRate/RateSummary/MobileSummary/mobileSummary.module.css b/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/mobileSummary.module.css similarity index 100% rename from components/HotelReservation/SelectRate/RateSummary/MobileSummary/mobileSummary.module.css rename to components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/mobileSummary.module.css diff --git a/components/HotelReservation/SelectRate/RateSummary/MobileSummary/summary.module.css b/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/summary.module.css similarity index 100% rename from components/HotelReservation/SelectRate/RateSummary/MobileSummary/summary.module.css rename to components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/summary.module.css diff --git a/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/index.tsx b/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/index.tsx new file mode 100644 index 000000000..586b27e52 --- /dev/null +++ b/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/index.tsx @@ -0,0 +1,269 @@ +"use client" +import { useRouter } from "next/navigation" +import { useTransition } from "react" +import { useIntl } from "react-intl" + +import { dt } from "@/lib/dt" +import { useRatesStore } from "@/stores/select-rate" + +import { getRates } from "@/components/HotelReservation/SelectRate/utils" +import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop" +import SignupPromoMobile from "@/components/HotelReservation/SignupPromo/Mobile" +import Button from "@/components/TempDesignSystem/Button" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Footnote from "@/components/TempDesignSystem/Text/Footnote" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import { formatPrice } from "@/utils/numberFormatting" + +import MobileSummary from "./MobileSummary" +import { calculateTotalPrice } from "./utils" + +import styles from "./rateSummary.module.css" + +import type { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary" +import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" + +export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) { + const { + bookingRooms, + petRoomPackage, + rateSummary, + roomsAvailability, + searchParams, + } = useRatesStore((state) => ({ + bookingRooms: state.booking.rooms, + petRoomPackage: state.petRoomPackage, + rateSummary: state.rateSummary, + roomsAvailability: state.roomsAvailability, + searchParams: state.searchParams, + })) + const intl = useIntl() + const router = useRouter() + const params = new URLSearchParams(searchParams) + const [_, startTransition] = useTransition() + + if (!roomsAvailability) { + return null + } + + const checkInDate = new Date(roomsAvailability.checkInDate) + const checkOutDate = new Date(roomsAvailability.checkOutDate) + const nights = dt(checkOutDate).diff(dt(checkInDate), "days") + + const totalNights = intl.formatMessage( + { id: "{totalNights, plural, one {# night} other {# nights}}" }, + { totalNights: nights } + ) + const totalAdults = intl.formatMessage( + { id: "{totalAdults, plural, one {# adult} other {# adults}}" }, + { totalAdults: bookingRooms.reduce((acc, room) => acc + room.adults, 0) } + ) + const childrenInOneOrMoreRooms = bookingRooms.some( + (room) => room.childrenInRoom?.length + ) + const childrenInroom = intl.formatMessage( + { id: "{totalChildren, plural, one {# child} other {# children}}" }, + { + totalChildren: bookingRooms.reduce( + (acc, room) => acc + (room.childrenInRoom?.length ?? 0), + 0 + ), + } + ) + const totalChildren = childrenInOneOrMoreRooms ? `, ${childrenInroom}` : "" + const totalRooms = intl.formatMessage( + { id: "{totalRooms, plural, one {# room} other {# rooms}}" }, + { totalRooms: bookingRooms.length } + ) + + const summaryPriceText = `${totalNights}, ${totalAdults}${totalChildren}, ${totalRooms}` + + const totalRoomsRequired = bookingRooms.length + const isAllRoomsSelected = rateSummary.length === totalRoomsRequired + const hasMemberRates = rateSummary.some((room) => room.member) + const showMemberDiscountBanner = hasMemberRates && !isUserLoggedIn + + const rates = getRates(roomsAvailability.rateDefinitions) + + const freeCancelation = intl.formatMessage({ id: "Free cancellation" }) + const nonRefundable = intl.formatMessage({ id: "Non-refundable" }) + const freeBooking = intl.formatMessage({ id: "Free rebooking" }) + const payLater = intl.formatMessage({ id: "Pay later" }) + const payNow = intl.formatMessage({ id: "Pay now" }) + + function getRateDetails(rateCode: string) { + const rate = Object.keys(rates).find((k) => + rates[k as keyof typeof rates].find((a) => a.rateCode === rateCode) + ) + + switch (rate) { + case "change": + return `${freeBooking}, ${payNow}` + case "flex": + return `${freeCancelation}, ${payLater}` + case "save": + default: + return `${nonRefundable}, ${payNow}` + } + } + + function handleSubmit(e: React.FormEvent) { + e.preventDefault() + startTransition(() => { + router.push(`details?${params}`) + }) + } + + if (!rateSummary.length) { + return null + } + + const totalPriceToShow = calculateTotalPrice( + rateSummary, + isUserLoggedIn, + petRoomPackage + ) + + return ( +
    +
    +
    +
    + {rateSummary.map((room, index) => ( +
    + + {intl.formatMessage( + { id: "Room {roomIndex}" }, + { roomIndex: index + 1 } + )} + + {room.roomType} + + {getRateDetails( + isUserLoggedIn && room.member + ? room.member?.rateCode + : room.public.rateCode + )} + +
    + ))} + {/* Render unselected rooms */} + {Array.from({ + length: totalRoomsRequired - rateSummary.length, + }).map((_, index) => ( +
    + + {intl.formatMessage( + { id: "Room {roomIndex}" }, + { roomIndex: rateSummary.length + index + 1 } + )} + + + {intl.formatMessage({ id: "Select room" })} + +
    + ))} +
    +
    + {showMemberDiscountBanner && ( +
    + { + const memberPrice = + room.member?.localPrice.pricePerStay ?? 0 + const isPetRoom = room.features.find( + (feature) => + feature.code === RoomPackageCodeEnum.PET_ROOM + ) + const petRoomPrice = + isPetRoom && petRoomPackage + ? Number(petRoomPackage.localPrice.totalPrice) + : 0 + return total + memberPrice + petRoomPrice + }, 0), + currency: + rateSummary[0].member?.localPrice.currency ?? + rateSummary[0].public.localPrice.currency, + }} + /> +
    + )} +
    + + {intl.formatMessage( + { id: "Total price (incl VAT)" }, + { b: (str) => {str} } + )} + + {summaryPriceText} +
    +
    +
    + + {formatPrice( + intl, + totalPriceToShow.local.price, + totalPriceToShow.local.currency + )} + + {totalPriceToShow.requested ? ( + + {intl.formatMessage( + { id: "Approx. {value}" }, + { + value: formatPrice( + intl, + totalPriceToShow.requested.price, + totalPriceToShow.requested.currency + ), + } + )} + + ) : null} +
    +
    + + {intl.formatMessage({ id: "Total price" })} + + + {formatPrice( + intl, + totalPriceToShow.local.price, + totalPriceToShow.local.currency + )} + + + {summaryPriceText} + +
    + +
    +
    +
    +
    + {showMemberDiscountBanner ? : null} + +
    +
    +
    + ) +} diff --git a/components/HotelReservation/SelectRate/RateSummary/rateSummary.module.css b/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/rateSummary.module.css similarity index 82% rename from components/HotelReservation/SelectRate/RateSummary/rateSummary.module.css rename to components/HotelReservation/SelectRate/RoomsContainer/RateSummary/rateSummary.module.css index cfff90da3..020c9af2e 100644 --- a/components/HotelReservation/SelectRate/RateSummary/rateSummary.module.css +++ b/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/rateSummary.module.css @@ -1,28 +1,29 @@ +@keyframes slideUp { + 0% { + bottom: -100%; + } + + 100% { + bottom: 0%; + } +} + .summary { - position: fixed; - z-index: 10; + align-items: center; + animation: slideUp 300ms ease forwards; + background-color: var(--Base-Surface-Primary-light-Normal); + border-top: 1px solid var(--Base-Border-Subtle); bottom: -100%; left: 0; + position: fixed; right: 0; - background-color: var(--Base-Surface-Primary-light-Normal); - align-items: center; - transition: bottom 300ms ease-in-out; + z-index: 10; } .content { - width: 100%; - max-width: var(--max-width-page); - margin: 0 auto; - flex-direction: column; - justify-content: space-between; - align-items: center; display: none; } -.summary[data-visible="true"] { - bottom: 0; -} - .summaryPriceContainer { display: flex; flex-direction: row; @@ -35,6 +36,7 @@ display: none; max-width: 264px; } + .summaryPrice { align-self: center; display: flex; @@ -74,34 +76,46 @@ @media (min-width: 1367px) { .summary { - padding: var(--Spacing-x3) 0 var(--Spacing-x5); border-top: 1px solid var(--Base-Border-Subtle); + padding: var(--Spacing-x3) 0 var(--Spacing-x5); } + .content { + align-items: center; display: flex; flex-direction: row; + justify-content: space-between; + margin: 0 auto; + max-width: var(--max-width-page); + width: 100%; } + .petInfo, .promoContainer, .summaryPriceTextDesktop { display: block; } + .summaryText { display: flex; gap: var(--Spacing-x2); } + .summaryPriceTextMobile { display: none; } + .summaryPrice, .continueButton { width: auto; } + .summaryPriceContainer { width: auto; padding: 0; align-items: center; } + .mobileSummary { display: none; } diff --git a/components/HotelReservation/SelectRate/RateSummary/utils.ts b/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils.ts similarity index 82% rename from components/HotelReservation/SelectRate/RateSummary/utils.ts rename to components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils.ts index 935e65abd..a19682d41 100644 --- a/components/HotelReservation/SelectRate/RateSummary/utils.ts +++ b/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils.ts @@ -14,15 +14,18 @@ export const calculateTotalPrice = ( (total, room) => { const priceToUse = isUserLoggedIn && room.member ? room.member : room.public - const isPetRoom = room.features.some( + const isPetRoom = room.features.find( (feature) => feature.code === RoomPackageCodeEnum.PET_ROOM ) - const petRoomPrice = - isPetRoom && petRoomPackage - ? isUserLoggedIn - ? Number(petRoomPackage.localPrice.totalPrice || 0) - : Number(petRoomPackage.requestedPrice.totalPrice || 0) - : 0 + + let petRoomPrice = 0 + if ( + petRoomPackage && + isPetRoom && + room.package === RoomPackageCodeEnum.PET_ROOM + ) { + petRoomPrice = Number(petRoomPackage.localPrice.totalPrice) + } return { local: { diff --git a/components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/SelectedRoomPanel/index.tsx b/components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/SelectedRoomPanel/index.tsx new file mode 100644 index 000000000..49e819c70 --- /dev/null +++ b/components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/SelectedRoomPanel/index.tsx @@ -0,0 +1,118 @@ +"use client" +import { useSession } from "next-auth/react" +import { useIntl } from "react-intl" + +import { useRatesStore } from "@/stores/select-rate" + +import { getRates } from "@/components/HotelReservation/SelectRate/utils" +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" +import { useRoomContext } from "@/contexts/Room" +import { isValidClientSession } from "@/utils/clientSession" + +import styles from "./selectedRoomPanel.module.css" + +import type { Room as SelectedRateRoom } from "@/types/components/hotelReservation/selectRate/selectRate" + +interface SelectedRoomPanelProps { + room: SelectedRateRoom +} + +export default function SelectedRoomPanel({ room }: SelectedRoomPanelProps) { + const intl = useIntl() + const { data: session } = useSession() + const isUserLoggedIn = isValidClientSession(session) + const { rateDefinitions, roomCategories } = useRatesStore((state) => ({ + rateDefinitions: state.roomsAvailability?.rateDefinitions, + roomCategories: state.roomCategories, + })) + const { + actions: { modifyRate }, + roomNr, + selectedRate, + } = useRoomContext() + + const images = roomCategories.find((roomCategory) => + roomCategory.roomTypes.some( + (roomType) => roomType.code === selectedRate?.roomTypeCode + ) + )?.images + + if (!rateDefinitions) { + return null + } + + const rates = getRates(rateDefinitions) + + const freeCancelation = intl.formatMessage({ id: "Free cancellation" }) + const nonRefundable = intl.formatMessage({ id: "Non-refundable" }) + const freeBooking = intl.formatMessage({ id: "Free rebooking" }) + const payLater = intl.formatMessage({ id: "Pay later" }) + const payNow = intl.formatMessage({ id: "Pay now" }) + + function getRateDetails(rateCode: string) { + const rate = Object.keys(rates).find((k) => + rates[k as keyof typeof rates].find((a) => a.rateCode === rateCode) + ) + + switch (rate) { + case "change": + return `${freeBooking}, ${payNow}` + case "flex": + return `${freeCancelation}, ${payLater}` + case "save": + default: + return `${nonRefundable}, ${payNow}` + } + } + + const rateCode = + isUserLoggedIn && selectedRate?.product.productType.member + ? selectedRate?.product.productType.member?.rateCode + : selectedRate?.product.productType.public.rateCode + + return ( +
    +
    + + {intl.formatMessage( + { id: "Room {roomIndex}" }, + { roomIndex: roomNr } + )} + + + {selectedRate?.roomType} + + + {rateCode ? getRateDetails(rateCode) : null} + + + {selectedRate?.product.productType.public.localPrice.pricePerNight}{" "} + {selectedRate?.product.productType.public.localPrice.currency}/ + {intl.formatMessage({ id: "night" })} + +
    +
    + {images?.[0]?.imageSizes?.tiny && ( +
    + {selectedRate?.roomType +
    + )} +
    + +
    +
    +
    + ) +} diff --git a/components/HotelReservation/SelectRate/SelectedRoomPanel/selectedRoomPanel.module.css b/components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/SelectedRoomPanel/selectedRoomPanel.module.css similarity index 90% rename from components/HotelReservation/SelectRate/SelectedRoomPanel/selectedRoomPanel.module.css rename to components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/SelectedRoomPanel/selectedRoomPanel.module.css index 04fae65bd..b59dd9f09 100644 --- a/components/HotelReservation/SelectRate/SelectedRoomPanel/selectedRoomPanel.module.css +++ b/components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/SelectedRoomPanel/selectedRoomPanel.module.css @@ -25,17 +25,23 @@ gap: var(--Spacing-x1); } +div.selectedRoomPanel p.subtitle { + padding-bottom: var(--Spacing-x1); +} + @media (max-width: 768px) { .imageContainer { width: 120px; height: 80px; } + .imageAndModifyButtonContainer { display: flex; flex-direction: column; align-items: flex-end; gap: var(--Spacing-x1); } + .modifyButtonContainer { position: relative; bottom: 0; diff --git a/components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/index.tsx b/components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/index.tsx new file mode 100644 index 000000000..9e62d9b9a --- /dev/null +++ b/components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/index.tsx @@ -0,0 +1,83 @@ +import { useEffect } from "react" +import { useIntl } from "react-intl" + +import { useRatesStore } from "@/stores/select-rate" + +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import { useRoomContext } from "@/contexts/Room" + +import SelectedRoomPanel from "./SelectedRoomPanel" +import { roomSelectionPanelVariants } from "./variants" + +import styles from "./multiRoomWrapper.module.css" + +export default function MultiRoomWrapper({ + children, + isMultiRoom, +}: React.PropsWithChildren<{ isMultiRoom: boolean }>) { + const intl = useIntl() + const activeRoom = useRatesStore((state) => state.activeRoom) + const { bookingRoom, isActiveRoom, roomNr, selectedRate } = useRoomContext() + + const onlyAdultsMsg = intl.formatMessage( + { id: "{adults} adults" }, + { adults: bookingRoom.adults } + ) + const adultsAndChildrenMsg = intl.formatMessage( + { id: "{adults} adults, {children} children" }, + { + adults: bookingRoom.adults, + children: bookingRoom.childrenInRoom?.length, + } + ) + + useEffect(() => { + requestAnimationFrame(() => { + const SCROLL_OFFSET = 100 + const roomElements = document.querySelectorAll(`.${styles.roomContainer}`) + + const selectedRoom = roomElements[activeRoom] + + if (selectedRoom) { + const elementPosition = selectedRoom.getBoundingClientRect().top + const offsetPosition = elementPosition + window.scrollY - SCROLL_OFFSET + + window.scrollTo({ + top: offsetPosition, + behavior: "smooth", + }) + } + }) + }, [activeRoom]) + + if (isMultiRoom) { + const classNames = roomSelectionPanelVariants({ + active: isActiveRoom, + selected: !!selectedRate && !isActiveRoom, + }) + return ( +
    + {selectedRate && !isActiveRoom ? null : ( + + {intl.formatMessage( + { id: "Room {roomIndex}" }, + { roomIndex: roomNr } + )} + ,{" "} + {bookingRoom.childrenInRoom?.length + ? adultsAndChildrenMsg + : onlyAdultsMsg} + + )} +
    +
    + +
    +
    {children}
    +
    +
    + ) + } + + return children +} diff --git a/components/HotelReservation/SelectRate/Rooms/rooms.module.css b/components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/multiRoomWrapper.module.css similarity index 68% rename from components/HotelReservation/SelectRate/Rooms/rooms.module.css rename to components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/multiRoomWrapper.module.css index 256231b99..d95314761 100644 --- a/components/HotelReservation/SelectRate/Rooms/rooms.module.css +++ b/components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/multiRoomWrapper.module.css @@ -1,27 +1,10 @@ -.content { - max-width: var(--max-width-page); - margin: 0 auto; - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); - padding: var(--Spacing-x2) 0; - overflow: hidden; -} - -.header { - display: flex; - flex-direction: row; - justify-content: space-between; - z-index: 1; -} - .roomContainer { - display: flex; - flex-direction: column; + background: var(--Base-Surface-Primary-light-Normal); border: 1px solid var(--Base-Border-Subtle); border-radius: var(--Corner-radius-Large); - padding: var(--Spacing-x3); - background: var(--Base-Surface-Primary-light-Normal); + display: flex; + flex-direction: column; + padding: var(--Spacing-x2); } .roomPanel, @@ -36,7 +19,7 @@ transform-origin: bottom; } -.roomPanel > * { +.roomPanel>* { overflow: hidden; } @@ -53,6 +36,7 @@ .roomSelectionPanelContainer.selected .roomSelectionPanel { display: none; } + .roomSelectionPanelContainer.active .roomSelectionPanel { grid-template-rows: 1fr; opacity: 1; @@ -60,14 +44,12 @@ padding-top: var(--Spacing-x1); } -.hotelAlert { - width: 100%; - margin: 0 auto; - padding: var(--Spacing-x-one-and-half); +div.roomContainer p.subtitle { + padding-bottom: var(--Spacing-x1); } @media (max-width: 768px) { .roomContainer { padding: var(--Spacing-x2); } -} +} \ No newline at end of file diff --git a/components/HotelReservation/SelectRate/Rooms/variants.ts b/components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/variants.ts similarity index 87% rename from components/HotelReservation/SelectRate/Rooms/variants.ts rename to components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/variants.ts index 7d539cc44..cc1e6e713 100644 --- a/components/HotelReservation/SelectRate/Rooms/variants.ts +++ b/components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/variants.ts @@ -1,6 +1,6 @@ import { cva } from "class-variance-authority" -import styles from "./rooms.module.css" +import styles from "./multiRoomWrapper.module.css" export const roomSelectionPanelVariants = cva( styles.roomSelectionPanelContainer, diff --git a/components/HotelReservation/SelectRate/RoomTypeList/FlexibilityOption/PriceList/index.tsx b/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/FlexibilityOption/PriceList/index.tsx similarity index 100% rename from components/HotelReservation/SelectRate/RoomTypeList/FlexibilityOption/PriceList/index.tsx rename to components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/FlexibilityOption/PriceList/index.tsx diff --git a/components/HotelReservation/SelectRate/RoomTypeList/FlexibilityOption/PriceList/priceList.module.css b/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/FlexibilityOption/PriceList/priceList.module.css similarity index 100% rename from components/HotelReservation/SelectRate/RoomTypeList/FlexibilityOption/PriceList/priceList.module.css rename to components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/FlexibilityOption/PriceList/priceList.module.css diff --git a/components/HotelReservation/SelectRate/RoomTypeList/FlexibilityOption/PriceList/utils.ts b/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/FlexibilityOption/PriceList/utils.ts similarity index 100% rename from components/HotelReservation/SelectRate/RoomTypeList/FlexibilityOption/PriceList/utils.ts rename to components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/FlexibilityOption/PriceList/utils.ts diff --git a/components/HotelReservation/SelectRate/RoomTypeList/FlexibilityOption/flexibilityOption.module.css b/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/FlexibilityOption/flexibilityOption.module.css similarity index 100% rename from components/HotelReservation/SelectRate/RoomTypeList/FlexibilityOption/flexibilityOption.module.css rename to components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/FlexibilityOption/flexibilityOption.module.css diff --git a/components/HotelReservation/SelectRate/RoomTypeList/FlexibilityOption/index.tsx b/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/FlexibilityOption/index.tsx similarity index 90% rename from components/HotelReservation/SelectRate/RoomTypeList/FlexibilityOption/index.tsx rename to components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/FlexibilityOption/index.tsx index 9bdb188e6..01f70d3d2 100644 --- a/components/HotelReservation/SelectRate/RoomTypeList/FlexibilityOption/index.tsx +++ b/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/FlexibilityOption/index.tsx @@ -1,5 +1,4 @@ "use client" - import { useIntl } from "react-intl" import { CheckIcon, InfoCircleIcon } from "@/components/Icons" @@ -8,6 +7,7 @@ import Button from "@/components/TempDesignSystem/Button" import Label from "@/components/TempDesignSystem/Form/Label" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" +import { useRoomContext } from "@/contexts/Room" import PriceTable from "./PriceList" @@ -16,16 +16,32 @@ import styles from "./flexibilityOption.module.css" import type { FlexibilityOptionProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption" export default function FlexibilityOption({ - handleSelect, + features, isSelected, isUserLoggedIn, paymentTerm, priceInformation, petRoomPackage, product, + roomType, + roomTypeCode, title, }: FlexibilityOptionProps) { const intl = useIntl() + const { + actions: { selectRate }, + } = useRoomContext() + + function handleSelect() { + if (product) { + selectRate({ + features, + product, + roomType, + roomTypeCode, + }) + } + } if (!product) { return ( @@ -48,18 +64,14 @@ export default function FlexibilityOption({ const { public: publicPrice, member: memberPrice } = product.productType - function handleOnSelect() { - handleSelect(publicPrice.rateCode, title, paymentTerm) - } - return (