diff --git a/.env.test b/.env.test index d2d1037ac..ce2b20278 100644 --- a/.env.test +++ b/.env.test @@ -42,3 +42,4 @@ GOOGLE_STATIC_MAP_SIGNATURE_SECRET="test" GOOGLE_STATIC_MAP_ID="test" GOOGLE_DYNAMIC_MAP_ID="test" HIDE_FOR_NEXT_RELEASE="true" +SALESFORCE_PREFERENCE_BASE_URL="test" diff --git a/app/[lang]/(live)/(public)/hotelreservation/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/layout.module.css index aaf8d1c3a..17d0bc32d 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/layout.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/layout.module.css @@ -1,6 +1,6 @@ .layout { min-height: 100dvh; + background-color: var(--Base-Background-Primary-Normal); max-width: var(--max-width); margin: 0 auto; - background-color: var(--Base-Background-Primary-Normal); } diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.module.css index 3266c418d..8edfdc8f0 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.module.css @@ -7,13 +7,13 @@ } .content { - max-width: 1134px; - margin-top: var(--Spacing-x5); + max-width: 1434px; margin-left: auto; margin-right: auto; display: flex; - justify-content: space-between; + flex-direction: column; gap: var(--Spacing-x7); + padding: var(--Spacing-x2); } .main { diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx index 796c0fedb..6c3a45365 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx @@ -15,8 +15,11 @@ export default async function SelectRatePage({ }: PageArgs) { setLang(params.lang) - // TODO: Use real endpoint. - const hotel = tempHotelData.data.attributes + const hotelData = await serverClient().hotel.hotelData.get({ + hotelId: searchParams.hotel, + language: params.lang, + include: ["RoomCategories"], + }) const roomConfigurations = await serverClient().hotel.availability.rooms({ hotelId: parseInt(searchParams.hotel, 10), @@ -24,18 +27,27 @@ export default async function SelectRatePage({ roomStayEndDate: "2024-11-03", adults: 1, }) + if (!roomConfigurations) { - return "No rooms found" + return "No rooms found" // TODO: Add a proper error message } + if (!hotelData) { + return "No hotel data found" // TODO: Add a proper error message + } + + const roomCategories = hotelData?.included + return (
- {/* TODO: Add Hotel Listing Card */} -
Hotel Listing Card TBI
-
+ {/* TODO: Add Hotel Listing Card */} +
Hotel Listing Card TBI
- +
diff --git a/components/ContentType/HotelPage/Rooms/index.tsx b/components/ContentType/HotelPage/Rooms/index.tsx index 62f0f448d..dcad34f41 100644 --- a/components/ContentType/HotelPage/Rooms/index.tsx +++ b/components/ContentType/HotelPage/Rooms/index.tsx @@ -23,20 +23,20 @@ export function Rooms({ rooms }: RoomsProps) { const mappedRooms = rooms .map((room) => { - const size = `${room.attributes.roomSize.min} - ${room.attributes.roomSize.max} m²` + const size = `${room.roomSize.min} - ${room.roomSize.max} m²` const personLabel = - room.attributes.occupancy.total === 1 + room.occupancy.total === 1 ? intl.formatMessage({ id: "hotelPages.rooms.roomCard.person" }) : intl.formatMessage({ id: "hotelPages.rooms.roomCard.persons" }) - const subtitle = `${size} (${room.attributes.occupancy.total} ${personLabel})` + const subtitle = `${size} (${room.occupancy.total} ${personLabel})` return { id: room.id, - images: room.attributes.content.images, - title: room.attributes.name, + images: room.images, + title: room.name, subtitle: subtitle, - sortOrder: room.attributes.sortOrder, + sortOrder: room.sortOrder, popularChoice: null, } }) diff --git a/components/Forms/BookingWidget/FormContent/Voucher/voucher.module.css b/components/Forms/BookingWidget/FormContent/Voucher/voucher.module.css index 83f02c14b..0deccbb43 100644 --- a/components/Forms/BookingWidget/FormContent/Voucher/voucher.module.css +++ b/components/Forms/BookingWidget/FormContent/Voucher/voucher.module.css @@ -32,6 +32,10 @@ display: none; } +.infoIcon { + stroke: var(--Base-Text-Disabled); +} + @media screen and (min-width: 768px) { .vouchers { display: none; diff --git a/components/Forms/BookingWidget/FormContent/formContent.module.css b/components/Forms/BookingWidget/FormContent/formContent.module.css index dfffecc96..a58bb1062 100644 --- a/components/Forms/BookingWidget/FormContent/formContent.module.css +++ b/components/Forms/BookingWidget/FormContent/formContent.module.css @@ -1,7 +1,3 @@ -.infoIcon { - stroke: var(--Base-Text-Disabled); -} - .vouchersHeader { display: flex; gap: var(--Spacing-x-one-and-half); diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/index.tsx new file mode 100644 index 000000000..62979d2c7 --- /dev/null +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/index.tsx @@ -0,0 +1,36 @@ +import { Button, Dialog, OverlayArrow, Popover } from "react-aria-components" + +import { CloseIcon } from "@/components/Icons" + +import styles from "./popover.module.css" + +import { PricePopoverProps } from "@/types/components/hotelReservation/selectRate/pricePopover" + +export default function PricePopover({ + children, + ...props +}: PricePopoverProps) { + return ( + + + + + + + + + {children} + + + ) +} diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/popover.module.css b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/popover.module.css new file mode 100644 index 000000000..bb60ba100 --- /dev/null +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/popover.module.css @@ -0,0 +1,12 @@ +.arrow { + top: -6px; +} + +.closeButton { + position: absolute; + top: 5px; + right: 5px; + background: none; + border: none; + cursor: pointer; +} diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx new file mode 100644 index 000000000..952d8add3 --- /dev/null +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx @@ -0,0 +1,102 @@ +import { useIntl } from "react-intl" + +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" + +import styles from "./priceList.module.css" + +import { PriceListProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption" + +export default function PriceList({ + publicPrice = {}, + memberPrice = {}, +}: PriceListProps) { + const intl = useIntl() + + const { localPrice: publicLocalPrice, requestedPrice: publicRequestedPrice } = + publicPrice + const { localPrice: memberLocalPrice, requestedPrice: memberRequestedPrice } = + memberPrice + + const showRequestedPrice = publicRequestedPrice && memberRequestedPrice + + return ( +
+
+
+ + {intl.formatMessage({ id: "Standard price" })} + +
+
+ {publicLocalPrice ? ( +
+ + {publicLocalPrice.pricePerNight} + + + {publicLocalPrice.currency} + +
+ ) : ( + + {intl.formatMessage({ id: "n/a" })} + + )} +
+
+ +
+
+ + {intl.formatMessage({ id: "Member price" })} + +
+
+ {memberLocalPrice ? ( +
+ + {memberLocalPrice.pricePerNight} + + + {memberLocalPrice.currency} + +
+ ) : ( + + - {intl.formatMessage({ id: "Currency Code" })} + + )} +
+
+ +
+
+ + {intl.formatMessage({ id: "Approx." })} + +
+
+ {showRequestedPrice ? ( + + {publicRequestedPrice.pricePerNight}/ + {memberRequestedPrice.pricePerNight}{" "} + {publicRequestedPrice.currency} + + ) : ( + - / - EUR + )} +
+
+
+ ) +} diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/priceList.module.css b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/priceList.module.css new file mode 100644 index 000000000..7320cf1be --- /dev/null +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/priceList.module.css @@ -0,0 +1,14 @@ +.priceRow { + display: flex; + justify-content: space-between; + padding: var(--Spacing-x-quarter) 0; +} + +.priceTable { + margin: 0; +} + +.price { + display: flex; + gap: var(--Spacing-x-half); +} diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/flexibilityOption.module.css b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/flexibilityOption.module.css index cf6c7b165..6d7bc5daf 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/flexibilityOption.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/flexibilityOption.module.css @@ -1,15 +1,80 @@ -.card { - font-size: 14px; - border-radius: var(--Corner-radius-Medium); - border: 1px solid var(--Base-Border-Normal); +.card, +.disabledCard { + border-radius: var(--Corner-radius-Large); padding: var(--Spacing-x-one-and-half) var(--Spacing-x2); + background-color: var(--Base-Surface-Secondary-light-Normal); + position: relative; + display: flex; + flex-direction: column; + gap: var(--Spacing-x-half); } -input[type="radio"]:checked + .card { +.disabledCard { + opacity: 0.6; +} + +.disabledCard:hover { + cursor: not-allowed; +} + +.card:hover { + cursor: pointer; background-color: var(--Base-Surface-Primary-light-Hover-alt); } +.checkIcon { + display: none; +} +input[type="radio"]:checked + .card { + border: 1px solid var(--Primary-Dark-On-Surface-Divider); + background-color: var(--Base-Surface-Primary-light-Hover-alt); +} +input[type="radio"]:checked + .card .checkIcon { + display: block; + position: absolute; + top: -10px; + right: -10px; +} .header { display: flex; - justify-content: space-between; + gap: var(--Spacing-x-half); +} + +.header .infoIcon, +.header .infoIcon path { + stroke: var(--UI-Text-Medium-contrast); + fill: transparent; +} + +.button { + background: none; + border: none; + cursor: pointer; + grid-area: chevron; + height: 100%; + justify-self: flex-end; + padding: 0; +} + +.popover { + background-color: var(--Main-Grey-White); + + border-radius: var(--Corner-radius-Medium); + left: 0px; + max-height: 400px; + padding: var(--Spacing-x2); + top: calc(55px + var(--Spacing-x1)); + width: 100%; + box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1); +} + +.popover section:focus-visible { + outline: none; +} +.popover .popoverText { + margin-bottom: var(--Spacing-x-half); +} +.popover .popoverHeading { + margin-bottom: var(--Spacing-x1); + font-weight: 600; /* TODO: Remove when this is updated in Design system */ } diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx index cda09ded4..481b79e98 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx @@ -1,9 +1,13 @@ "use client" -import { useIntl } from "react-intl" +import { useState } from "react" +import { Button, DialogTrigger } from "react-aria-components" -import Body from "@/components/TempDesignSystem/Text/Body" +import { CheckCircleIcon, InfoCircleIcon } from "@/components/Icons" import Caption from "@/components/TempDesignSystem/Text/Caption" +import PricePopover from "./Popover" +import PriceTable from "./PriceList" + import styles from "./flexibilityOption.module.css" import { FlexibilityOptionProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption" @@ -12,59 +16,90 @@ export default function FlexibilityOption({ product, name, paymentTerm, + priceInformation, }: FlexibilityOptionProps) { - const intl = useIntl() + const [rootDiv, setRootDiv] = useState(undefined) + const [isPopoverOpen, setIsPopoverOpen] = useState(false) - if (!product) { - // TODO: Implement empty state when this rate can't be booked - return
TBI: Rate not available
+ function setRef(node: Element | null) { + if (node) { + setRootDiv(node) + } } - const { productType } = product - const { public: publicPrice, member: memberPrice } = productType - const { localPrice: publicLocalPrice, requestedPrice: publicRequestedPrice } = - publicPrice - const { localPrice: memberLocalPrice, requestedPrice: memberRequestedPrice } = - memberPrice + if (!product) { + return ( +
+
+ + {name} + ({paymentTerm}) +
+ +
+ ) + } + + const { public: publicPrice, member: memberPrice } = product.productType return (