From 527ab170b550ddfbb476a587267ed57ac75667ed Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Thu, 9 Oct 2025 11:34:58 +0000 Subject: [PATCH] fix(BOOK-405): Pushing to history when opening sidepeek to avoid navigating back inside the booking flow Approved-by: Chuma Mcphoy (We Ahead) --- .../ExpiringPointsSeeAllButton.tsx | 16 +- .../DestinationPage/TopImages/index.tsx | 36 +- .../HotelPage/PreviewImages/index.tsx | 27 +- .../Rooms/MultiRoom/RoomDetailsSidePeek.tsx | 21 +- .../Rooms/SingleRoom/RoomDetailsSidePeek.tsx | 23 +- .../SidePeeks/BookedRoomSidePeek/index.tsx | 460 ------------------ .../RoomDetails.tsx | 2 +- .../bookedRoomSidePeekContent.module.css} | 0 .../BookedRoomSidePeekContent/index.tsx | 457 +++++++++++++++++ .../components/HotelDetailsSidePeek/index.tsx | 19 +- .../components/RoomDetailsSidePeek/index.tsx | 18 +- packages/common/hooks/usePopStateHandler.ts | 32 ++ .../lib/components/ImageGallery/index.tsx | 15 +- .../lib/components/Lightbox/index.tsx | 44 +- .../SidePeek/SelfControlled/index.tsx | 88 ++-- 15 files changed, 674 insertions(+), 584 deletions(-) delete mode 100644 apps/scandic-web/components/SidePeeks/BookedRoomSidePeek/index.tsx rename apps/scandic-web/components/SidePeeks/{BookedRoomSidePeek => BookedRoomSidePeekContent}/RoomDetails.tsx (98%) rename apps/scandic-web/components/SidePeeks/{BookedRoomSidePeek/bookedRoomSidePeek.module.css => BookedRoomSidePeekContent/bookedRoomSidePeekContent.module.css} (100%) create mode 100644 apps/scandic-web/components/SidePeeks/BookedRoomSidePeekContent/index.tsx create mode 100644 packages/common/hooks/usePopStateHandler.ts diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Points/PointsToSpendCard/ExpiringPointsSeeAllButton.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Points/PointsToSpendCard/ExpiringPointsSeeAllButton.tsx index c51eed574..74204e9dc 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Points/PointsToSpendCard/ExpiringPointsSeeAllButton.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Points/PointsToSpendCard/ExpiringPointsSeeAllButton.tsx @@ -1,6 +1,6 @@ "use client" -import { DialogTrigger } from "react-aria-components" +import { useState } from "react" import { useIntl } from "react-intl" import { Button } from "@scandic-hotels/design-system/Button" @@ -22,15 +22,23 @@ export default function ExpiringPointsSeeAllButton({ expiryDate, }: ExpiringPointsSeeAllButtonProps) { const intl = useIntl() + const [isOpen, setIsOpen] = useState(false) return ( - - setIsOpen(false)} >
@@ -48,6 +56,6 @@ export default function ExpiringPointsSeeAllButton({ />
-
+ ) } diff --git a/apps/scandic-web/components/ContentType/DestinationPage/TopImages/index.tsx b/apps/scandic-web/components/ContentType/DestinationPage/TopImages/index.tsx index 369d3fd2c..735ba5163 100644 --- a/apps/scandic-web/components/ContentType/DestinationPage/TopImages/index.tsx +++ b/apps/scandic-web/components/ContentType/DestinationPage/TopImages/index.tsx @@ -3,9 +3,9 @@ import { useState } from "react" import { useIntl } from "react-intl" +import { Button } from "@scandic-hotels/design-system/Button" import Image from "@scandic-hotels/design-system/Image" import Lightbox from "@scandic-hotels/design-system/Lightbox" -import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton" import { mapImageVaultImagesToGalleryImages } from "@/utils/imageGallery" @@ -61,29 +61,29 @@ export default function TopImages({ images, destinationName }: TopImageProps) { {images.length > 1 && ( <> - {lightboxState.open ? ( - setLightboxState({ open: false, activeIndex: 0 })} - /> - ) : null} + setLightboxState({ open: false, activeIndex: 0 })} + isOpen={lightboxState.open} + /> )} diff --git a/apps/scandic-web/components/ContentType/HotelPage/PreviewImages/index.tsx b/apps/scandic-web/components/ContentType/HotelPage/PreviewImages/index.tsx index d33ec998c..da54dc985 100644 --- a/apps/scandic-web/components/ContentType/HotelPage/PreviewImages/index.tsx +++ b/apps/scandic-web/components/ContentType/HotelPage/PreviewImages/index.tsx @@ -71,21 +71,18 @@ export default function PreviewImages({ defaultMessage: "See all photos", })} - {lightboxState.isOpen ? ( - - setLightboxState({ activeIndex: 0, isOpen: false }) - } - /> - ) : null} + setLightboxState({ activeIndex: 0, isOpen: false })} + isOpen={lightboxState.isOpen} + /> )} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/MultiRoom/RoomDetailsSidePeek.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/MultiRoom/RoomDetailsSidePeek.tsx index 8e986bcb9..d8e120402 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/MultiRoom/RoomDetailsSidePeek.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/MultiRoom/RoomDetailsSidePeek.tsx @@ -1,9 +1,11 @@ "use client" -import { Button as ButtonRAC, DialogTrigger } from "react-aria-components" +import { useState } from "react" +import { Button as ButtonRAC } from "react-aria-components" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import SidePeekSelfControlled from "@scandic-hotels/design-system/SidePeekSelfControlled" -import BookedRoomSidePeek from "@/components/SidePeeks/BookedRoomSidePeek" +import BookedRoomSidePeekContent from "@/components/SidePeeks/BookedRoomSidePeekContent" import { trackOpenSidePeekEvent } from "@/utils/tracking" import styles from "./sidePeek.module.css" @@ -21,11 +23,14 @@ export default function RoomDetailsSidePeek({ booking, user, }: RoomDetailsSidePeekProps) { + const [isOpen, setIsOpen] = useState(false) + return ( - + <> { + setIsOpen(true) trackOpenSidePeekEvent({ name: SidePeekEnum.bookedRoomDetails, hotelId: booking.hotelId, @@ -35,7 +40,13 @@ export default function RoomDetailsSidePeek({ > - - + setIsOpen(false)} + > + + + ) } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/SingleRoom/RoomDetailsSidePeek.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/SingleRoom/RoomDetailsSidePeek.tsx index c8e529f09..9a53e0467 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/SingleRoom/RoomDetailsSidePeek.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/SingleRoom/RoomDetailsSidePeek.tsx @@ -1,14 +1,15 @@ "use client" -import { DialogTrigger } from "react-aria-components" +import { useState } from "react" import { useIntl } from "react-intl" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton" +import SidePeekSelfControlled from "@scandic-hotels/design-system/SidePeekSelfControlled" import { useMyStayStore } from "@/stores/my-stay" -import BookedRoomSidePeek from "@/components/SidePeeks/BookedRoomSidePeek" +import BookedRoomSidePeekContent from "@/components/SidePeeks/BookedRoomSidePeekContent" import { trackOpenSidePeekEvent } from "@/utils/tracking" import { SidePeekEnum } from "@/types/sidepeek" @@ -23,9 +24,10 @@ export default function RoomDetailsSidePeek({ }: RoomDetailsSidePeekProps) { const intl = useIntl() const bookedRoom = useMyStayStore((state) => state.bookedRoom) + const [isOpen, setIsOpen] = useState(false) return ( - + <> - - + setIsOpen(false)} + > + + + ) } diff --git a/apps/scandic-web/components/SidePeeks/BookedRoomSidePeek/index.tsx b/apps/scandic-web/components/SidePeeks/BookedRoomSidePeek/index.tsx deleted file mode 100644 index e2136f653..000000000 --- a/apps/scandic-web/components/SidePeeks/BookedRoomSidePeek/index.tsx +++ /dev/null @@ -1,460 +0,0 @@ -import { useIntl } from "react-intl" - -import { getRoomFeatureDescription } from "@scandic-hotels/booking-flow/utils/getRoomFeatureDescription" -import { sumPackages } from "@scandic-hotels/booking-flow/utils/SelectRate" -import { changeOrCancelDateFormat } from "@scandic-hotels/common/constants/dateFormats" -import { dt } from "@scandic-hotels/common/dt" -import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" -import Accordion from "@scandic-hotels/design-system/Accordion" -import AccordionItem from "@scandic-hotels/design-system/Accordion/AccordionItem" -import IconChip from "@scandic-hotels/design-system/IconChip" -import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon" -import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" -import ImageGallery from "@scandic-hotels/design-system/ImageGallery" -import SidePeekSelfControlled from "@scandic-hotels/design-system/SidePeekSelfControlled" -import { Typography } from "@scandic-hotels/design-system/Typography" -import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter" - -import GuestDetails from "@/components/HotelReservation/MyStay/GuestDetails" -import PriceType from "@/components/HotelReservation/MyStay/PriceType" -import { hasModifiableRate } from "@/components/HotelReservation/MyStay/utils" -import useLang from "@/hooks/useLang" -import { mapApiImagesToGalleryImages } from "@/utils/imageGallery" - -import RoomDetails from "./RoomDetails" - -import styles from "./bookedRoomSidePeek.module.css" - -import type { BedTypeSchema } from "@scandic-hotels/booking-flow/stores/enter-details/types" -import type { BreakfastPackage } from "@scandic-hotels/trpc/routers/hotels/schemas/packages" -import type { BookingConfirmationSchema } from "@scandic-hotels/trpc/types/bookingConfirmation" -import type { Child } from "@scandic-hotels/trpc/types/child" -import type { Room as HotelRoom } from "@scandic-hotels/trpc/types/hotel" -import type { Packages } from "@scandic-hotels/trpc/types/packages" - -import type { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay" -import type { SafeUser } from "@/types/user" - -type PartialHotelRoom = Pick< - HotelRoom, - "descriptions" | "images" | "name" | "roomFacilities" | "roomTypes" -> - -type Room = Pick< - BookingConfirmationSchema, - | "adults" - | "bookingCode" - | "cancellationNumber" - | "checkInDate" - | "cheques" - | "confirmationNumber" - | "refId" - | "currencyCode" - | "guest" - | "rateDefinition" - | "totalPoints" - | "totalPrice" - | "vouchers" -> & { - bedType: BedTypeSchema - breakfast: Omit | false | undefined - childrenInRoom: Child[] - isCancelled: boolean - packages: Packages | null - priceType: PriceTypeEnum - roomName: string - roomNumber: number - terms: string | null -} - -interface RoomDetailsSidePeekProps { - hotelRoom: PartialHotelRoom | null - room: Room - user: SafeUser -} - -export default function BookedRoomSidePeek({ - hotelRoom, - room, - user, -}: RoomDetailsSidePeekProps) { - const intl = useIntl() - const lang = useLang() - - const { - adults, - bedType, - bookingCode, - breakfast, - cancellationNumber, - checkInDate, - cheques, - childrenInRoom, - confirmationNumber, - refId, - currencyCode, - guest, - isCancelled, - roomName, - packages, - priceType, - rateDefinition, - roomNumber, - totalPoints, - terms, - totalPrice, - vouchers, - } = room - - let totalRoomPrice = totalPrice - // API returns negative values for totalPrice - // on voucher bookings (╯°□°)╯︵ ┻━┻ - if (vouchers && totalRoomPrice < 0) { - const pkgsSum = sumPackages(packages) - totalRoomPrice = pkgsSum.price - } - - const fromDate = dt(checkInDate).locale(lang) - - const galleryImages = hotelRoom - ? mapApiImagesToGalleryImages(hotelRoom.images) - : null - - const adultsMsg = intl.formatMessage( - { - defaultMessage: "{adults, plural, one {# adult} other {# adults}}", - }, - { - adults: adults, - } - ) - - const childrenMsg = intl.formatMessage( - { - defaultMessage: "{children, plural, one {# child} other {# children}}", - }, - { - children: childrenInRoom.length, - } - ) - - const adultsOnlyMsg = adultsMsg - const adultsAndChildrenMsg = [adultsMsg, childrenMsg].join(", ") - - const formattedTotalPrice = formatPrice(intl, totalPrice, currencyCode) - - let breakfastPrice = intl.formatMessage({ - defaultMessage: "No breakfast", - }) - if (rateDefinition.breakfastIncluded) { - breakfastPrice = intl.formatMessage({ - defaultMessage: "Included", - }) - } else if (breakfast) { - breakfastPrice = formatPrice( - intl, - breakfast.localPrice.totalPrice, - breakfast.localPrice.currency - ) - } - - const hotelRoomName = hotelRoom?.name || roomName - - return ( - -
-
- {isCancelled ? ( - - } - > - - - {intl.formatMessage({ - defaultMessage: "Cancelled", - })} - - - - ) : ( -
- - - {intl.formatMessage( - { - defaultMessage: "Room {roomIndex}", - }, - { roomIndex: roomNumber } - )} - - -
- )} -
- - {isCancelled ? ( - - {intl.formatMessage({ - defaultMessage: "Cancellation no", - })} - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {":"} - - ) : ( - - {intl.formatMessage({ - defaultMessage: "Booking number", - })} - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {":"} - - )} - - - {isCancelled ? ( - - {cancellationNumber} - - ) : ( - {confirmationNumber} - )} - -
-
-
-
- {galleryImages ? ( - - ) : null} -
-
-
- - - -

- {intl.formatMessage({ - defaultMessage: "Guests", - })} -

-
-
-
- -

- {childrenInRoom.length > 0 - ? adultsAndChildrenMsg - : adultsOnlyMsg} -

-
-
-
-
- - - -

- {intl.formatMessage({ - defaultMessage: "Terms", - })} -

-
-
-
- -

{terms}

-
-
-
- {hasModifiableRate(rateDefinition.cancellationRule) && ( -
- - - -

- {intl.formatMessage({ - defaultMessage: "Modify By", - })} -

-
-
-
- -

- {intl.formatMessage( - { - defaultMessage: "Until {time}, {date}", - }, - { - time: "18:00", - date: fromDate.format(changeOrCancelDateFormat[lang]), - } - )} -

-
-
-
- )} -
- - - -

- {intl.formatMessage({ - defaultMessage: "Breakfast", - })} -

-
-
-
- -

{breakfastPrice}

-
-
-
- {packages?.some((item) => - Object.values(RoomPackageCodeEnum).includes( - item.code as RoomPackageCodeEnum - ) - ) && ( -
- - - -

- {intl.formatMessage({ - defaultMessage: "Room classification", - })} -

-
-
-
- -

- {packages - ?.filter((item) => - Object.values(RoomPackageCodeEnum).includes( - item.code as RoomPackageCodeEnum - ) - ) - .map((item) => - getRoomFeatureDescription( - item.code, - item.description, - intl - ) - ) - .join(", ")} -

-
-
-
- )} -
- - - -

- {intl.formatMessage({ - defaultMessage: "Bed preference", - })} -

-
-
-
- -

{bedType?.description}

-
-
-
-
-
-
-
- -

- {intl.formatMessage({ - defaultMessage: "Room total", - })} -

-
- - -
-
-
- {bookingCode && ( - - } - > - {intl.formatMessage( - { - defaultMessage: "Booking code: {value}", - }, - { - value: bookingCode, - strong: (text) => ( - - {text} - - ), - } - )} - - - )} - - -
- {hotelRoom ? ( - - - - - - ) : null} -
-
- ) -} diff --git a/apps/scandic-web/components/SidePeeks/BookedRoomSidePeek/RoomDetails.tsx b/apps/scandic-web/components/SidePeeks/BookedRoomSidePeekContent/RoomDetails.tsx similarity index 98% rename from apps/scandic-web/components/SidePeeks/BookedRoomSidePeek/RoomDetails.tsx rename to apps/scandic-web/components/SidePeeks/BookedRoomSidePeekContent/RoomDetails.tsx index 7825c64fc..5333bba4f 100644 --- a/apps/scandic-web/components/SidePeeks/BookedRoomSidePeek/RoomDetails.tsx +++ b/apps/scandic-web/components/SidePeeks/BookedRoomSidePeekContent/RoomDetails.tsx @@ -8,7 +8,7 @@ import { Typography } from "@scandic-hotels/design-system/Typography" import { getBedIconName } from "@/components/utils" -import styles from "./bookedRoomSidePeek.module.css" +import styles from "./bookedRoomSidePeekContent.module.css" import type { RoomDetailsProps } from "@/types/components/sidePeeks/bookedRoomSidePeek" diff --git a/apps/scandic-web/components/SidePeeks/BookedRoomSidePeek/bookedRoomSidePeek.module.css b/apps/scandic-web/components/SidePeeks/BookedRoomSidePeekContent/bookedRoomSidePeekContent.module.css similarity index 100% rename from apps/scandic-web/components/SidePeeks/BookedRoomSidePeek/bookedRoomSidePeek.module.css rename to apps/scandic-web/components/SidePeeks/BookedRoomSidePeekContent/bookedRoomSidePeekContent.module.css diff --git a/apps/scandic-web/components/SidePeeks/BookedRoomSidePeekContent/index.tsx b/apps/scandic-web/components/SidePeeks/BookedRoomSidePeekContent/index.tsx new file mode 100644 index 000000000..8b8f71161 --- /dev/null +++ b/apps/scandic-web/components/SidePeeks/BookedRoomSidePeekContent/index.tsx @@ -0,0 +1,457 @@ +import { useIntl } from "react-intl" + +import { getRoomFeatureDescription } from "@scandic-hotels/booking-flow/utils/getRoomFeatureDescription" +import { sumPackages } from "@scandic-hotels/booking-flow/utils/SelectRate" +import { changeOrCancelDateFormat } from "@scandic-hotels/common/constants/dateFormats" +import { dt } from "@scandic-hotels/common/dt" +import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" +import Accordion from "@scandic-hotels/design-system/Accordion" +import AccordionItem from "@scandic-hotels/design-system/Accordion/AccordionItem" +import IconChip from "@scandic-hotels/design-system/IconChip" +import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon" +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import ImageGallery from "@scandic-hotels/design-system/ImageGallery" +import { Typography } from "@scandic-hotels/design-system/Typography" +import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter" + +import GuestDetails from "@/components/HotelReservation/MyStay/GuestDetails" +import PriceType from "@/components/HotelReservation/MyStay/PriceType" +import { hasModifiableRate } from "@/components/HotelReservation/MyStay/utils" +import useLang from "@/hooks/useLang" +import { mapApiImagesToGalleryImages } from "@/utils/imageGallery" + +import RoomDetails from "./RoomDetails" + +import styles from "./bookedRoomSidePeekContent.module.css" + +import type { BedTypeSchema } from "@scandic-hotels/booking-flow/stores/enter-details/types" +import type { BreakfastPackage } from "@scandic-hotels/trpc/routers/hotels/schemas/packages" +import type { BookingConfirmationSchema } from "@scandic-hotels/trpc/types/bookingConfirmation" +import type { Child } from "@scandic-hotels/trpc/types/child" +import type { Room as HotelRoom } from "@scandic-hotels/trpc/types/hotel" +import type { Packages } from "@scandic-hotels/trpc/types/packages" + +import type { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay" +import type { SafeUser } from "@/types/user" + +type PartialHotelRoom = Pick< + HotelRoom, + "descriptions" | "images" | "name" | "roomFacilities" | "roomTypes" +> + +type Room = Pick< + BookingConfirmationSchema, + | "adults" + | "bookingCode" + | "cancellationNumber" + | "checkInDate" + | "cheques" + | "confirmationNumber" + | "refId" + | "currencyCode" + | "guest" + | "rateDefinition" + | "totalPoints" + | "totalPrice" + | "vouchers" +> & { + bedType: BedTypeSchema + breakfast: Omit | false | undefined + childrenInRoom: Child[] + isCancelled: boolean + packages: Packages | null + priceType: PriceTypeEnum + room: PartialHotelRoom | null + roomName: string + roomNumber: number + terms: string | null +} + +interface BookedRoomSidepeekContentProps { + room: Room + user: SafeUser +} + +export default function BookedRoomSidePeekContent({ + room, + user, +}: BookedRoomSidepeekContentProps) { + const intl = useIntl() + const lang = useLang() + + const { + adults, + bedType, + bookingCode, + breakfast, + cancellationNumber, + checkInDate, + cheques, + childrenInRoom, + confirmationNumber, + refId, + currencyCode, + guest, + isCancelled, + roomName, + packages, + priceType, + rateDefinition, + room: hotelRoom, + roomNumber, + totalPoints, + terms, + totalPrice, + vouchers, + } = room + + let totalRoomPrice = totalPrice + // API returns negative values for totalPrice + // on voucher bookings (╯°□°)╯︵ ┻━┻ + if (vouchers && totalRoomPrice < 0) { + const pkgsSum = sumPackages(packages) + totalRoomPrice = pkgsSum.price + } + + const fromDate = dt(checkInDate).locale(lang) + + const galleryImages = hotelRoom + ? mapApiImagesToGalleryImages(hotelRoom.images) + : null + + const adultsMsg = intl.formatMessage( + { + defaultMessage: "{adults, plural, one {# adult} other {# adults}}", + }, + { + adults: adults, + } + ) + + const childrenMsg = intl.formatMessage( + { + defaultMessage: "{children, plural, one {# child} other {# children}}", + }, + { + children: childrenInRoom.length, + } + ) + + const adultsOnlyMsg = adultsMsg + const adultsAndChildrenMsg = [adultsMsg, childrenMsg].join(", ") + + const formattedTotalPrice = formatPrice(intl, totalPrice, currencyCode) + + let breakfastPrice = intl.formatMessage({ + defaultMessage: "No breakfast", + }) + if (rateDefinition.breakfastIncluded) { + breakfastPrice = intl.formatMessage({ + defaultMessage: "Included", + }) + } else if (breakfast) { + breakfastPrice = formatPrice( + intl, + breakfast.localPrice.totalPrice, + breakfast.localPrice.currency + ) + } + + const hotelRoomName = hotelRoom?.name || roomName + + return ( +
+
+ {isCancelled ? ( + + } + > + + + {intl.formatMessage({ + defaultMessage: "Cancelled", + })} + + + + ) : ( +
+ + + {intl.formatMessage( + { + defaultMessage: "Room {roomIndex}", + }, + { roomIndex: roomNumber } + )} + + +
+ )} +
+ + {isCancelled ? ( + + {intl.formatMessage({ + defaultMessage: "Cancellation no", + })} + {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} + {":"} + + ) : ( + + {intl.formatMessage({ + defaultMessage: "Booking number", + })} + {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} + {":"} + + )} + + + {isCancelled ? ( + + {cancellationNumber} + + ) : ( + {confirmationNumber} + )} + +
+
+
+
+ {galleryImages ? ( + + ) : null} +
+
+
+ + + +

