diff --git a/.gitignore b/.gitignore
index 4663988fb..8a6f4e73e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,5 +42,8 @@ certificates
#vscode
.vscode/
+#cursor
+.cursorrules
+
# localfile with all the CSS variables exported from design system
variables.css
\ No newline at end of file
diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.module.css
deleted file mode 100644
index 464c8ce65..000000000
--- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.module.css
+++ /dev/null
@@ -1,24 +0,0 @@
-.page {
- min-height: 100dvh;
- padding-top: var(--Spacing-x6);
- padding-left: var(--Spacing-x2);
- padding-right: var(--Spacing-x2);
- background-color: var(--Scandic-Brand-Warm-White);
-}
-
-.content {
- max-width: var(--max-width);
- margin: 0 auto;
- display: flex;
- flex-direction: column;
- gap: var(--Spacing-x7);
- padding: var(--Spacing-x2);
-}
-
-.main {
- flex-grow: 1;
-}
-
-.summary {
- max-width: 340px;
-}
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 2d80356ac..b361aaa10 100644
--- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx
+++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx
@@ -1,14 +1,15 @@
+import { notFound } from "next/navigation"
+
import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
import { serverClient } from "@/lib/trpc/server"
import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard"
-import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection"
+import Rooms from "@/components/HotelReservation/SelectRate/Rooms"
import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { setLang } from "@/i18n/serverContext"
-import styles from "./page.module.css"
-
-import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
+import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
+import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import { LangParams, PageArgs } from "@/types/params"
export default async function SelectRatePage({
@@ -20,10 +21,15 @@ export default async function SelectRatePage({
const selectRoomParams = new URLSearchParams(searchParams)
const selectRoomParamsObject =
getHotelReservationQueryParams(selectRoomParams)
+
+ if (!selectRoomParamsObject.room) {
+ return notFound()
+ }
+
const adults = selectRoomParamsObject.room[0].adults // TODO: Handle multiple rooms
const children = selectRoomParamsObject.room[0].child?.length // TODO: Handle multiple rooms
- const [hotelData, roomConfigurations, user] = await Promise.all([
+ const [hotelData, roomsAvailability, packages, user] = await Promise.all([
serverClient().hotel.hotelData.get({
hotelId: searchParams.hotel,
language: params.lang,
@@ -36,10 +42,22 @@ export default async function SelectRatePage({
adults,
children,
}),
+ serverClient().hotel.packages.get({
+ hotelId: searchParams.hotel,
+ startDate: searchParams.fromDate,
+ endDate: searchParams.toDate,
+ adults: adults,
+ children: children,
+ packageCodes: [
+ RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
+ RoomPackageCodeEnum.PET_ROOM,
+ RoomPackageCodeEnum.ALLERGY_ROOM,
+ ],
+ }),
getProfileSafely(),
])
- if (!roomConfigurations) {
+ if (!roomsAvailability) {
return "No rooms found" // TODO: Add a proper error message
}
@@ -50,17 +68,14 @@ export default async function SelectRatePage({
const roomCategories = hotelData?.included
return (
-
+
+ >
)
}
diff --git a/components/BookingWidget/MobileToggleButton/index.tsx b/components/BookingWidget/MobileToggleButton/index.tsx
index 3bc438c41..a58bdd1b2 100644
--- a/components/BookingWidget/MobileToggleButton/index.tsx
+++ b/components/BookingWidget/MobileToggleButton/index.tsx
@@ -68,7 +68,14 @@ export default function MobileToggleButton({
{`${selectedFromDate} - ${selectedToDate} (${intl.formatMessage(
{ id: "booking.nights" },
{ totalNights: nights }
- )}) ${intl.formatMessage({ id: "booking.adults" }, { totalAdults })}, ${intl.formatMessage({ id: "booking.children" }, { totalChildren })}, ${intl.formatMessage({ id: "booking.rooms" }, { totalRooms })}`}
+ )}) ${intl.formatMessage({ id: "booking.adults" }, { totalAdults })}, ${
+ totalChildren > 0
+ ? intl.formatMessage(
+ { id: "booking.children" },
+ { totalChildren }
+ ) + ", "
+ : ""
+ }${intl.formatMessage({ id: "booking.rooms" }, { totalRooms })}`}
diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx
index 0da2b79e2..ba126c1f2 100644
--- a/components/HotelReservation/EnterDetails/Payment/index.tsx
+++ b/components/HotelReservation/EnterDetails/Payment/index.tsx
@@ -22,7 +22,7 @@ import { useEnterDetailsStore } from "@/stores/enter-details"
import LoadingSpinner from "@/components/LoadingSpinner"
import Button from "@/components/TempDesignSystem/Button"
-import Checkbox from "@/components/TempDesignSystem/Checkbox"
+import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
diff --git a/components/HotelReservation/SelectRate/RoomFilter/index.tsx b/components/HotelReservation/SelectRate/RoomFilter/index.tsx
new file mode 100644
index 000000000..eb7c442b3
--- /dev/null
+++ b/components/HotelReservation/SelectRate/RoomFilter/index.tsx
@@ -0,0 +1,124 @@
+"use client"
+
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useCallback, useEffect, useMemo } from "react"
+import { FormProvider, useForm } from "react-hook-form"
+import { useIntl } from "react-intl"
+import { z } from "zod"
+
+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 { getIconForFeatureCode } from "../utils"
+
+import styles from "./roomFilter.module.css"
+
+import {
+ type RoomFilterProps,
+ RoomPackageCodeEnum,
+} from "@/types/components/hotelReservation/selectRate/roomFilter"
+
+export default function RoomFilter({
+ numberOfRooms,
+ onFilter,
+ filterOptions,
+}: RoomFilterProps) {
+ const initialFilterValues = useMemo(
+ () =>
+ filterOptions.reduce(
+ (acc, option) => {
+ acc[option.code] = false
+ return acc
+ },
+ {} as Record
+ ),
+ [filterOptions]
+ )
+
+ const intl = useIntl()
+ const methods = useForm>({
+ defaultValues: initialFilterValues,
+ mode: "all",
+ reValidateMode: "onChange",
+ resolver: zodResolver(z.object({})),
+ })
+
+ const { watch, getValues, handleSubmit } = 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",
+ })
+
+ const submitFilter = useCallback(() => {
+ const data = getValues()
+ onFilter(data)
+ }, [onFilter, getValues])
+
+ useEffect(() => {
+ const subscription = watch(() => handleSubmit(submitFilter)())
+ return () => subscription.unsubscribe()
+ }, [handleSubmit, watch, submitFilter])
+
+ return (
+
+
+
+ {intl.formatMessage(
+ { id: "Room types available" },
+ { numberOfRooms }
+ )}
+
+
+
+
+
+ {intl.formatMessage({ id: "Filter" })}
+
+
+ {Object.entries(selectedFilters)
+ .filter(([_, value]) => value)
+ .map(([key]) => intl.formatMessage({ id: key }))
+ .join(", ")}
+
+
+
+ {intl.formatMessage(
+ { id: "Room types available" },
+ { numberOfRooms }
+ )}
+
+
+
+
+
+
+ )
+}
diff --git a/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css b/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css
new file mode 100644
index 000000000..9cce04e43
--- /dev/null
+++ b/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css
@@ -0,0 +1,43 @@
+.container {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.roomsFilter {
+ display: flex;
+ flex-direction: row;
+ gap: var(--Spacing-x1);
+ align-items: center;
+}
+
+.roomsFilter .infoIcon,
+.roomsFilter .infoIcon path {
+ stroke: var(--UI-Text-Medium-contrast);
+ fill: transparent;
+}
+.filterInfo {
+ display: flex;
+ flex-direction: row;
+ gap: var(--Spacing-x-half);
+ align-items: flex-end;
+}
+
+.infoDesktop {
+ display: none;
+}
+
+.infoMobile {
+ display: block;
+}
+
+@media (min-width: 768px) {
+ .infoDesktop {
+ display: block;
+ }
+
+ .infoMobile {
+ display: none;
+ }
+}
diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx
index 76f366adb..dc7ca20fc 100644
--- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx
+++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx
@@ -40,6 +40,9 @@ export default function PriceList({
{publicLocalPrice.currency}
+
+ /{intl.formatMessage({ id: "night" })}
+
) : (
@@ -64,6 +67,9 @@ export default function PriceList({
{memberLocalPrice.currency}
+
+ /{intl.formatMessage({ id: "night" })}
+
) : (
diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/priceList.module.css b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/priceList.module.css
index 7320cf1be..4f3431525 100644
--- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/priceList.module.css
+++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/priceList.module.css
@@ -12,3 +12,8 @@
display: flex;
gap: var(--Spacing-x-half);
}
+
+.perNight {
+ font-weight: 400;
+ font-size: var(--typography-Caption-Regular-fontSize);
+}
diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx
index a523305ae..a0a92bb66 100644
--- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx
+++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx
@@ -19,6 +19,7 @@ export default function FlexibilityOption({
priceInformation,
roomType,
roomTypeCode,
+ features,
handleSelectRate,
}: FlexibilityOptionProps) {
const [rootDiv, setRootDiv] = useState(undefined)
@@ -52,6 +53,7 @@ export default function FlexibilityOption({
priceName: name,
public: publicPrice,
member: memberPrice,
+ features,
}
handleSelectRate(rate)
}
diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx
index b929bfe76..b7ecc3c63 100644
--- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx
+++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx
@@ -1,29 +1,56 @@
+import { differenceInCalendarDays } from "date-fns"
import { useIntl } from "react-intl"
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 styles from "./rateSummary.module.css"
-import { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary"
+import type { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary"
+import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
export default function RateSummary({
rateSummary,
isUserLoggedIn,
+ packages,
+ roomsAvailability,
}: RateSummaryProps) {
const intl = useIntl()
+ const {
+ member,
+ public: publicRate,
+ features,
+ roomType,
+ priceName,
+ } = rateSummary
+ const priceToShow = isUserLoggedIn ? member : publicRate
- const priceToShow = isUserLoggedIn ? rateSummary.member : rateSummary.public
+ const isPetRoomSelected = features.some(
+ (feature) => feature.code === RoomPackageCodeEnum.PET_ROOM
+ )
+
+ const petRoomPackage = packages.find(
+ (pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
+ )
+
+ const petRoomPrice = petRoomPackage?.calculatedPrice ?? null
+ const petRoomCurrency = petRoomPackage?.currency ?? null
+
+ const checkInDate = new Date(roomsAvailability.checkInDate)
+ const checkOutDate = new Date(roomsAvailability.checkOutDate)
+ const nights = differenceInCalendarDays(checkOutDate, checkInDate)
return (
- {rateSummary.roomType}
- {rateSummary.priceName}
+ {roomType}
+ {priceName}
-
+
{priceToShow?.localPrice.pricePerStay}{" "}
{priceToShow?.localPrice.currency}
@@ -34,7 +61,49 @@ export default function RateSummary({
{priceToShow?.requestedPrice?.currency}