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/actions/editProfile.ts b/actions/editProfile.ts index 8f0791c48..acf5d1b12 100644 --- a/actions/editProfile.ts +++ b/actions/editProfile.ts @@ -148,7 +148,7 @@ export const editProfile = protectedServerActionProcedure ) } - const apiResponse = await api.patch(api.endpoints.v1.profile, { + const apiResponse = await api.patch(api.endpoints.v1.Profile.profile, { body, cache: "no-store", headers: { diff --git a/actions/registerUser.ts b/actions/registerUser.ts index e65fc357f..ecd2318b4 100644 --- a/actions/registerUser.ts +++ b/actions/registerUser.ts @@ -55,7 +55,7 @@ export const registerUser = serviceServerActionProcedure let apiResponse try { - apiResponse = await api.post(api.endpoints.v1.profile, { + apiResponse = await api.post(api.endpoints.v1.Profile.profile, { body: parsedPayload.data, headers: { Authorization: `Bearer ${ctx.serviceToken}`, diff --git a/actions/registerUserBookingFlow.ts b/actions/registerUserBookingFlow.ts index a0539d351..a34cad231 100644 --- a/actions/registerUserBookingFlow.ts +++ b/actions/registerUserBookingFlow.ts @@ -33,7 +33,7 @@ export const registerUserBookingFlow = serviceServerActionProcedure // TODO: Consume the API to register the user as soon as passwordless signup is enabled. // let apiResponse // try { - // apiResponse = await api.post(api.endpoints.v1.profile, { + // apiResponse = await api.post(api.endpoints.v1.Profile.profile, { // body: payload, // headers: { // Authorization: `Bearer ${ctx.serviceToken}`, diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx index 13512d701..73496fe4e 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx @@ -1,5 +1,3 @@ -import { env } from "@/env/server" - import Divider from "@/components/TempDesignSystem/Divider" import type { ProfileLayoutProps } from "@/types/components/myPages/myProfile/layout" @@ -17,7 +15,7 @@ export default function ProfileLayout({ {profile} {creditCards} - {env.HIDE_FOR_NEXT_RELEASE ? null : communication} + {communication} ) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx index ef7e818c4..c4a682da9 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx @@ -45,8 +45,8 @@ export default async function BookingConfirmationPage({ } ) - const fromDate = dt(booking.temp.fromDate).locale(params.lang) - const toDate = dt(booking.temp.toDate).locale(params.lang) + const fromDate = dt(booking.checkInDate).locale(params.lang) + const toDate = dt(booking.checkOutDate).locale(params.lang) const nights = intl.formatMessage( { id: "booking.nights" }, { @@ -77,7 +77,7 @@ export default async function BookingConfirmationPage({ textTransform="regular" type="h1" > - {booking.hotel.name} + {booking.hotel?.data.attributes.name} @@ -91,7 +91,7 @@ export default async function BookingConfirmationPage({ {intl.formatMessage( { id: "Reference #{bookingNr}" }, - { bookingNr: "A92320VV" } + { bookingNr: booking.confirmationNumber } )} @@ -183,11 +183,13 @@ export default async function BookingConfirmationPage({
- {booking.hotel.name} + {booking.hotel?.data.attributes.name} - {booking.hotel.email} - {booking.hotel.phoneNumber} + {booking.hotel?.data.attributes.contactInformation.email} + + + {booking.hotel?.data.attributes.contactInformation.phoneNumber}
@@ -219,7 +221,16 @@ export default async function BookingConfirmationPage({ {intl.formatMessage({ id: "Total cost" })} - {booking.temp.total} + + {" "} + {intl.formatMessage( + { id: "{amount} {currency}" }, + { + amount: intl.formatNumber(booking.totalPrice), + currency: booking.currencyCode, + } + )} + {`${intl.formatMessage({ id: "Approx." })} ${booking.temp.totalInEuro}`} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/[...paths]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/[...paths]/page.tsx new file mode 100644 index 000000000..03a82e5f5 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/[...paths]/page.tsx @@ -0,0 +1 @@ +export { default } from "../page" diff --git a/app/[lang]/webview/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/loading.tsx similarity index 65% rename from app/[lang]/webview/loading.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/loading.tsx index c739b6635..0fad268cc 100644 --- a/app/[lang]/webview/loading.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/loading.tsx @@ -1,5 +1,5 @@ import LoadingSpinner from "@/components/LoadingSpinner" -export default function Loading() { +export default function LoadingHotelHeader() { return } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx new file mode 100644 index 000000000..75101475a --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx @@ -0,0 +1,25 @@ +import { redirect } from "next/navigation" + +import { getHotelData } from "@/lib/trpc/memoizedRequests" + +import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader" + +import type { LangParams, PageArgs } from "@/types/params" + +export default async function HotelHeader({ + params, + searchParams, +}: PageArgs) { + const home = `/${params.lang}` + if (!searchParams.hotel) { + redirect(home) + } + const hotel = await getHotelData({ + hotelId: searchParams.hotel, + language: params.lang, + }) + if (!hotel?.data) { + redirect(home) + } + return +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/[...paths]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/[...paths]/page.tsx new file mode 100644 index 000000000..03a82e5f5 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/[...paths]/page.tsx @@ -0,0 +1 @@ +export { default } from "../page" diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/loading.tsx new file mode 100644 index 000000000..67515d4f5 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/loading.tsx @@ -0,0 +1,5 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function LoadingHotelSidePeek() { + return +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/page.tsx new file mode 100644 index 000000000..deca843c3 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/page.tsx @@ -0,0 +1,24 @@ +import { redirect } from "next/navigation" + +import { getHotelData } from "@/lib/trpc/memoizedRequests" + +import SidePeek from "@/components/HotelReservation/EnterDetails/SidePeek" + +import type { LangParams, PageArgs } from "@/types/params" + +export default async function HotelSidePeek({ + params, + searchParams, +}: PageArgs) { + if (!searchParams.hotel) { + redirect(`/${params.lang}`) + } + const hotel = await getHotelData({ + hotelId: searchParams.hotel, + language: params.lang, + }) + if (!hotel?.data) { + redirect(`/${params.lang}`) + } + return +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx new file mode 100644 index 000000000..ed36a8476 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx @@ -0,0 +1,70 @@ +import { + getProfileSafely, + getSelectedRoomAvailability, +} from "@/lib/trpc/memoizedRequests" + +import Summary from "@/components/HotelReservation/EnterDetails/Summary" +import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" + +import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" +import { LangParams, PageArgs, SearchParams } from "@/types/params" + +export default async function SummaryPage({ + searchParams, +}: PageArgs>) { + const selectRoomParams = new URLSearchParams(searchParams) + const { hotel, adults, children, roomTypeCode, rateCode, fromDate, toDate } = + getQueryParamsForEnterDetails(selectRoomParams) + + const availability = await getSelectedRoomAvailability({ + hotelId: parseInt(hotel), + adults, + children, + roomStayStartDate: fromDate, + roomStayEndDate: toDate, + rateCode, + roomTypeCode, + }) + const user = await getProfileSafely() + + if (!availability) { + console.error("No hotel or availability data", availability) + // TODO: handle this case + return null + } + + const prices = user + ? { + local: { + price: availability.memberRate?.localPrice.pricePerStay, + currency: availability.memberRate?.localPrice.currency, + }, + euro: { + price: availability.memberRate?.requestedPrice?.pricePerStay, + currency: availability.memberRate?.requestedPrice?.currency, + }, + } + : { + local: { + price: availability.publicRate?.localPrice.pricePerStay, + currency: availability.publicRate?.localPrice.currency, + }, + euro: { + price: availability.publicRate?.requestedPrice?.pricePerStay, + currency: availability.publicRate?.requestedPrice?.currency, + }, + } + + return ( + + ) +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.module.css index 4f337ccb2..296eea04d 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.module.css @@ -1,5 +1,4 @@ .layout { - min-height: 100dvh; background-color: var(--Scandic-Brand-Warm-White); } @@ -9,7 +8,6 @@ grid-template-columns: 1fr 340px; grid-template-rows: auto 1fr; margin: var(--Spacing-x5) auto 0; - padding-top: var(--Spacing-x6); /* simulates padding on viewport smaller than --max-width-navigation */ width: min( calc(100dvw - (var(--Spacing-x2) * 2)), @@ -17,8 +15,81 @@ ); } -.summary { - align-self: flex-start; +.summaryContainer { grid-column: 2 / 3; grid-row: 1/-1; } + +.summary { + background-color: var(--Main-Grey-White); + + border-color: var(--Primary-Light-On-Surface-Divider-subtle); + border-style: solid; + border-width: 1px; + border-radius: var(--Corner-radius-Large); + + z-index: 1; +} + +.hider { + display: none; +} + +.shadow { + display: none; +} + +@media screen and (min-width: 950px) { + .summaryContainer { + display: grid; + grid-template-rows: auto auto 1fr; + margin-top: calc(0px - var(--Spacing-x9)); + } + + .summary { + position: sticky; + top: calc( + var(--booking-widget-desktop-height) + + var(--booking-widget-desktop-height) + var(--Spacing-x-one-and-half) + ); + margin-top: calc(0px - var(--Spacing-x9)); + border-bottom: none; + border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; + } + + .hider { + display: block; + background-color: var(--Scandic-Brand-Warm-White); + position: sticky; + margin-top: var(--Spacing-x4); + top: calc( + var(--booking-widget-desktop-height) + + var(--booking-widget-desktop-height) - 6px + ); + height: 40px; + } + + .shadow { + display: block; + background-color: var(--Main-Grey-White); + border-color: var(--Primary-Light-On-Surface-Divider-subtle); + border-style: solid; + border-left-width: 1px; + border-right-width: 1px; + border-top: none; + border-bottom: none; + } +} + +@media screen and (min-width: 1367px) { + .summary { + top: calc( + var(--booking-widget-desktop-height) + var(--Spacing-x2) + + var(--Spacing-x-half) + ); + } + + .hider { + top: calc(var(--booking-widget-desktop-height) - 6px); + } +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx index 0e8edd50e..c38349b26 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx @@ -1,54 +1,42 @@ -import { redirect } from "next/navigation" - -import { - getCreditCardsSafely, - getHotelData, - getProfileSafely, -} from "@/lib/trpc/memoizedRequests" - import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider" import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom" -import SidePeek from "@/components/HotelReservation/EnterDetails/SidePeek" -import Summary from "@/components/HotelReservation/EnterDetails/Summary" -import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader" import { setLang } from "@/i18n/serverContext" +import { preload } from "./page" + import styles from "./layout.module.css" -import { StepEnum } from "@/types/components/enterDetails/step" +import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step" import type { LangParams, LayoutArgs } from "@/types/params" -function preload(id: string, lang: string) { - void getHotelData(id, lang) - void getProfileSafely() - void getCreditCardsSafely() -} - export default async function StepLayout({ + summary, children, + hotelHeader, params, -}: React.PropsWithChildren>) { + sidePeek, +}: React.PropsWithChildren< + LayoutArgs & { + hotelHeader: React.ReactNode + sidePeek: React.ReactNode + summary: React.ReactNode + }>) { setLang(params.lang) - preload("811", params.lang) - - const hotel = await getHotelData("811", params.lang) - - if (!hotel?.data) { - redirect(`/${params.lang}`) - } - + preload() return ( - +
- + {hotelHeader}
{children} -
- + {sidePeek}
) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 8e8d3c891..9465bfd11 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -1,10 +1,13 @@ import { notFound } from "next/navigation" import { + getBreakfastPackages, getCreditCardsSafely, getHotelData, getProfileSafely, + getSelectedRoomAvailability, } from "@/lib/trpc/memoizedRequests" +import { HotelIncludeEnum } from "@/server/routers/hotels/input" import BedType from "@/components/HotelReservation/EnterDetails/BedType" import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast" @@ -12,66 +15,140 @@ import Details from "@/components/HotelReservation/EnterDetails/Details" import HistoryStateManager from "@/components/HotelReservation/EnterDetails/HistoryStateManager" import Payment from "@/components/HotelReservation/EnterDetails/Payment" import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion" +import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { getIntl } from "@/i18n" -import { StepEnum } from "@/types/components/enterDetails/step" +import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step" +import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import type { LangParams, PageArgs } from "@/types/params" +export function preload() { + void getProfileSafely() + void getCreditCardsSafely() +} + function isValidStep(step: string): step is StepEnum { return Object.values(StepEnum).includes(step as StepEnum) } export default async function StepPage({ params, -}: PageArgs) { - const { step, lang } = params + searchParams, +}: PageArgs) { + const { lang } = params const intl = await getIntl() + const selectRoomParams = new URLSearchParams(searchParams) + const { + hotel: hotelId, + adults, + children, + roomTypeCode, + rateCode, + fromDate, + toDate, + } = getQueryParamsForEnterDetails(selectRoomParams) - const hotel = await getHotelData("811", lang) + const breakfastInput = { adults, fromDate, hotelId, toDate } + void getBreakfastPackages(breakfastInput) + void getSelectedRoomAvailability({ + hotelId: parseInt(searchParams.hotel), + adults, + children, + roomStayStartDate: fromDate, + roomStayEndDate: toDate, + rateCode, + roomTypeCode, + }) + + const hotelData = await getHotelData({ + hotelId, + language: lang, + include: [HotelIncludeEnum.RoomCategories], + }) + const roomAvailability = await getSelectedRoomAvailability({ + hotelId: parseInt(searchParams.hotel), + adults, + children, + roomStayStartDate: fromDate, + roomStayEndDate: toDate, + rateCode, + roomTypeCode, + }) + const breakfastPackages = await getBreakfastPackages(breakfastInput) const user = await getProfileSafely() const savedCreditCards = await getCreditCardsSafely() - if (!isValidStep(step) || !hotel) { + if (!isValidStep(params.step) || !hotelData || !roomAvailability) { return notFound() } + const mustBeGuaranteed = roomAvailability?.mustBeGuaranteed ?? false + + const paymentGuarantee = intl.formatMessage({ + id: "Payment Guarantee", + }) + const payment = intl.formatMessage({ + id: "Payment", + }) + const guaranteeWithCard = intl.formatMessage({ + id: "Guarantee booking with credit card", + }) + const selectPaymentMethod = intl.formatMessage({ + id: "Select payment method", + }) + + const availableRoom = roomAvailability.selectedRoom?.roomType + const bedTypes = hotelData.included + ?.find((room) => room.name === availableRoom) + ?.roomTypes.map((room) => ({ + description: room.mainBed.description, + size: room.mainBed.widthRange, + value: room.code, + })) + return (
+ + {/* TODO: How to handle no beds found? */} + {bedTypes ? ( + + + + ) : null} + - - - - +
diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.module.css index 0969a7151..1730ffa68 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.module.css @@ -1,4 +1,3 @@ .layout { - min-height: 100dvh; background-color: var(--Base-Background-Primary-Normal); } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx index 1591e811b..5fe20e0d9 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -5,11 +5,12 @@ import { getLocations } from "@/lib/trpc/memoizedRequests" import { fetchAvailableHotels, + generateChildrenString, getFiltersFromHotels, } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils" import HotelCardListing from "@/components/HotelReservation/HotelCardListing" import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter" -import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" +import { getHotelReservationQueryParams } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { ChevronRightIcon } from "@/components/Icons" import StaticMap from "@/components/Maps/StaticMap" import Link from "@/components/TempDesignSystem/Link" @@ -42,7 +43,9 @@ export default async function SelectHotelPage({ const selectHotelParamsObject = getHotelReservationQueryParams(selectHotelParams) const adults = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms - const children = selectHotelParamsObject.room[0].child?.length // TODO: Handle multiple rooms + const children = selectHotelParamsObject.room[0].child + ? generateChildrenString(selectHotelParamsObject.room[0].child) + : undefined // TODO: Handle multiple rooms const hotels = await fetchAvailableHotels({ cityId: city.id, diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts index 8dfc76a1d..a6a48e12f 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts @@ -2,9 +2,11 @@ import { serverClient } from "@/lib/trpc/server" import { getLang } from "@/i18n/serverContext" +import { BedTypeEnum } from "@/types/components/bookingWidget/enums" import { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput" import { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" import { Filter } from "@/types/components/hotelReservation/selectHotel/hotelFilters" +import { Child } from "@/types/components/hotelReservation/selectRate/selectRate" export async function fetchAvailableHotels( input: AvailabilityInput @@ -41,3 +43,19 @@ export function getFiltersFromHotels(hotels: HotelData[]) { return filterList } + +const bedTypeMap: Record = { + [BedTypeEnum.IN_ADULTS_BED]: "ParentsBed", + [BedTypeEnum.IN_CRIB]: "Crib", + [BedTypeEnum.IN_EXTRA_BED]: "ExtraBed", +} + +export function generateChildrenString(children: Child[]): string { + return `[${children + ?.map((child) => { + const age = child.age + const bedType = bedTypeMap[+child.bed] + return `${age}:${bedType}` + }) + .join(",")}]` +} 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..502a5833c 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,18 @@ +import { notFound } from "next/navigation" + import { getProfileSafely } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" +import { HotelIncludeEnum } from "@/server/routers/hotels/input" import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard" -import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection" -import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" +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 { generateChildrenString } from "../select-hotel/utils" -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,14 +24,22 @@ export default async function SelectRatePage({ const selectRoomParams = new URLSearchParams(searchParams) const selectRoomParamsObject = getHotelReservationQueryParams(selectRoomParams) - 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([ + if (!selectRoomParamsObject.room) { + return notFound() + } + + const adults = selectRoomParamsObject.room[0].adults // TODO: Handle multiple rooms + const childrenCount = selectRoomParamsObject.room[0].child?.length + const children = selectRoomParamsObject.room[0].child + ? generateChildrenString(selectRoomParamsObject.room[0].child) + : undefined // TODO: Handle multiple rooms + + const [hotelData, roomsAvailability, packages, user] = await Promise.all([ serverClient().hotel.hotelData.get({ hotelId: searchParams.hotel, language: params.lang, - include: ["RoomCategories"], + include: [HotelIncludeEnum.RoomCategories], }), serverClient().hotel.availability.rooms({ hotelId: parseInt(searchParams.hotel, 10), @@ -36,10 +48,22 @@ export default async function SelectRatePage({ adults, children, }), + serverClient().hotel.packages.get({ + hotelId: searchParams.hotel, + startDate: searchParams.fromDate, + endDate: searchParams.toDate, + adults, + children: childrenCount, + 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 +74,14 @@ export default async function SelectRatePage({ const roomCategories = hotelData?.included return ( -
+ <> -
-
- -
-
-
+ + ) } diff --git a/app/[lang]/(live)/@bookingwidget/page.tsx b/app/[lang]/(live)/@bookingwidget/page.tsx index 16bc85731..6c944ae69 100644 --- a/app/[lang]/(live)/@bookingwidget/page.tsx +++ b/app/[lang]/(live)/@bookingwidget/page.tsx @@ -3,13 +3,11 @@ import { serverClient } from "@/lib/trpc/server" import BookingWidget, { preload } from "@/components/BookingWidget" -import { BookingWidgetSearchParams } from "@/types/components/bookingWidget" -import { LangParams, PageArgs } from "@/types/params" +import { PageArgs } from "@/types/params" export default async function BookingWidgetPage({ - params, searchParams, -}: PageArgs) { +}: PageArgs<{}, URLSearchParams>) { if (env.HIDE_FOR_NEXT_RELEASE) { return null } diff --git a/app/globals.css b/app/globals.css index 539f9827a..4a8b655c1 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,5 +1,5 @@ @font-face { - font-display: swap; + font-display: fallback; font-family: "biro script plus"; font-style: normal; font-weight: 400; @@ -7,7 +7,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "brandon text"; font-weight: 700; src: @@ -16,7 +16,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "brandon text"; font-weight: 900; src: @@ -25,7 +25,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira mono"; font-style: normal; font-weight: 400; @@ -33,7 +33,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira mono"; font-style: normal; font-weight: 500; @@ -41,7 +41,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira mono"; font-style: normal; font-weight: 700; @@ -49,7 +49,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira sans"; font-style: normal; font-weight: 300; @@ -57,7 +57,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira sans"; font-style: normal; font-weight: 400; @@ -65,7 +65,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira sans"; font-style: normal; font-weight: 500; @@ -73,7 +73,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira sans"; font-style: normal; font-weight: 600; @@ -81,7 +81,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira sans"; font-style: normal; font-weight: 700; @@ -89,7 +89,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira sans"; font-style: normal; font-weight: 900; diff --git a/components/Blocks/DynamicContent/Overview/Stats/ExpiringPoints/index.tsx b/components/Blocks/DynamicContent/Overview/Stats/ExpiringPoints/index.tsx index e64ddb4ef..fa21a8c83 100644 --- a/components/Blocks/DynamicContent/Overview/Stats/ExpiringPoints/index.tsx +++ b/components/Blocks/DynamicContent/Overview/Stats/ExpiringPoints/index.tsx @@ -4,6 +4,7 @@ import { dt } from "@/lib/dt" import Body from "@/components/TempDesignSystem/Text/Body" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" +import { formatNumber } from "@/utils/format" import { getMembership } from "@/utils/user" import type { UserProps } from "@/types/components/myPages/user" @@ -16,9 +17,6 @@ export default async function ExpiringPoints({ user }: UserProps) { // TODO: handle this case? return null } - - // sv hardcoded to force space on thousands - const formatter = new Intl.NumberFormat(Lang.sv) const d = dt(membership.pointsExpiryDate) const dateFormat = getLang() == Lang.fi ? "DD.MM.YYYY" : "YYYY-MM-DD" @@ -29,7 +27,7 @@ export default async function ExpiringPoints({ user }: UserProps) { {intl.formatMessage( { id: "spendable points expiring by" }, { - points: formatter.format(membership.pointsToExpire), + points: formatNumber(membership.pointsToExpire), date: d.format(dateFormat), } )} diff --git a/components/Blocks/DynamicContent/Points/EarnAndBurn/AwardPoints/index.tsx b/components/Blocks/DynamicContent/Points/EarnAndBurn/AwardPoints/index.tsx index 119324806..e32c3c5ca 100644 --- a/components/Blocks/DynamicContent/Points/EarnAndBurn/AwardPoints/index.tsx +++ b/components/Blocks/DynamicContent/Points/EarnAndBurn/AwardPoints/index.tsx @@ -1,8 +1,7 @@ import { useIntl } from "react-intl" -import { Lang } from "@/constants/languages" - import Body from "@/components/TempDesignSystem/Text/Body" +import { formatNumber } from "@/utils/format" import { awardPointsVariants } from "./awardPointsVariants" @@ -32,12 +31,10 @@ export default function AwardPoints({ variant, }) - // sv hardcoded to force space on thousands - const formatter = new Intl.NumberFormat(Lang.sv) return ( {isCalculated - ? formatter.format(awardPoints) + ? formatNumber(awardPoints) : intl.formatMessage({ id: "Points being calculated" })} ) diff --git a/components/BookingWidget/Client.tsx b/components/BookingWidget/Client.tsx index fda8683c9..46942ca44 100644 --- a/components/BookingWidget/Client.tsx +++ b/components/BookingWidget/Client.tsx @@ -1,16 +1,18 @@ "use client" import { zodResolver } from "@hookform/resolvers/zod" -import { useEffect, useState } from "react" +import { useEffect, useRef, useState } from "react" import { FormProvider, useForm } from "react-hook-form" import { dt } from "@/lib/dt" +import { StickyElementNameEnum } from "@/stores/sticky-position" import Form from "@/components/Forms/BookingWidget" import { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema" import { CloseLargeIcon } from "@/components/Icons" +import useStickyPosition from "@/hooks/useStickyPosition" import { debounce } from "@/utils/debounce" +import { getFormattedUrlQueryParams } from "@/utils/url" -import getHotelReservationQueryParams from "../HotelReservation/SelectRate/RoomSelection/utils" import MobileToggleButton from "./MobileToggleButton" import styles from "./bookingWidget.module.css" @@ -18,6 +20,7 @@ import styles from "./bookingWidget.module.css" import type { BookingWidgetClientProps, BookingWidgetSchema, + BookingWidgetSearchParams, } from "@/types/components/bookingWidget" import type { Location } from "@/types/trpc/routers/hotel/locations" @@ -27,6 +30,11 @@ export default function BookingWidgetClient({ searchParams, }: BookingWidgetClientProps) { const [isOpen, setIsOpen] = useState(false) + const bookingWidgetRef = useRef(null) + useStickyPosition({ + ref: bookingWidgetRef, + name: StickyElementNameEnum.BOOKING_WIDGET, + }) const sessionStorageSearchData = typeof window !== "undefined" @@ -36,12 +44,14 @@ export default function BookingWidgetClient({ ? JSON.parse(sessionStorageSearchData) : undefined - const bookingWidgetSearchParams = searchParams - ? new URLSearchParams(searchParams) - : undefined - const bookingWidgetSearchData = bookingWidgetSearchParams - ? getHotelReservationQueryParams(bookingWidgetSearchParams) - : undefined + const bookingWidgetSearchData: BookingWidgetSearchParams | undefined = + searchParams + ? (getFormattedUrlQueryParams(new URLSearchParams(searchParams), { + adults: "number", + age: "number", + bed: "number", + }) as BookingWidgetSearchParams) + : undefined const getLocationObj = (destination: string): Location | undefined => { if (destination) { @@ -66,9 +76,9 @@ export default function BookingWidgetClient({ const selectedLocation = bookingWidgetSearchData ? getLocationObj( - (bookingWidgetSearchData.city ?? - bookingWidgetSearchData.hotel) as string - ) + (bookingWidgetSearchData.city ?? + bookingWidgetSearchData.hotel) as string + ) : undefined const methods = useForm({ @@ -83,7 +93,7 @@ export default function BookingWidgetClient({ // UTC is required to handle requests from far away timezones https://scandichotels.atlassian.net/browse/SWAP-6375 & PET-507 // This is specifically to handle timezones falling in different dates. fromDate: isDateParamValid - ? bookingWidgetSearchData?.fromDate.toString() + ? bookingWidgetSearchData?.fromDate?.toString() : dt().utc().format("YYYY-MM-DD"), toDate: isDateParamValid ? bookingWidgetSearchData?.toDate?.toString() @@ -92,10 +102,10 @@ export default function BookingWidgetClient({ bookingCode: "", redemption: false, voucher: false, - rooms: [ + rooms: bookingWidgetSearchData?.room ?? [ { adults: 1, - children: [], + child: [], }, ], }, @@ -136,7 +146,10 @@ export default function BookingWidgetClient({ return ( -
+
+
+
+
-
- - {intl.formatMessage( - { id: "Things nearby HOTEL_NAME" }, - { hotelName } - )} - + +
+ + {intl.formatMessage( + { id: "Things nearby HOTEL_NAME" }, + { hotelName } + )} + - {poisInGroups.map(({ group, pois }) => - pois.length ? ( -
- -

- - {intl.formatMessage({ id: group })} -

- -
    - {pois.map((poi) => ( -
  • - -
  • - ))} -
-
- ) : null - )} -
- + {poisInGroups.map(({ group, pois }) => + pois.length ? ( +
+ +

+ + {intl.formatMessage({ id: group })} +

+ +
    + {pois.map((poi) => ( +
  • + +
  • + ))} +
+
+ ) : null + )} +
+ +
+ ) } diff --git a/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/sidebar.module.css b/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/sidebar.module.css index 11f26b822..4bab124e7 100644 --- a/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/sidebar.module.css +++ b/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/sidebar.module.css @@ -1,50 +1,12 @@ .sidebar { - --sidebar-max-width: 26.25rem; - --sidebar-mobile-toggle-height: 91px; - --sidebar-mobile-fullscreen-height: calc( - 100vh - var(--main-menu-mobile-height) - var(--sidebar-mobile-toggle-height) - ); - - position: absolute; - top: var(--sidebar-mobile-fullscreen-height); - height: 100%; - right: 0; - left: 0; background-color: var(--Base-Surface-Primary-light-Normal); - z-index: 1; - transition: top 0.3s; -} - -.sidebar:not(.fullscreen) { - border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; -} - -.sidebar.fullscreen { - top: 0; -} - -.sidebarToggle { - position: relative; - margin: var(--Spacing-x4) 0 var(--Spacing-x2); - width: 100%; -} - -.sidebarToggle::before { - content: ""; - position: absolute; - display: block; - top: -0.5rem; - width: 100px; - height: 3px; - background-color: var(--UI-Text-High-contrast); + z-index: 2; } .sidebarContent { display: grid; gap: var(--Spacing-x5); align-content: start; - padding: var(--Spacing-x3) var(--Spacing-x2); - height: var(--sidebar-mobile-fullscreen-height); overflow-y: auto; } @@ -90,12 +52,65 @@ background-color: var(--Base-Surface-Primary-light-Hover); } +@media screen and (max-width: 767px) { + .sidebar { + --sidebar-mobile-toggle-height: 84px; + --sidebar-mobile-top-space: 40px; + --sidebar-mobile-content-height: calc( + var(--hotel-map-height) - var(--sidebar-mobile-toggle-height) - + var(--sidebar-mobile-top-space) + ); + + position: absolute; + bottom: calc(-1 * var(--sidebar-mobile-content-height)); + width: 100%; + transition: + bottom 0.3s, + top 0.3s; + border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; + } + + .sidebar.fullscreen + .backdrop { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 100%; + background-color: rgba(0, 0, 0, 0.4); + z-index: 1; + } + + .sidebar.fullscreen { + bottom: 0; + } + + .sidebarToggle { + position: relative; + margin-top: var(--Spacing-x4); + } + + .sidebarToggle::before { + content: ""; + position: absolute; + display: block; + top: -0.5rem; + width: 100px; + height: 3px; + background-color: var(--UI-Text-High-contrast); + } + + .sidebarContent { + padding: var(--Spacing-x3) var(--Spacing-x2); + height: var(--sidebar-mobile-content-height); + } +} + @media screen and (min-width: 768px) { .sidebar { position: static; width: 40vw; min-width: 10rem; - max-width: var(--sidebar-max-width); + max-width: 26.25rem; background-color: var(--Base-Surface-Primary-light-Normal); } diff --git a/components/ContentType/HotelPage/Map/DynamicMap/dynamicMap.module.css b/components/ContentType/HotelPage/Map/DynamicMap/dynamicMap.module.css index 32df5b502..7cdd02371 100644 --- a/components/ContentType/HotelPage/Map/DynamicMap/dynamicMap.module.css +++ b/components/ContentType/HotelPage/Map/DynamicMap/dynamicMap.module.css @@ -1,18 +1,19 @@ .dynamicMap { - position: fixed; - top: var(--main-menu-mobile-height); - right: 0; - bottom: 0; + --hotel-map-height: 100dvh; + + position: absolute; + top: 0; left: 0; - z-index: var(--dialog-z-index); + height: var(--hotel-map-height); + width: 100dvw; + z-index: var(--hotel-dynamic-map-z-index); display: flex; background-color: var(--Base-Surface-Primary-light-Normal); } - -@media screen and (min-width: 768px) { - .dynamicMap { - top: var(--main-menu-desktop-height); - } +.wrapper { + position: absolute; + top: 0; + left: 0; } .closeButton { diff --git a/components/ContentType/HotelPage/Map/DynamicMap/index.tsx b/components/ContentType/HotelPage/Map/DynamicMap/index.tsx index f539d7614..969b24588 100644 --- a/components/ContentType/HotelPage/Map/DynamicMap/index.tsx +++ b/components/ContentType/HotelPage/Map/DynamicMap/index.tsx @@ -1,6 +1,6 @@ "use client" import { APIProvider } from "@vis.gl/react-google-maps" -import { useEffect, useRef, useState } from "react" +import { useCallback, useEffect, useRef, useState } from "react" import { Dialog, Modal } from "react-aria-components" import { useIntl } from "react-intl" @@ -10,6 +10,7 @@ import CloseLargeIcon from "@/components/Icons/CloseLarge" import InteractiveMap from "@/components/Maps/InteractiveMap" import Button from "@/components/TempDesignSystem/Button" import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" +import { debounce } from "@/utils/debounce" import Sidebar from "./Sidebar" @@ -25,9 +26,10 @@ export default function DynamicMap({ mapId, }: DynamicMapProps) { const intl = useIntl() + const rootDiv = useRef(null) + const [mapHeight, setMapHeight] = useState("0px") const { isDynamicMapOpen, closeDynamicMap } = useHotelPageStore() const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0) - const hasMounted = useRef(false) const [activePoi, setActivePoi] = useState(null) useHandleKeyUp((event: KeyboardEvent) => { @@ -36,23 +38,47 @@ export default function DynamicMap({ } }) - // Making sure the map is always opened at the top of the page, just below the header. + // Calculate the height of the map based on the viewport height from the start-point (below the header and booking widget) + const handleMapHeight = useCallback(() => { + const topPosition = rootDiv.current?.getBoundingClientRect().top ?? 0 + const scrollY = window.scrollY + setMapHeight(`calc(100dvh - ${topPosition + scrollY}px)`) + }, []) + + // Making sure the map is always opened at the top of the page, + // just below the header and booking widget as these should stay visible. // When closing, the page should scroll back to the position it was before opening the map. useEffect(() => { // Skip the first render - if (!hasMounted.current) { - hasMounted.current = true + if (!rootDiv.current) { return } if (isDynamicMapOpen && scrollHeightWhenOpened === 0) { - setScrollHeightWhenOpened(window.scrollY) + const scrollY = window.scrollY + setScrollHeightWhenOpened(scrollY) window.scrollTo({ top: 0, behavior: "instant" }) } else if (!isDynamicMapOpen && scrollHeightWhenOpened !== 0) { window.scrollTo({ top: scrollHeightWhenOpened, behavior: "instant" }) setScrollHeightWhenOpened(0) } - }, [isDynamicMapOpen, scrollHeightWhenOpened]) + }, [isDynamicMapOpen, scrollHeightWhenOpened, rootDiv]) + + useEffect(() => { + const debouncedResizeHandler = debounce(function () { + handleMapHeight() + }) + + const observer = new ResizeObserver(debouncedResizeHandler) + + observer.observe(document.documentElement) + + return () => { + if (observer) { + observer.unobserve(document.documentElement) + } + } + }, [rootDiv, isDynamicMapOpen, handleMapHeight]) const closeButton = (
) } diff --git a/components/ContentType/HotelPage/Map/MapWithCard/index.tsx b/components/ContentType/HotelPage/Map/MapWithCard/index.tsx new file mode 100644 index 000000000..b544e39d3 --- /dev/null +++ b/components/ContentType/HotelPage/Map/MapWithCard/index.tsx @@ -0,0 +1,24 @@ +"use client" + +import { PropsWithChildren, useRef } from "react" + +import { StickyElementNameEnum } from "@/stores/sticky-position" + +import useStickyPosition from "@/hooks/useStickyPosition" + +import styles from "./mapWithCard.module.css" + +export default function MapWithCard({ children }: PropsWithChildren) { + const mapWithCardRef = useRef(null) + useStickyPosition({ + ref: mapWithCardRef, + name: StickyElementNameEnum.HOTEL_STATIC_MAP, + group: "hotelPage", + }) + + return ( +
+ {children} +
+ ) +} diff --git a/components/ContentType/HotelPage/Map/MapWithCard/mapWithCard.module.css b/components/ContentType/HotelPage/Map/MapWithCard/mapWithCard.module.css new file mode 100644 index 000000000..528c5fa42 --- /dev/null +++ b/components/ContentType/HotelPage/Map/MapWithCard/mapWithCard.module.css @@ -0,0 +1,12 @@ +.mapWithCard { + position: sticky; + top: var(--booking-widget-desktop-height); + min-height: 500px; /* Fixed min to not cover the marker with the card */ + height: calc( + 100vh - var(--main-menu-desktop-height) - + var(--booking-widget-desktop-height) + ); /* Full height without the header + booking widget */ + max-height: 935px; /* Fixed max according to figma */ + overflow: hidden; + width: 100%; +} diff --git a/components/ContentType/HotelPage/Map/MobileMapToggle/mobileToggle.module.css b/components/ContentType/HotelPage/Map/MobileMapToggle/mobileToggle.module.css index e5bb3b75b..b2edd97a0 100644 --- a/components/ContentType/HotelPage/Map/MobileMapToggle/mobileToggle.module.css +++ b/components/ContentType/HotelPage/Map/MobileMapToggle/mobileToggle.module.css @@ -1,7 +1,7 @@ .mobileToggle { position: sticky; bottom: var(--Spacing-x5); - z-index: 1; + z-index: var(--hotel-mobile-map-toggle-button-z-index); margin: 0 auto; display: grid; grid-template-columns: repeat(2, 1fr); diff --git a/components/ContentType/HotelPage/PreviewImages/index.tsx b/components/ContentType/HotelPage/PreviewImages/index.tsx index 99704a61b..2a0074c5c 100644 --- a/components/ContentType/HotelPage/PreviewImages/index.tsx +++ b/components/ContentType/HotelPage/PreviewImages/index.tsx @@ -21,9 +21,9 @@ export default async function PreviewImages({ {images.slice(0, 3).map((image, index) => ( {image.alt} + ) diff --git a/components/ContentType/HotelPage/Rooms/index.tsx b/components/ContentType/HotelPage/Rooms/index.tsx index b7ac43a90..e976ee8af 100644 --- a/components/ContentType/HotelPage/Rooms/index.tsx +++ b/components/ContentType/HotelPage/Rooms/index.tsx @@ -22,27 +22,6 @@ export function Rooms({ rooms }: RoomsProps) { const scrollRef = useRef(null) - const mappedRooms = rooms - .map((room) => { - const size = `${room.roomSize.min} - ${room.roomSize.max} m²` - const personLabel = - room.occupancy.total === 1 - ? intl.formatMessage({ id: "hotelPages.rooms.roomCard.person" }) - : intl.formatMessage({ id: "hotelPages.rooms.roomCard.persons" }) - - const subtitle = `${size} (${room.occupancy.total} ${personLabel})` - - return { - id: room.id, - images: room.images, - title: room.name, - subtitle: subtitle, - sortOrder: room.sortOrder, - popularChoice: null, - } - }) - .sort((a, b) => a.sortOrder - b.sortOrder) - function handleShowMore() { if (scrollRef.current && allRoomsVisible) { scrollRef.current.scrollIntoView({ behavior: "smooth" }) @@ -64,15 +43,9 @@ export function Rooms({ rooms }: RoomsProps) { - {mappedRooms.map(({ id, images, title, subtitle, popularChoice }) => ( -
- + {rooms.map((room) => ( +
+
))} diff --git a/components/ContentType/HotelPage/TabNavigation/index.tsx b/components/ContentType/HotelPage/TabNavigation/index.tsx index 6f8b29130..b2699f461 100644 --- a/components/ContentType/HotelPage/TabNavigation/index.tsx +++ b/components/ContentType/HotelPage/TabNavigation/index.tsx @@ -1,12 +1,15 @@ "use client" import { useRouter } from "next/navigation" -import { useEffect } from "react" +import { useEffect, useRef } from "react" import { useIntl } from "react-intl" +import { StickyElementNameEnum } from "@/stores/sticky-position" + import Link from "@/components/TempDesignSystem/Link" import useHash from "@/hooks/useHash" import useScrollSpy from "@/hooks/useScrollSpy" +import useStickyPosition from "@/hooks/useStickyPosition" import styles from "./tabNavigation.module.css" @@ -23,6 +26,12 @@ export default function TabNavigation({ const hash = useHash() const intl = useIntl() const router = useRouter() + const tabNavigationRef = useRef(null) + useStickyPosition({ + ref: tabNavigationRef, + name: StickyElementNameEnum.HOTEL_TAB_NAVIGATION, + group: "hotelPage", + }) const tabLinks: { hash: HotelHashValues; text: string }[] = [ { @@ -71,7 +80,7 @@ export default function TabNavigation({ }, [activeSectionId, router]) return ( -
+