+ {intl.formatMessage({ + defaultMessage: "Guests", + })} +

+
+
+
+ +

+ {childrenInRoom.length > 0 + ? adultsAndChildrenMsg + : adultsOnlyMsg} +

+
+
+
+
+ + + +

+ {intl.formatMessage({ + defaultMessage: "Terms", + })} +

+
+
+
+ +

{terms}

+
+
+
+ {hasModifiableRate(rateDefinition.cancellationRule) && ( +
+ + + +

+ {intl.formatMessage({ + defaultMessage: "Modify By", + })} +

+
+
+
+ +

+ {intl.formatMessage( + { + defaultMessage: "Until {time}, {date}", + }, + { + time: "18:00", + date: fromDate.format(changeOrCancelDateFormat[lang]), + } + )} +

+
+
+
+ )} +
+ + + +

+ {intl.formatMessage({ + defaultMessage: "Breakfast", + })} +

+
+
+
+ +

{breakfastPrice}

+
+
+
+ {packages?.some((item) => + Object.values(RoomPackageCodeEnum).includes( + item.code as RoomPackageCodeEnum + ) + ) && ( +
+ + + +

+ {intl.formatMessage({ + defaultMessage: "Room classification", + })} +

+
+
+
+ +

+ {packages + ?.filter((item) => + Object.values(RoomPackageCodeEnum).includes( + item.code as RoomPackageCodeEnum + ) + ) + .map((item) => + getRoomFeatureDescription( + item.code, + item.description, + intl + ) + ) + .join(", ")} +

+
+
+
+ )} +
+ + + +

+ {intl.formatMessage({ + defaultMessage: "Bed preference", + })} +

+
+
+
+ +

{bedType?.description}

+
+
+
+
+
+
+
+ +

+ {intl.formatMessage({ + defaultMessage: "Room total", + })} +

+
+ + +
+
+
+ {bookingCode && ( + + } + > + {intl.formatMessage( + { + defaultMessage: "Booking code: {value}", + }, + { + value: bookingCode, + strong: (text) => ( + + {text} + + ), + } + )} + + + )} + + +
+ {hotelRoom ? ( + + + + + + ) : null} +
+ ) +} diff --git a/packages/booking-flow/lib/components/HotelDetailsSidePeek/index.tsx b/packages/booking-flow/lib/components/HotelDetailsSidePeek/index.tsx index 72044c644..f3431b042 100644 --- a/packages/booking-flow/lib/components/HotelDetailsSidePeek/index.tsx +++ b/packages/booking-flow/lib/components/HotelDetailsSidePeek/index.tsx @@ -1,6 +1,6 @@ "use client" -import { DialogTrigger } from "react-aria-components" +import { type ReactNode, useState } from "react" import { Button } from "@scandic-hotels/design-system/Button" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" @@ -14,7 +14,6 @@ import type { Hotel, Restaurant, } from "@scandic-hotels/trpc/types/hotel" -import type { ReactNode } from "react" enum SidePeekEnum { hotelDetails = "hotel-detail-side-peek", @@ -59,19 +58,21 @@ export function HotelDetailsSidePeek({ buttonVariant, }: HotelDetailsSidePeekProps) { const buttonProps = buttonPropsMap[buttonVariant] + const [isOpen, setIsOpen] = useState(false) return ( - + <> - + setIsOpen(false)} + > - + ) } diff --git a/packages/booking-flow/lib/components/RoomDetailsSidePeek/index.tsx b/packages/booking-flow/lib/components/RoomDetailsSidePeek/index.tsx index db98201a9..8d54f8a88 100644 --- a/packages/booking-flow/lib/components/RoomDetailsSidePeek/index.tsx +++ b/packages/booking-flow/lib/components/RoomDetailsSidePeek/index.tsx @@ -1,6 +1,6 @@ "use client" -import { DialogTrigger } from "react-aria-components" +import { useState } from "react" import { Button } from "@scandic-hotels/design-system/Button" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" @@ -54,28 +54,34 @@ export function RoomDetailsSidePeek({ buttonVariant: variant = "primary", }: RoomDetailsSidePeekProps) { const buttonProps = buttonPropsMap[variant] + const [isOpen, setIsOpen] = useState(false) return ( - + <> - + setIsOpen(false)} + > - + ) } diff --git a/packages/common/hooks/usePopStateHandler.ts b/packages/common/hooks/usePopStateHandler.ts new file mode 100644 index 000000000..f7b07198a --- /dev/null +++ b/packages/common/hooks/usePopStateHandler.ts @@ -0,0 +1,32 @@ +"use client" + +import { useEffect, useRef } from "react" + +const callbacks = new Set<() => void>() + +if (typeof window !== "undefined") { + window.addEventListener("popstate", () => { + callbacks.forEach((callback) => callback()) + }) +} + +export default function usePopStateHandler( + callback: () => void, + enabled = true +) { + const callbackRef = useRef(callback) + callbackRef.current = callback + + useEffect(() => { + if (!enabled) { + return + } + + const handler = () => callbackRef.current() + callbacks.add(handler) + + return () => { + callbacks.delete(handler) + } + }, [enabled]) +} diff --git a/packages/design-system/lib/components/ImageGallery/index.tsx b/packages/design-system/lib/components/ImageGallery/index.tsx index ce91c5138..f76ab577a 100644 --- a/packages/design-system/lib/components/ImageGallery/index.tsx +++ b/packages/design-system/lib/components/ImageGallery/index.tsx @@ -85,14 +85,13 @@ function ImageGallery({ })} /> - {isOpen ? ( - setIsOpen(false)} - hideLabel={hideLabel} - /> - ) : null} + setIsOpen(false)} + isOpen={isOpen} + hideLabel={hideLabel} + /> ) } diff --git a/packages/design-system/lib/components/Lightbox/index.tsx b/packages/design-system/lib/components/Lightbox/index.tsx index b84c82c7f..fa095a823 100644 --- a/packages/design-system/lib/components/Lightbox/index.tsx +++ b/packages/design-system/lib/components/Lightbox/index.tsx @@ -3,6 +3,8 @@ import { AnimatePresence, motion } from 'motion/react' import { useEffect, useState } from 'react' import { Dialog, Modal, ModalOverlay } from 'react-aria-components' +import usePopStateHandler from '@scandic-hotels/common/hooks/usePopStateHandler' + import FullView from './FullView' import Gallery from './Gallery' @@ -18,6 +20,7 @@ type LightboxProps = { images: LightboxImage[] dialogTitle: string /* Accessible title for dialog screen readers */ onClose: () => void + isOpen: boolean activeIndex?: number hideLabel?: boolean } @@ -26,21 +29,34 @@ export default function Lightbox({ images, dialogTitle, onClose, + isOpen, activeIndex = 0, hideLabel, }: LightboxProps) { const [selectedImageIndex, setSelectedImageIndex] = useState(activeIndex) const [isFullView, setIsFullView] = useState(false) + function handleClose(moveBack = false) { + setSelectedImageIndex(0) + if (moveBack) { + window.history.back() + } else { + onClose() + } + } + + usePopStateHandler(() => handleClose(), isOpen) + + useEffect(() => { + if (isOpen) { + window.history.pushState(null, '', window.location.href) + } + }, [isOpen]) + useEffect(() => { setSelectedImageIndex(activeIndex) }, [activeIndex]) - function handleClose() { - setSelectedImageIndex(0) - onClose() - } - function handleNext() { setSelectedImageIndex((prevIndex) => (prevIndex + 1) % images.length) } @@ -51,24 +67,10 @@ export default function Lightbox({ ) } - useEffect(() => { - function handlePopState() { - handleClose() - } - - window.history.pushState(null, '', window.location.href) - window.addEventListener('popstate', handlePopState) - - return () => { - window.removeEventListener('popstate', handlePopState) - } - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, []) - return ( handleClose(true)} className={styles.overlay} isDismissable > diff --git a/packages/design-system/lib/components/SidePeek/SelfControlled/index.tsx b/packages/design-system/lib/components/SidePeek/SelfControlled/index.tsx index 57335c095..b4d83581f 100644 --- a/packages/design-system/lib/components/SidePeek/SelfControlled/index.tsx +++ b/packages/design-system/lib/components/SidePeek/SelfControlled/index.tsx @@ -4,6 +4,7 @@ import { useEffect } from 'react' import { Dialog, Modal, ModalOverlay } from 'react-aria-components' import { useIntl } from 'react-intl' +import usePopStateHandler from '@scandic-hotels/common/hooks/usePopStateHandler' import useSetOverflowVisibleOnRA from '@scandic-hotels/common/hooks/useSetOverflowVisibleOnRA' import { IconButton } from '../../IconButton' @@ -14,45 +15,72 @@ import SidePeekSEO from './SidePeekSEO' import styles from './sidePeekSelfControlled.module.css' -import type { SidePeekSelfControlledProps } from './sidePeek' +interface SidePeekSelfControlledProps extends React.PropsWithChildren { + title: string + isOpen: boolean + onClose: () => void +} export default function SidePeekSelfControlled({ children, + isOpen, + onClose, title, -}: React.PropsWithChildren) { +}: SidePeekSelfControlledProps) { const intl = useIntl() + + function handleClose(moveBack = false) { + if (moveBack) { + window.history.back() + } else { + onClose() + } + } + + // Only register popstate handler when open + usePopStateHandler(() => handleClose(), isOpen) + + useEffect(() => { + if (isOpen) { + window.history.pushState(null, '', window.location.href) + } + }, [isOpen]) + return ( <> - + handleClose(true)} + isOpen={isOpen} + > - {({ close }) => ( - - )} +