diff --git a/.env.test b/.env.test index f651cbe63..1d303b099 100644 --- a/.env.test +++ b/.env.test @@ -44,3 +44,5 @@ GOOGLE_DYNAMIC_MAP_ID="test" HIDE_FOR_NEXT_RELEASE="true" SALESFORCE_PREFERENCE_BASE_URL="test" USE_NEW_REWARDS_ENDPOINT="true" + +TZ=UTC diff --git a/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx b/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx index 6775fd188..048cf9e5f 100644 --- a/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx @@ -1,7 +1,7 @@ import { Suspense } from "react" import Breadcrumbs from "@/components/Breadcrumbs" -import BreadcrumbsSkeleton from "@/components/Breadcrumbs/BreadcrumbsSkeleton" +import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton" import { setLang } from "@/i18n/serverContext" import { LangParams, PageArgs } from "@/types/params" diff --git a/app/[lang]/(live)/(protected)/my-pages/[...path]/page.tsx b/app/[lang]/(live)/(protected)/my-pages/[...path]/page.tsx index 528b7b211..4a94f599c 100644 --- a/app/[lang]/(live)/(protected)/my-pages/[...path]/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/[...path]/page.tsx @@ -10,7 +10,7 @@ import styles from "./page.module.css" import type { LangParams, PageArgs } from "@/types/params" -export { generateMetadataAccountPage as generateMetadata } from "@/utils/generateMetadata" +export { generateMetadata } from "@/utils/generateMetadata" export default async function MyPages({ params, diff --git a/app/[lang]/(live)/(protected)/my-pages/layout.tsx b/app/[lang]/(live)/(protected)/my-pages/layout.tsx index 43268adca..80a038b9a 100644 --- a/app/[lang]/(live)/(protected)/my-pages/layout.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/layout.tsx @@ -2,8 +2,8 @@ import { Suspense } from "react" import LoadingSpinner from "@/components/LoadingSpinner" import Sidebar from "@/components/MyPages/Sidebar" +import Surprises from "@/components/MyPages/Surprises" -// import Surprises from "@/components/MyPages/Surprises" import styles from "./layout.module.css" export default async function MyPagesLayout({ @@ -24,9 +24,7 @@ export default async function MyPagesLayout({ - {/* TODO: Waiting on new API stuff - - */} + ) } diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/edit/page.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/edit/page.tsx index dd8c6eb91..c2d24fe62 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/edit/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/edit/page.tsx @@ -1,5 +1,5 @@ import ProfilePage from "../page" -export { generateMetadataAccountPage as generateMetadata } from "@/utils/generateMetadata" +export { generateMetadata } from "@/utils/generateMetadata" export default ProfilePage diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/page.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/page.tsx index ee8e3ad34..749065fc5 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/page.tsx @@ -7,7 +7,7 @@ import { setLang } from "@/i18n/serverContext" import { LangParams, PageArgs } from "@/types/params" -export { generateMetadataAccountPage as generateMetadata } from "@/utils/generateMetadata" +export { generateMetadata } from "@/utils/generateMetadata" export default async function ProfilePage({ params }: PageArgs) { setLang(params.lang) diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/page.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/page.tsx index ec8f14553..f8024a816 100644 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/page.tsx +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/page.tsx @@ -1,7 +1,7 @@ import { Suspense } from "react" import Breadcrumbs from "@/components/Breadcrumbs" -import BreadcrumbsSkeleton from "@/components/Breadcrumbs/BreadcrumbsSkeleton" +import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton" import { setLang } from "@/i18n/serverContext" import { LangParams, PageArgs } from "@/types/params" diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx index 215e3aec8..f81aa2edf 100644 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx @@ -3,6 +3,7 @@ import { notFound } from "next/navigation" import { isSignupPage } from "@/constants/routes/signup" import { env } from "@/env/server" +import { getHotelPage } from "@/lib/trpc/memoizedRequests" import HotelPage from "@/components/ContentType/HotelPage" import LoyaltyPage from "@/components/ContentType/LoyaltyPage" @@ -19,7 +20,7 @@ import { export { generateMetadata } from "@/utils/generateMetadata" -export default function ContentTypePage({ +export default async function ContentTypePage({ params, }: PageArgs) { setLang(params.lang) @@ -57,7 +58,12 @@ export default function ContentTypePage({ if (env.HIDE_FOR_NEXT_RELEASE) { return notFound() } - return + const hotelPageData = await getHotelPage() + return hotelPageData ? ( + + ) : ( + notFound() + ) default: const type: never = params.contentType console.error(`Unsupported content type given: ${type}`) 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 b81003b9d..23a66f340 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx @@ -1,6 +1,8 @@ import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests" -import BookingConfirmation from "@/components/HotelReservation/BookingConfirmation" +import Header from "@/components/HotelReservation/BookingConfirmation/Header" +import Rooms from "@/components/HotelReservation/BookingConfirmation/Rooms" +import Summary from "@/components/HotelReservation/BookingConfirmation/Summary" import { setLang } from "@/i18n/serverContext" import styles from "./page.module.css" @@ -12,11 +14,12 @@ export default async function BookingConfirmationPage({ searchParams, }: PageArgs) { setLang(params.lang) - const confirmationNumber = searchParams.confirmationNumber - void getBookingConfirmation(confirmationNumber) + void getBookingConfirmation(searchParams.confirmationNumber) return ( -
- -
+
+
+ + +
) } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/layout.module.css new file mode 100644 index 000000000..1730ffa68 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/layout.module.css @@ -0,0 +1,3 @@ +.layout { + background-color: var(--Base-Background-Primary-Normal); +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/layout.tsx new file mode 100644 index 000000000..b9ad3b13c --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/layout.tsx @@ -0,0 +1,16 @@ +import { notFound } from "next/navigation" + +import { env } from "@/env/server" + +import styles from "./layout.module.css" + +import { LangParams, LayoutArgs } from "@/types/params" + +export default function PaymentCallbackLayout({ + children, +}: React.PropsWithChildren>) { + if (env.HIDE_FOR_NEXT_RELEASE) { + return notFound() + } + return
{children}
+} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx new file mode 100644 index 000000000..0e4e716f2 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx @@ -0,0 +1,70 @@ +import { redirect } from "next/navigation" + +import { + BOOKING_CONFIRMATION_NUMBER, + PaymentErrorCodeEnum, +} from "@/constants/booking" +import { Lang } from "@/constants/languages" +import { + bookingConfirmation, + payment, +} from "@/constants/routes/hotelReservation" +import { serverClient } from "@/lib/trpc/server" + +import PaymentCallback from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback" + +import { LangParams, PageArgs } from "@/types/params" + +export default async function PaymentCallbackPage({ + params, + searchParams, +}: PageArgs< + LangParams, + { status: "error" | "success" | "cancel"; confirmationNumber?: string } +>) { + console.log(`[payment-callback] callback started`) + const lang = params.lang + const status = searchParams.status + const confirmationNumber = searchParams.confirmationNumber + + if (status === "success" && confirmationNumber) { + const confirmationUrl = `${bookingConfirmation(lang)}?${BOOKING_CONFIRMATION_NUMBER}=${confirmationNumber}` + + console.log(`[payment-callback] redirecting to: ${confirmationUrl}`) + redirect(confirmationUrl) + } + + const returnUrl = payment(lang) + const searchObject = new URLSearchParams() + + if (confirmationNumber) { + try { + const bookingStatus = await serverClient().booking.status({ + confirmationNumber, + }) + if (bookingStatus.metadata) { + searchObject.set( + "errorCode", + bookingStatus.metadata.errorCode?.toString() ?? "" + ) + } + } catch (error) { + console.error( + `[payment-callback] failed to get booking status for ${confirmationNumber}, status: ${status}` + ) + if (status === "cancel") { + searchObject.set("errorCode", PaymentErrorCodeEnum.Cancelled.toString()) + } + if (status === "error") { + searchObject.set("errorCode", PaymentErrorCodeEnum.Failed.toString()) + } + } + } + + return ( + + ) +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/[...paths]/loading.tsx similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/loading.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/[...paths]/loading.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/[...paths]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/[...paths]/page.tsx index 03a82e5f5..fee05b878 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/[...paths]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/[...paths]/page.tsx @@ -1 +1,21 @@ -export { default } from "../page" +import { getHotelData } from "@/lib/trpc/memoizedRequests" + +import SidePeek from "@/components/HotelReservation/SidePeek" + +import type { LangParams, PageArgs } from "@/types/params" + +export default async function HotelSidePeek({ + params, + searchParams, +}: PageArgs) { + if (!searchParams.hotel) { + return + } + + const hotel = await getHotelData({ + hotelId: searchParams.hotel, + language: params.lang, + }) + + return +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/page.tsx index a73eb305e..438f146ae 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/page.tsx @@ -1,25 +1,3 @@ -import { getHotelData } from "@/lib/trpc/memoizedRequests" - -import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" -import SidePeek from "@/components/HotelReservation/SidePeek" - -import type { LangParams, PageArgs } from "@/types/params" - -export default async function HotelSidePeek({ - params, - searchParams, -}: PageArgs) { - const search = new URLSearchParams(searchParams) - const { hotel: hotelId } = getQueryParamsForEnterDetails(search) - - if (!hotelId) { - return - } - - const hotel = await getHotelData({ - hotelId: hotelId, - language: params.lang, - }) - - return +export default function HotelSidePeekSlot() { + return null } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx index 7c5573536..ea19ffda1 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx @@ -1,7 +1,7 @@ import { notFound } from "next/navigation" import { env } from "@/env/server" -import { getLocations } from "@/lib/trpc/memoizedRequests" +import { getCityCoordinates, getLocations } from "@/lib/trpc/memoizedRequests" import { getHotelPins } from "@/components/HotelReservation/HotelCardDialogListing/utils" import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap" @@ -58,6 +58,10 @@ export default async function SelectHotelMapPage({ const hotelPins = getHotelPins(hotels) const filterList = getFiltersFromHotels(hotels) + const cityCoordinates = await getCityCoordinates({ + city: city.name, + hotel: { address: hotels[0].hotelData.address.streetAddress }, + }) return ( @@ -67,6 +71,7 @@ export default async function SelectHotelMapPage({ mapId={googleMapId} hotels={hotels} filterList={filterList} + cityCoordinates={cityCoordinates} /> ) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css index e42544196..cb7245753 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css @@ -14,6 +14,10 @@ padding: var(--Spacing-x3) var(--Spacing-x2) 0 var(--Spacing-x2); } +.header nav { + display: none; +} + .cityInformation { display: flex; flex-wrap: wrap; @@ -65,13 +69,19 @@ var(--Spacing-x5); } + .header nav { + display: block; + max-width: var(--max-width-navigation); + padding-left: 0; + } + .sorter { display: block; width: 339px; } .title { - margin: 0 auto; + margin: var(--Spacing-x3) auto 0; display: flex; max-width: var(--max-width-navigation); align-items: center; 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 62cab6b85..3954be688 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -1,6 +1,10 @@ import { notFound } from "next/navigation" +import { Suspense } from "react" -import { selectHotelMap } from "@/constants/routes/hotelReservation" +import { + selectHotel, + selectHotelMap, +} from "@/constants/routes/hotelReservation" import { getLocations } from "@/lib/trpc/memoizedRequests" import { @@ -19,6 +23,8 @@ import { import { ChevronRightIcon } from "@/components/Icons" import StaticMap from "@/components/Maps/StaticMap" import Alert from "@/components/TempDesignSystem/Alert" +import Breadcrumbs from "@/components/TempDesignSystem/Breadcrumbs" +import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton" import Button from "@/components/TempDesignSystem/Button" import Link from "@/components/TempDesignSystem/Link" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" @@ -45,8 +51,12 @@ export default async function SelectHotelPage({ (location) => location.name.toLowerCase() === searchParams.city.toLowerCase() ) + if (!city) return notFound() + const isCityWithCountry = (city: any): city is { country: string } => + "country" in city + const intl = await getIntl() const selectHotelParams = new URLSearchParams(searchParams) const selectHotelParamsObject = @@ -65,12 +75,36 @@ export default async function SelectHotelPage({ }) const filterList = getFiltersFromHotels(hotels) + const breadcrumbs = [ + { + title: intl.formatMessage({ id: "Home" }), + href: `/${params.lang}`, + uid: "home-page", + }, + { + title: intl.formatMessage({ id: "Hotel reservation" }), + href: `/${params.lang}/hotelreservation`, + uid: "hotel-reservation", + }, + { + title: intl.formatMessage({ id: "Select hotel" }), + href: `${selectHotel(params.lang)}/?${selectHotelParams}`, + uid: "select-hotel", + }, + { + title: city.name, + uid: city.id, + }, + ] const isAllUnavailable = hotels.every((hotel) => hotel.price === undefined) return ( <>
+ }> + +
{city.name} @@ -94,6 +128,7 @@ export default async function SelectHotelPage({
{ + beforeAll(() => { + jest.useFakeTimers({ now: NOW }) + }) + + afterAll(() => { + jest.useRealTimers() + }) + + describe("getValidFromDate", () => { + it("returns today when empty string is provided", () => { + const actual = getValidFromDate("") + expect(actual.toISOString()).toBe("2020-10-01T00:00:00.000Z") + }) + + it("returns today when undefined is provided", () => { + const actual = getValidFromDate(undefined) + expect(actual.toISOString()).toBe("2020-10-01T00:00:00.000Z") + }) + + it("returns given date in utc", () => { + const actual = getValidFromDate("2024-01-01") + expect(actual.toISOString()).toBe("2024-01-01T00:00:00.000Z") + }) + }) + + describe("getValidToDate", () => { + it("returns day after fromDate when empty string is provided", () => { + const actual = getValidToDate("", NOW) + expect(actual.toISOString()).toBe("2020-10-02T00:00:00.000Z") + }) + + it("returns day after fromDate when undefined is provided", () => { + const actual = getValidToDate(undefined, NOW) + expect(actual.toISOString()).toBe("2020-10-02T00:00:00.000Z") + }) + + it("returns given date in utc", () => { + const actual = getValidToDate("2024-01-01", NOW) + expect(actual.toISOString()).toBe("2024-01-01T00:00:00.000Z") + }) + + it("fallsback to day after fromDate when given date is before fromDate", () => { + const actual = getValidToDate("2020-09-30", NOW) + expect(actual.toISOString()).toBe("2020-10-02T00:00:00.000Z") + }) + }) +}) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/getValidDates.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/getValidDates.ts new file mode 100644 index 000000000..d9bcbf09e --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/getValidDates.ts @@ -0,0 +1,55 @@ +import { Dayjs } from "dayjs" + +import { dt } from "@/lib/dt" + +/** + * Get valid dates from stringFromDate and stringToDate making sure that they are not in the past and chronologically correct + * @example const { fromDate, toDate} = getValidDates("2021-01-01", "2021-01-02") + */ +export function getValidDates( + stringFromDate: string | undefined, + stringToDate: string | undefined +): { fromDate: Dayjs; toDate: Dayjs } { + const fromDate = getValidFromDate(stringFromDate) + const toDate = getValidToDate(stringToDate, fromDate) + + return { fromDate, toDate } +} + +/** + * Get valid fromDate from stringFromDate making sure that it is not in the past + */ +export function getValidFromDate(stringFromDate: string | undefined): Dayjs { + const now = dt().utc() + if (!stringFromDate) { + return now + } + const toDate = dt(stringFromDate) + + const yesterday = now.subtract(1, "day") + if (!toDate.isAfter(yesterday)) { + return now + } + + return toDate +} + +/** + * Get valid toDate from stringToDate making sure that it is after fromDate + */ +export function getValidToDate( + stringToDate: string | undefined, + fromDate: Dayjs | Date +): Dayjs { + const tomorrow = dt().utc().add(1, "day") + if (!stringToDate) { + return tomorrow + } + + const toDate = dt(stringToDate) + if (toDate.isAfter(fromDate)) { + return toDate + } + + return tomorrow +} 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 fd9db4d6c..fa32a54cd 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -1,22 +1,17 @@ import { notFound } from "next/navigation" +import { Suspense } from "react" -import { dt } from "@/lib/dt" -import { - getHotelData, - getLocations, - getProfileSafely, -} from "@/lib/trpc/memoizedRequests" -import { serverClient } from "@/lib/trpc/server" +import { getHotelData, getLocations } from "@/lib/trpc/memoizedRequests" import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard" -import Rooms from "@/components/HotelReservation/SelectRate/Rooms" -import { - generateChildrenString, - getHotelReservationQueryParams, -} from "@/components/HotelReservation/SelectRate/RoomSelection/utils" +import { RoomsContainer } from "@/components/HotelReservation/SelectRate/Rooms/RoomsContainer" +import { RoomsContainerSkeleton } from "@/components/HotelReservation/SelectRate/Rooms/RoomsContainerSkeleton" +import { getHotelReservationQueryParams } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { setLang } from "@/i18n/serverContext" +import { safeTry } from "@/utils/safeTry" + +import { getValidDates } from "./getValidDates" -import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import type { LangParams, PageArgs } from "@/types/params" @@ -45,71 +40,44 @@ export default async function SelectRatePage({ return notFound() } - const validFromDate = - searchParams.fromDate && - dt(searchParams.fromDate).isAfter(dt().subtract(1, "day")) - ? searchParams.fromDate - : dt().utc().format("YYYY-MM-DD") - const validToDate = - searchParams.toDate && dt(searchParams.toDate).isAfter(validFromDate) - ? searchParams.toDate - : dt().utc().add(1, "day").format("YYYY-MM-DD") - const adults = selectRoomParamsObject.room[0].adults || 1 // 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([ - getHotelData({ hotelId: searchParams.hotel, language: params.lang }), - serverClient().hotel.availability.rooms({ - hotelId: parseInt(searchParams.hotel, 10), - roomStayStartDate: validFromDate, - roomStayEndDate: validToDate, - 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 (!roomsAvailability) { - 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 - - const noRoomsAvailable = roomsAvailability.roomConfigurations.reduce( - (acc, room) => { - return acc && room.status === "NotAvailable" - }, - true + const { fromDate, toDate } = getValidDates( + searchParams.fromDate, + searchParams.toDate ) + const adults = selectRoomParamsObject.room[0].adults || 1 // TODO: Handle multiple rooms + const children = selectRoomParamsObject.room[0].child // TODO: Handle multiple rooms + + const [hotelData, hotelDataError] = await safeTry( + getHotelData({ hotelId: searchParams.hotel, language: params.lang }) + ) + + if (!hotelData && !hotelDataError) { + return notFound() + } + + const hotelId = +searchParams.hotel return ( <> - - + + }> + + ) } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/loading.tsx deleted file mode 100644 index 0fad268cc..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import LoadingSpinner from "@/components/LoadingSpinner" - -export default function LoadingHotelHeader() { - return -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.module.css deleted file mode 100644 index 82d6353ac..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.module.css +++ /dev/null @@ -1,64 +0,0 @@ -.header { - background-color: var(--Base-Surface-Subtle-Normal); - padding: var(--Spacing-x3) var(--Spacing-x2); -} - -.wrapper { - display: flex; - flex-direction: column; - gap: var(--Spacing-x3); - justify-content: center; -} - -.titleContainer { - display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: center; - gap: var(--Spacing-x1); -} - -.descriptionContainer { - display: flex; - flex-direction: column; - gap: var(--Spacing-x-one-and-half); -} - -.address { - display: flex; - gap: var(--Spacing-x-one-and-half); - font-style: normal; -} - -.dividerContainer { - display: none; -} - -@media (min-width: 768px) { - .header { - padding: var(--Spacing-x4) 0; - } - - .wrapper { - flex-direction: row; - gap: var(--Spacing-x6); - margin: 0 auto; - /* simulates padding on viewport smaller than --max-width-navigation */ - width: min( - calc(100dvw - (var(--Spacing-x2) * 2)), - var(--max-width-navigation) - ); - } - - .titleContainer > h1 { - white-space: nowrap; - } - - .dividerContainer { - display: block; - } - - .address { - gap: var(--Spacing-x3); - } -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.tsx deleted file mode 100644 index 83412f1d1..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { redirect } from "next/navigation" - -import { getHotelData } from "@/lib/trpc/memoizedRequests" - -import Divider from "@/components/TempDesignSystem/Divider" -import Body from "@/components/TempDesignSystem/Text/Body" -import Caption from "@/components/TempDesignSystem/Text/Caption" -import Title from "@/components/TempDesignSystem/Text/Title" -import { getIntl } from "@/i18n" - -import styles from "./page.module.css" - -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 hotelData = await getHotelData({ - hotelId: searchParams.hotel, - language: params.lang, - }) - if (!hotelData?.data) { - redirect(home) - } - - const intl = await getIntl() - const hotel = hotelData.data.attributes - return ( -
-
-
- - {hotel.name} - -
- - {hotel.address.streetAddress}, {hotel.address.city} - -
- -
- - {intl.formatMessage( - { id: "Distance in km to city centre" }, - { number: hotel.location.distanceToCentre } - )} - -
-
-
- -
-
- - {hotel.hotelContent.texts.descriptions.short} - -
-
-
- ) -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/loading.tsx deleted file mode 100644 index 78b79a040..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import LoadingSpinner from "@/components/LoadingSpinner" - -export default function LoadingSummaryHeader() { - return -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.module.css deleted file mode 100644 index f680a23a1..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.module.css +++ /dev/null @@ -1,68 +0,0 @@ -.mobileSummary { - display: block; -} - -.desktopSummary { - display: none; -} - -.summary { - background-color: var(--Main-Grey-White); - - border-color: var(--Primary-Light-On-Surface-Divider-subtle); - border-style: solid; - border-width: 1px; - border-bottom: none; - z-index: 10; -} - -.hider { - display: none; -} - -.shadow { - display: none; -} - -@media screen and (min-width: 1367px) { - .mobileSummary { - display: none; - } - - .desktopSummary { - 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(--Spacing-x2) + - var(--Spacing-x-half) - ); - z-index: 10; - border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; - margin-top: calc(0px - var(--Spacing-x9)); - } - - .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; - } - - .hider { - display: block; - background-color: var(--Scandic-Brand-Warm-White); - position: sticky; - top: calc(var(--booking-widget-desktop-height) - 6px); - margin-top: var(--Spacing-x4); - height: 40px; - } -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx deleted file mode 100644 index 0444913f1..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { redirect } from "next/navigation" - -import { selectRate } from "@/constants/routes/hotelReservation" -import { - getPackages, - getProfileSafely, - getSelectedRoomAvailability, -} from "@/lib/trpc/memoizedRequests" - -import Summary from "@/components/HotelReservation/EnterDetails/Summary" -import { SummaryBottomSheet } from "@/components/HotelReservation/EnterDetails/Summary/BottomSheet" -import { - generateChildrenString, - getQueryParamsForEnterDetails, -} from "@/components/HotelReservation/SelectRate/RoomSelection/utils" - -import styles from "./page.module.css" - -import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" -import { LangParams, PageArgs, SearchParams } from "@/types/params" - -export default async function SummaryPage({ - params, - searchParams, -}: PageArgs>) { - const selectRoomParams = new URLSearchParams(searchParams) - const { hotel, rooms, fromDate, toDate } = - getQueryParamsForEnterDetails(selectRoomParams) - - const { - adults, - children, - roomTypeCode, - rateCode, - packages: packageCodes, - } = rooms[0] // TODO: Handle multiple rooms - - const availability = await getSelectedRoomAvailability({ - hotelId: hotel, - adults, - children: children ? generateChildrenString(children) : undefined, - roomStayStartDate: fromDate, - roomStayEndDate: toDate, - rateCode, - roomTypeCode, - packageCodes, - }) - const user = await getProfileSafely() - - const packages = packageCodes - ? await getPackages({ - hotelId: hotel, - startDate: fromDate, - endDate: toDate, - adults, - children: children?.length, - packageCodes, - }) - : null - - if (!availability || !availability.selectedRoom) { - console.error("No hotel or availability data", availability) - // TODO: handle this case - redirect(selectRate(params.lang)) - } - - const prices = - user && availability.memberRate - ? { - local: { - price: availability.memberRate.localPrice.pricePerStay, - currency: availability.memberRate.localPrice.currency, - }, - euro: availability.memberRate.requestedPrice - ? { - price: availability.memberRate.requestedPrice.pricePerStay, - currency: availability.memberRate.requestedPrice.currency, - } - : undefined, - } - : { - local: { - price: availability.publicRate.localPrice.pricePerStay, - currency: availability.publicRate.localPrice.currency, - }, - euro: availability.publicRate?.requestedPrice - ? { - price: availability.publicRate?.requestedPrice.pricePerStay, - currency: availability.publicRate?.requestedPrice.currency, - } - : undefined, - } - - return ( - <> -
- -
- -
-
-
-
-
-
- -
-
-
- - ) -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/_preload.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/_preload.ts deleted file mode 100644 index 6013a49cc..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/_preload.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { - getCreditCardsSafely, - getProfileSafely, -} from "@/lib/trpc/memoizedRequests" - -export function preload() { - void getProfileSafely() - void getCreditCardsSafely() -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/enterDetailsLayout.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/enterDetailsLayout.css deleted file mode 100644 index 0322e44a7..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/enterDetailsLayout.css +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Due to css import issues with parallel routes we are forced to - * use a regular css file and import it in the page.tsx - * This is addressed in Next 15: https://github.com/vercel/next.js/pull/66300 - */ - -.enter-details-layout { - background-color: var(--Scandic-Brand-Warm-White); -} - -.enter-details-layout__container { - display: grid; - gap: var(--Spacing-x3) var(--Spacing-x9); - /* simulates padding on viewport smaller than --max-width-navigation */ -} - -.enter-details-layout__content { - margin: var(--Spacing-x3) var(--Spacing-x2) 0; -} - -.enter-details-layout__summaryContainer { - position: sticky; - bottom: 0; - left: 0; - right: 0; -} - -@media screen and (min-width: 1367px) { - .enter-details-layout__container { - grid-template-columns: 1fr 340px; - grid-template-rows: auto 1fr; - margin: var(--Spacing-x5) auto 0; - width: min( - calc(100dvw - (var(--Spacing-x2) * 2)), - var(--max-width-navigation) - ); - } - - .enter-details-layout__summaryContainer { - position: static; - display: grid; - grid-column: 2/3; - grid-row: 1/-1; - } -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/layout.tsx deleted file mode 100644 index 2bd8a5102..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/layout.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { getProfileSafely } from "@/lib/trpc/memoizedRequests" - -import { setLang } from "@/i18n/serverContext" -import DetailsProvider from "@/providers/DetailsProvider" - -import { preload } from "./_preload" - -import type { LangParams, LayoutArgs } from "@/types/params" - -export default async function StepLayout({ - children, - hotelHeader, - params, - summary, -}: React.PropsWithChildren< - LayoutArgs & { - hotelHeader: React.ReactNode - summary: React.ReactNode - } ->) { - setLang(params.lang) - preload() - - const user = await getProfileSafely() - - return ( - -
- {hotelHeader} -
-
{children}
- -
-
-
- ) -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.module.css new file mode 100644 index 000000000..5c757de70 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.module.css @@ -0,0 +1,35 @@ +.container { + display: grid; + gap: var(--Spacing-x3) var(--Spacing-x9); +} + +.content { + margin: var(--Spacing-x3) var(--Spacing-x2) 0; +} + +.summary { + position: sticky; + bottom: 0; + left: 0; + right: 0; +} + +@media screen and (min-width: 1367px) { + .container { + grid-template-columns: 1fr 340px; + grid-template-rows: auto 1fr; + margin: var(--Spacing-x5) auto 0; + /* simulates padding on viewport smaller than --max-width-navigation */ + width: min( + calc(100dvw - (var(--Spacing-x2) * 2)), + var(--max-width-navigation) + ); + } + + .summary { + position: static; + display: grid; + grid-column: 2/3; + grid-row: 1/-1; + } +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx index d175fc25f..bcfcca8c6 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx @@ -1,11 +1,11 @@ -import "./enterDetailsLayout.css" - import { notFound } from "next/navigation" +import { Suspense } from "react" import { getBreakfastPackages, getCreditCardsSafely, getHotelData, + getPackages, getProfileSafely, getSelectedRoomAvailability, } from "@/lib/trpc/memoizedRequests" @@ -13,16 +13,21 @@ import { import BedType from "@/components/HotelReservation/EnterDetails/BedType" import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast" import Details from "@/components/HotelReservation/EnterDetails/Details" +import HotelHeader from "@/components/HotelReservation/EnterDetails/Header" import HistoryStateManager from "@/components/HotelReservation/EnterDetails/HistoryStateManager" import Payment from "@/components/HotelReservation/EnterDetails/Payment" import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion" import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom" +import Summary from "@/components/HotelReservation/EnterDetails/Summary" import { generateChildrenString, getQueryParamsForEnterDetails, } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { getIntl } from "@/i18n" -import StepsProvider from "@/providers/StepsProvider" +import { setLang } from "@/i18n/serverContext" +import EnterDetailsProvider from "@/providers/EnterDetailsProvider" + +import styles from "./page.module.css" import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import { StepEnum } from "@/types/enums/step" @@ -36,60 +41,78 @@ export default async function StepPage({ params: { lang }, searchParams, }: PageArgs) { + if (!isValidStep(searchParams.step)) { + return notFound() + } + setLang(lang) + const intl = await getIntl() const selectRoomParams = new URLSearchParams(searchParams) + // Deleting step to avoid double searchparams after rewrite selectRoomParams.delete("step") - const searchParamsString = selectRoomParams.toString() + const booking = getQueryParamsForEnterDetails(selectRoomParams) + const { hotel: hotelId, - rooms, + rooms: [ + { adults, children, roomTypeCode, rateCode, packages: packageCodes }, + ], // TODO: Handle multiple rooms fromDate, toDate, - } = getQueryParamsForEnterDetails(selectRoomParams) - - const { - adults, - children, - roomTypeCode, - rateCode, - packages: packageCodes, - } = rooms[0] // TODO: Handle multiple rooms + } = booking const childrenAsString = children && generateChildrenString(children) - const breakfastInput = { adults, fromDate, hotelId, toDate } - void getBreakfastPackages(breakfastInput) - void getSelectedRoomAvailability({ - hotelId, + const selectedRoomAvailabilityInput = { adults, children: childrenAsString, + hotelId, + packageCodes, + rateCode, roomStayStartDate: fromDate, roomStayEndDate: toDate, - rateCode, roomTypeCode, - packageCodes, - }) + } - const roomAvailability = await getSelectedRoomAvailability({ - hotelId, - adults, - children: childrenAsString, - roomStayStartDate: fromDate, - roomStayEndDate: toDate, - rateCode, - roomTypeCode, - packageCodes, - }) + void getProfileSafely() + void getCreditCardsSafely() + void getBreakfastPackages(breakfastInput) + void getSelectedRoomAvailability(selectedRoomAvailabilityInput) + if (packageCodes?.length) { + void getPackages({ + adults, + children: children?.length, + endDate: toDate, + hotelId, + packageCodes, + startDate: fromDate, + }) + } + + const packages = packageCodes + ? await getPackages({ + adults, + children: children?.length, + endDate: toDate, + hotelId, + packageCodes, + startDate: fromDate, + }) + : null + + const roomAvailability = await getSelectedRoomAvailability( + selectedRoomAvailabilityInput + ) const hotelData = await getHotelData({ hotelId, - language: lang, isCardOnlyPayment: roomAvailability?.mustBeGuaranteed, + language: lang, }) const breakfastPackages = await getBreakfastPackages(breakfastInput) const user = await getProfileSafely() const savedCreditCards = await getCreditCardsSafely() - if (!isValidStep(searchParams.step) || !hotelData || !roomAvailability) { + if (!hotelData || !roomAvailability) { return notFound() } @@ -121,66 +144,96 @@ export default async function StepPage({ : undefined return ( - -
- - +
+ +
+
+
+ + - {/* TODO: How to handle no beds found? */} - {roomAvailability.bedTypes ? ( - - - - ) : null} + {/* TODO: How to handle no beds found? */} + {roomAvailability.bedTypes ? ( + + + + ) : null} - {breakfastPackages?.length ? ( - - - - ) : null} + {breakfastPackages?.length ? ( + + + + ) : null} - -
- + +
+ - - - -
- + + + + + +
+
+ +
+ + ) } diff --git a/app/[lang]/(live)/layout.tsx b/app/[lang]/(live)/layout.tsx index e1c63bbbc..037f2d714 100644 --- a/app/[lang]/(live)/layout.tsx +++ b/app/[lang]/(live)/layout.tsx @@ -9,6 +9,7 @@ import TrpcProvider from "@/lib/trpc/Provider" import TokenRefresher from "@/components/Auth/TokenRefresher" import AdobeSDKScript from "@/components/Current/AdobeSDKScript" import VwoScript from "@/components/Current/VwoScript" +import StorageCleaner from "@/components/HotelReservation/EnterDetails/StorageCleaner" import { ToastHandler } from "@/components/TempDesignSystem/Toasts" import { preloadUserTracking } from "@/components/TrackingSDK" import { getIntl } from "@/i18n" @@ -64,6 +65,7 @@ export default async function RootLayout({ {footer} + diff --git a/app/api/web/payment-callback/[lang]/[status]/route.ts b/app/api/web/payment-callback/[lang]/[status]/route.ts deleted file mode 100644 index 5884d63f9..000000000 --- a/app/api/web/payment-callback/[lang]/[status]/route.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { NextRequest, NextResponse } from "next/server" - -import { - BOOKING_CONFIRMATION_NUMBER, - PaymentErrorCodeEnum, -} from "@/constants/booking" -import { Lang } from "@/constants/languages" -import { - bookingConfirmation, - payment, -} from "@/constants/routes/hotelReservation" -import { serverClient } from "@/lib/trpc/server" -import { getPublicURL } from "@/server/utils" - -export async function GET( - request: NextRequest, - { params }: { params: { lang: string; status: string } } -): Promise { - const publicURL = getPublicURL(request) - - console.log(`[payment-callback] callback started`) - const lang = params.lang as Lang - const status = params.status - - const queryParams = request.nextUrl.searchParams - const confirmationNumber = queryParams.get(BOOKING_CONFIRMATION_NUMBER) - - if (status === "success" && confirmationNumber) { - const confirmationUrl = new URL(`${publicURL}/${bookingConfirmation(lang)}`) - confirmationUrl.searchParams.set( - BOOKING_CONFIRMATION_NUMBER, - confirmationNumber - ) - - console.log(`[payment-callback] redirecting to: ${confirmationUrl}`) - return NextResponse.redirect(confirmationUrl) - } - - const returnUrl = new URL(`${publicURL}/${payment(lang)}`) - returnUrl.search = queryParams.toString() - - if (confirmationNumber) { - try { - const bookingStatus = await serverClient().booking.status({ - confirmationNumber, - }) - if (bookingStatus.metadata) { - returnUrl.searchParams.set( - "errorCode", - bookingStatus.metadata.errorCode?.toString() ?? "" - ) - } - } catch (error) { - console.error( - `[payment-callback] failed to get booking status for ${confirmationNumber}, status: ${status}` - ) - - if (status === "cancel") { - returnUrl.searchParams.set( - "errorCode", - PaymentErrorCodeEnum.Cancelled.toString() - ) - } - if (status === "error") { - returnUrl.searchParams.set( - "errorCode", - PaymentErrorCodeEnum.Failed.toString() - ) - } - } - } - - console.log(`[payment-callback] redirecting to: ${returnUrl}`) - return NextResponse.redirect(returnUrl) -} diff --git a/app/api/web/revalidate/route.ts b/app/api/web/revalidate/route.ts index df022b91a..42baad8d6 100644 --- a/app/api/web/revalidate/route.ts +++ b/app/api/web/revalidate/route.ts @@ -29,6 +29,7 @@ const validateJsonBody = z.object({ }) .optional(), locale: z.nativeEnum(Lang), + publish_details: z.object({ locale: z.nativeEnum(Lang) }).optional(), uid: z.string(), url: z.string().optional(), page_settings: z @@ -67,15 +68,19 @@ export async function POST(request: NextRequest) { data: { content_type, entry }, }, } = validatedData - const refsTag = generateRefsResponseTag(entry.locale, entry.uid) - const refTag = generateRefTag(entry.locale, content_type.uid, entry.uid) - const tag = generateTag(entry.locale, entry.uid) + + // The publish_details.locale is the locale that the entry is published in, regardless if it is "localized" or not + const entryLocale = entry.publish_details?.locale ?? entry.locale + + const refsTag = generateRefsResponseTag(entryLocale, entry.uid) + const refTag = generateRefTag(entryLocale, content_type.uid, entry.uid) + const tag = generateTag(entryLocale, entry.uid) const languageSwitcherTag = generateTag( - entry.locale, + entryLocale, entry.uid, languageSwitcherAffix ) - const metadataTag = generateTag(entry.locale, entry.uid, metadataAffix) + const metadataTag = generateTag(entryLocale, entry.uid, metadataAffix) console.info(`Revalidating refsTag: ${refsTag}`) revalidateTag(refsTag) @@ -94,12 +99,12 @@ export async function POST(request: NextRequest) { if (entry.breadcrumbs) { const breadcrumbsRefsTag = generateRefsResponseTag( - entry.locale, + entryLocale, entry.uid, breadcrumbsAffix ) const breadcrumbsTag = generateTag( - entry.locale, + entryLocale, entry.uid, breadcrumbsAffix ) @@ -113,7 +118,7 @@ export async function POST(request: NextRequest) { if (entry.page_settings?.hide_booking_widget) { const bookingwidgetTag = generateTag( - entry.locale, + entryLocale, entry.uid, bookingwidgetAffix ) diff --git a/components/Blocks/DynamicContent/Stays/Soonest/EmptyUpcomingStays/index.tsx b/components/Blocks/DynamicContent/Stays/Soonest/EmptyUpcomingStays/index.tsx index 3835ae61d..fee95429c 100644 --- a/components/Blocks/DynamicContent/Stays/Soonest/EmptyUpcomingStays/index.tsx +++ b/components/Blocks/DynamicContent/Stays/Soonest/EmptyUpcomingStays/index.tsx @@ -14,7 +14,13 @@ export default async function EmptyUpcomingStaysBlock() { return (
- + <Title + as="h4" + level="h3" + color="red" + className={styles.title} + textAlign="center" + > {intl.formatMessage({ id: "You have no upcoming stays." })} <span className={styles.burgundyTitle}> {intl.formatMessage({ id: "Where should you go next?" })} diff --git a/components/Blocks/DynamicContent/Stays/Upcoming/EmptyUpcomingStays/index.tsx b/components/Blocks/DynamicContent/Stays/Upcoming/EmptyUpcomingStays/index.tsx index 3835ae61d..fee95429c 100644 --- a/components/Blocks/DynamicContent/Stays/Upcoming/EmptyUpcomingStays/index.tsx +++ b/components/Blocks/DynamicContent/Stays/Upcoming/EmptyUpcomingStays/index.tsx @@ -14,7 +14,13 @@ export default async function EmptyUpcomingStaysBlock() { return ( <section className={styles.container}> <div className={styles.titleContainer}> - <Title as="h4" level="h3" color="red" className={styles.title}> + <Title + as="h4" + level="h3" + color="red" + className={styles.title} + textAlign="center" + > {intl.formatMessage({ id: "You have no upcoming stays." })} <span className={styles.burgundyTitle}> {intl.formatMessage({ id: "Where should you go next?" })} diff --git a/components/Breadcrumbs/index.tsx b/components/Breadcrumbs/index.tsx index a3244fab8..00d3d7985 100644 --- a/components/Breadcrumbs/index.tsx +++ b/components/Breadcrumbs/index.tsx @@ -1,60 +1,25 @@ import { serverClient } from "@/lib/trpc/server" -import { ChevronRightSmallIcon,HouseIcon } from "@/components/Icons" -import Link from "@/components/TempDesignSystem/Link" -import Footnote from "@/components/TempDesignSystem/Text/Footnote" - -import styles from "./breadcrumbs.module.css" +import BreadcrumbsComp from "@/components/TempDesignSystem/Breadcrumbs" +import { generateBreadcrumbsSchema } from "@/utils/jsonSchemas" export default async function Breadcrumbs() { const breadcrumbs = await serverClient().contentstack.breadcrumbs.get() + if (!breadcrumbs?.length) { return null } + const jsonSchema = generateBreadcrumbsSchema(breadcrumbs) - const homeBreadcrumb = breadcrumbs.shift() return ( - <nav className={styles.breadcrumbs}> - <ul className={styles.list}> - {homeBreadcrumb ? ( - <li className={styles.listItem}> - <Link - className={styles.homeLink} - color="peach80" - href={homeBreadcrumb.href!} - variant="breadcrumb" - > - <HouseIcon width={16} height={16} color="peach80" /> - </Link> - <ChevronRightSmallIcon aria-hidden="true" color="peach80" /> - </li> - ) : null} - - {breadcrumbs.map((breadcrumb) => { - if (breadcrumb.href) { - return ( - <li key={breadcrumb.uid} className={styles.listItem}> - <Link - color="peach80" - href={breadcrumb.href} - variant="breadcrumb" - > - {breadcrumb.title} - </Link> - <ChevronRightSmallIcon aria-hidden="true" color="peach80" /> - </li> - ) - } - - return ( - <li key={breadcrumb.uid} className={styles.listItem}> - <Footnote color="burgundy" type="bold"> - {breadcrumb.title} - </Footnote> - </li> - ) - })} - </ul> - </nav> + <> + <script + type={jsonSchema.type} + dangerouslySetInnerHTML={{ + __html: JSON.stringify(jsonSchema.jsonLd), + }} + /> + <BreadcrumbsComp breadcrumbs={breadcrumbs} /> + </> ) } diff --git a/components/ContentType/ContentPage/HotelListingItem/index.tsx b/components/ContentType/ContentPage/HotelListingItem/index.tsx index 2f8924f1b..18f8f6f7f 100644 --- a/components/ContentType/ContentPage/HotelListingItem/index.tsx +++ b/components/ContentType/ContentPage/HotelListingItem/index.tsx @@ -8,6 +8,7 @@ import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" +import getSingleDecimal from "@/utils/numberFormatting" import styles from "./hotelListingItem.module.css" @@ -47,7 +48,7 @@ export default async function HotelListingItem({ <Caption color="uiTextPlaceholder"> {intl.formatMessage( { id: "Distance in km to city centre" }, - { number: distanceToCentre } + { number: getSingleDecimal(distanceToCentre / 1000) } )} </Caption> </div> diff --git a/components/ContentType/HotelPage/IntroSection/index.tsx b/components/ContentType/HotelPage/IntroSection/index.tsx index fe2db96c0..7a5349ff0 100644 --- a/components/ContentType/HotelPage/IntroSection/index.tsx +++ b/components/ContentType/HotelPage/IntroSection/index.tsx @@ -1,7 +1,6 @@ import { about } from "@/constants/routes/hotelPageParams" -import { ChevronRightSmallIcon } from "@/components/Icons" -import TripAdvisorIcon from "@/components/Icons/TripAdvisor" +import { ChevronRightSmallIcon, TripAdvisorIcon } from "@/components/Icons" import Link from "@/components/TempDesignSystem/Link" import BiroScript from "@/components/TempDesignSystem/Text/BiroScript" import Body from "@/components/TempDesignSystem/Text/Body" @@ -9,6 +8,7 @@ import Preamble from "@/components/TempDesignSystem/Text/Preamble" import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" +import getSingleDecimal from "@/utils/numberFormatting" import styles from "./introSection.module.css" @@ -26,7 +26,7 @@ export default async function IntroSection({ const { distanceToCentre } = location const formattedDistanceText = intl.formatMessage( { id: "Distance in km to city centre" }, - { number: distanceToCentre } + { number: getSingleDecimal(distanceToCentre / 1000) } ) const lang = getLang() const formattedLocationText = `${streetAddress}, ${city} (${formattedDistanceText})` diff --git a/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx b/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx index db979bf47..d58cb10fc 100644 --- a/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx +++ b/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx @@ -60,13 +60,20 @@ export default function Sidebar({ } } - function handleMouseEnter(poiName: string) { + function handleMouseEnter(poiName: string | undefined) { + if (!poiName) return + if (!isClicking) { onActivePoiChange(poiName) } } - function handlePoiClick(poiName: string, poiCoordinates: Coordinates) { + function handlePoiClick( + poiName: string | undefined, + poiCoordinates: Coordinates + ) { + if (!poiName || !poiCoordinates) return + setIsClicking(true) toggleFullScreenSidebar() onActivePoiChange(poiName) diff --git a/components/ContentType/HotelPage/Map/DynamicMap/index.tsx b/components/ContentType/HotelPage/Map/DynamicMap/index.tsx index 969b24588..6af68db43 100644 --- a/components/ContentType/HotelPage/Map/DynamicMap/index.tsx +++ b/components/ContentType/HotelPage/Map/DynamicMap/index.tsx @@ -113,7 +113,7 @@ export default function DynamicMap({ activePoi={activePoi} hotelName={hotelName} pointsOfInterest={pointsOfInterest} - onActivePoiChange={setActivePoi} + onActivePoiChange={(poi) => setActivePoi(poi ?? null)} coordinates={coordinates} /> <InteractiveMap @@ -121,7 +121,7 @@ export default function DynamicMap({ coordinates={coordinates} pointsOfInterest={pointsOfInterest} activePoi={activePoi} - onActivePoiChange={setActivePoi} + onActivePoiChange={(poi) => setActivePoi(poi ?? null)} mapId={mapId} /> </Dialog> diff --git a/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx b/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx index b6784513a..dba816cb5 100644 --- a/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx +++ b/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx @@ -1,24 +1,23 @@ "use client" +import Link from "next/link" import { useIntl } from "react-intl" -import useSidePeekStore from "@/stores/sidepeek" - import { ChevronRightSmallIcon } from "@/components/Icons" import ImageGallery from "@/components/ImageGallery" import Button from "@/components/TempDesignSystem/Button" import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import { getRoomNameAsParam } from "../../utils" + import styles from "./roomCard.module.css" import type { RoomCardProps } from "@/types/components/hotelPage/room" -import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek" -export function RoomCard({ hotelId, room }: RoomCardProps) { +export function RoomCard({ room }: RoomCardProps) { const { images, name, roomSize, occupancy } = room const intl = useIntl() - const openSidePeek = useSidePeekStore((state) => state.openSidePeek) const size = roomSize?.min === roomSize?.max @@ -51,21 +50,11 @@ export function RoomCard({ hotelId, room }: RoomCardProps) { )} </Body> </div> - <Button - intent="text" - type="button" - size="medium" - theme="base" - onClick={() => - openSidePeek({ - key: SidePeekEnum.roomDetails, - hotelId, - roomTypeCode: room.roomTypes[0].code, - }) - } - > - {intl.formatMessage({ id: "See room details" })} - <ChevronRightSmallIcon color="burgundy" width={20} height={20} /> + <Button intent="text" type="button" size="medium" theme="base" asChild> + <Link scroll={false} href={`?s=${getRoomNameAsParam(name)}`}> + {intl.formatMessage({ id: "See room details" })} + <ChevronRightSmallIcon color="burgundy" width={20} height={20} /> + </Link> </Button> </div> </article> diff --git a/components/ContentType/HotelPage/Rooms/index.tsx b/components/ContentType/HotelPage/Rooms/index.tsx index 325f60eb6..e976ee8af 100644 --- a/components/ContentType/HotelPage/Rooms/index.tsx +++ b/components/ContentType/HotelPage/Rooms/index.tsx @@ -15,7 +15,7 @@ import styles from "./rooms.module.css" import type { RoomsProps } from "@/types/components/hotelPage/room" import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation" -export function Rooms({ hotelId, rooms }: RoomsProps) { +export function Rooms({ rooms }: RoomsProps) { const intl = useIntl() const showToggleButton = rooms.length > 3 const [allRoomsVisible, setAllRoomsVisible] = useState(!showToggleButton) @@ -45,7 +45,7 @@ export function Rooms({ hotelId, rooms }: RoomsProps) { > {rooms.map((room) => ( <div key={room.id}> - <RoomCard hotelId={hotelId} room={room} /> + <RoomCard room={room} /> </div> ))} </Grids.Stackable> diff --git a/components/ContentType/HotelPage/SidePeeks/AboutTheHotel/ContactInformation/contactInformation.module.css b/components/ContentType/HotelPage/SidePeeks/AboutTheHotel/ContactInformation/contactInformation.module.css new file mode 100644 index 000000000..b616382ab --- /dev/null +++ b/components/ContentType/HotelPage/SidePeeks/AboutTheHotel/ContactInformation/contactInformation.module.css @@ -0,0 +1,48 @@ +.wrapper { + display: flex; + flex-direction: column; + gap: var(--Spacing-x-one-and-half); +} + +.information { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--Spacing-x2); + grid-template-areas: + "address drivingDirections" + "contact socials" + "email email" + "ecoLabel ecoLabel"; +} + +.address { + grid-area: address; +} + +.drivingDirections { + grid-area: drivingDirections; +} + +.contact { + grid-area: contact; +} + +.socials { + grid-area: socials; +} + +.socialIcons { + display: flex; + gap: var(--Spacing-x1); + align-items: center; +} + +.email { + grid-area: email; +} + +.ecoLabel { + grid-area: ecoLabel; + display: flex; + gap: var(--Spacing-x-one-and-half); +} diff --git a/components/ContentType/HotelPage/SidePeeks/AboutTheHotel/ContactInformation/index.tsx b/components/ContentType/HotelPage/SidePeeks/AboutTheHotel/ContactInformation/index.tsx new file mode 100644 index 000000000..f4bf137a4 --- /dev/null +++ b/components/ContentType/HotelPage/SidePeeks/AboutTheHotel/ContactInformation/index.tsx @@ -0,0 +1,120 @@ +import { FacebookIcon, InstagramIcon } from "@/components/Icons" +import Image from "@/components/Image" +import Link from "@/components/TempDesignSystem/Link" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import Title from "@/components/TempDesignSystem/Text/Title" +import { getIntl } from "@/i18n" +import { getLang } from "@/i18n/serverContext" + +import styles from "./contactInformation.module.css" + +import type { ContactInformationProps } from "@/types/components/hotelPage/sidepeek/contactInformation" + +export default async function ContactInformation({ + hotelAddress, + coordinates, + contact, + socials, + ecoLabels, +}: ContactInformationProps) { + const intl = await getIntl() + const lang = getLang() + + const { latitude, longitude } = coordinates + const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${latitude},${longitude}` + + return ( + <div className={styles.wrapper}> + <Subtitle color="burgundy" asChild> + <Title level="h3"> + {intl.formatMessage({ id: "Practical information" })} + + +
+
+ + {intl.formatMessage({ id: "Address" })} + + {hotelAddress.streetAddress} + {hotelAddress.city} +
+
+ + {intl.formatMessage({ id: "Driving directions" })} + + + Google Maps + +
+
+ + {intl.formatMessage({ id: "Contact us" })} + + + + {contact.phoneNumber} + + +
+
+ + {intl.formatMessage({ id: "Follow us" })} + +
+ {socials.instagram && ( + + + + )} + {socials.facebook && ( + + + + )} +
+
+
+ + {intl.formatMessage({ id: "Email" })} + + + {contact.email} + +
+ {ecoLabels.nordicEcoLabel && ( +
+ {intl.formatMessage({ +
+ + {intl.formatMessage({ id: "Nordic Swan Ecolabel" })} + + + {ecoLabels.svanenEcoLabelCertificateNumber} + +
+
+ )} +
+
+ ) +} diff --git a/components/ContentType/HotelPage/SidePeeks/AboutTheHotel/aboutTheHotel.module.css b/components/ContentType/HotelPage/SidePeeks/AboutTheHotel/aboutTheHotel.module.css new file mode 100644 index 000000000..00ab8aebe --- /dev/null +++ b/components/ContentType/HotelPage/SidePeeks/AboutTheHotel/aboutTheHotel.module.css @@ -0,0 +1,5 @@ +.wrapper { + display: flex; + flex-direction: column; + gap: var(--Spacing-x3); +} diff --git a/components/ContentType/HotelPage/SidePeeks/AboutTheHotel/index.tsx b/components/ContentType/HotelPage/SidePeeks/AboutTheHotel/index.tsx new file mode 100644 index 000000000..436147a50 --- /dev/null +++ b/components/ContentType/HotelPage/SidePeeks/AboutTheHotel/index.tsx @@ -0,0 +1,46 @@ +import { about } from "@/constants/routes/hotelPageParams" + +import Divider from "@/components/TempDesignSystem/Divider" +import SidePeek from "@/components/TempDesignSystem/SidePeek" +import Body from "@/components/TempDesignSystem/Text/Body" +import Preamble from "@/components/TempDesignSystem/Text/Preamble" +import { getIntl } from "@/i18n" +import { getLang } from "@/i18n/serverContext" + +import ContactInformation from "./ContactInformation" + +import styles from "./aboutTheHotel.module.css" + +import type { AboutTheHotelSidePeekProps } from "@/types/components/hotelPage/sidepeek/aboutTheHotel" + +export default async function AboutTheHotelSidePeek({ + hotelAddress, + coordinates, + contact, + socials, + ecoLabels, + descriptions, +}: AboutTheHotelSidePeekProps) { + const lang = getLang() + const intl = await getIntl() + + return ( + +
+ + + {descriptions.descriptions.medium} + {descriptions.facilityInformation} +
+
+ ) +} diff --git a/components/ContentType/HotelPage/SidePeeks/Room/index.tsx b/components/ContentType/HotelPage/SidePeeks/Room/index.tsx new file mode 100644 index 000000000..ded1acf8d --- /dev/null +++ b/components/ContentType/HotelPage/SidePeeks/Room/index.tsx @@ -0,0 +1,118 @@ +import Link from "next/link" + +import ImageGallery from "@/components/ImageGallery" +import { getBedIcon } from "@/components/SidePeeks/RoomSidePeek/bedIcon" +import { getFacilityIcon } from "@/components/SidePeeks/RoomSidePeek/facilityIcon" +import Button from "@/components/TempDesignSystem/Button" +import SidePeek from "@/components/TempDesignSystem/SidePeek" +import Body from "@/components/TempDesignSystem/Text/Body" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import { getIntl } from "@/i18n" + +import { getRoomNameAsParam } from "../../utils" + +import styles from "./room.module.css" + +import type { RoomSidePeekProps } from "@/types/components/hotelPage/sidepeek/room" + +export default async function RoomSidePeek({ room }: RoomSidePeekProps) { + const intl = await getIntl() + const { roomSize, occupancy, descriptions, images } = room + const roomDescription = descriptions.medium + const totalOccupancy = occupancy.total + // TODO: Not defined where this should lead. + const ctaUrl = "" + + return ( + +
+
+ + {roomSize.min === roomSize.max + ? roomSize.min + : `${roomSize.min} - ${roomSize.max}`} + m².{" "} + {intl.formatMessage( + { id: "booking.accommodatesUpTo" }, + { nrOfGuests: totalOccupancy } + )} + +
+ +
+ {roomDescription} +
+ +
+ +

+ {intl.formatMessage({ id: "booking.thisRoomIsEquippedWith" })} +

+
+
    + {room.roomFacilities + .sort((a, b) => a.sortOrder - b.sortOrder) + .map((facility) => { + const Icon = getFacilityIcon(facility.icon) + return ( +
  • + {Icon && ( + + )} + + {facility.name} + +
  • + ) + })} +
+
+ +
+ +

{intl.formatMessage({ id: "booking.bedOptions" })}

+
+ + {intl.formatMessage({ id: "booking.basedOnAvailability" })} + +
    + {room.roomTypes.map((roomType) => { + const BedIcon = getBedIcon(roomType.mainBed.type) + return ( +
  • + {BedIcon && ( + + )} + + {roomType.mainBed.description} + +
  • + ) + })} +
+
+
+ {ctaUrl && ( +
+ +
+ )} +
+ ) +} diff --git a/components/ContentType/HotelPage/SidePeeks/Room/room.module.css b/components/ContentType/HotelPage/SidePeeks/Room/room.module.css new file mode 100644 index 000000000..2996dd8f2 --- /dev/null +++ b/components/ContentType/HotelPage/SidePeeks/Room/room.module.css @@ -0,0 +1,48 @@ +.content { + display: grid; + gap: var(--Spacing-x2); + position: relative; + margin-bottom: calc( + var(--Spacing-x4) * 2 + 80px + ); /* Creates space between the wrapper and buttonContainer */ +} +.innerContent { + display: grid; + gap: var(--Spacing-x-one-and-half); +} + +.imageContainer { + position: relative; + border-radius: var(--Corner-radius-Medium); + overflow: hidden; +} + +.facilityList { + column-count: 2; + column-gap: var(--Spacing-x2); +} + +.bedOptions { + display: flex; + flex-direction: column; +} + +.listItem { + display: flex; + gap: var(--Spacing-x1); + margin-bottom: var(--Spacing-x-half); +} + +.noIcon { + margin-left: var(--Spacing-x4); +} + +.buttonContainer { + background-color: var(--Base-Background-Primary-Normal); + border-top: 1px solid var(--Base-Border-Subtle); + padding: var(--Spacing-x4) var(--Spacing-x2); + width: 100%; + position: absolute; + left: 0; + bottom: 0; +} diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/index.tsx b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/index.tsx index e00ed5964..3313a109b 100644 --- a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/index.tsx @@ -16,7 +16,7 @@ export default async function Facility({ data }: FacilityProps) { return (
- {image.imageSizes.medium && ( + {image?.imageSizes.medium && ( {image.metaData.altText = { [FacilityEnum.GymTrainingFacilities]: IconName.Fitness, [FacilityEnum.KeyAccessOnlyToHealthClubGym]: IconName.Fitness, [FacilityEnum.FreeWiFi]: IconName.Wifi, - [FacilityEnum.MeetingRooms]: IconName.People2, - [FacilityEnum.MeetingConferenceFacilities]: IconName.People2, + [FacilityEnum.MeetingRooms]: IconName.Business, + [FacilityEnum.MeetingConferenceFacilities]: IconName.Business, [FacilityEnum.PetFriendlyRooms]: IconName.Pets, [FacilityEnum.Sauna]: IconName.Sauna, [FacilityEnum.Restaurant]: IconName.Restaurant, diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index a565ea117..969d75936 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -1,77 +1,128 @@ -import hotelPageParams from "@/constants/routes/hotelPageParams" +import { notFound } from "next/navigation" + +import { + activities, + amenities, + meetingsAndConferences, + restaurantAndBar, +} from "@/constants/routes/hotelPageParams" import { env } from "@/env/server" -import { serverClient } from "@/lib/trpc/server" +import { getHotelData, getHotelPage } from "@/lib/trpc/memoizedRequests" import AccordionSection from "@/components/Blocks/Accordion" -import HotelReservationSidePeek from "@/components/HotelReservation/SidePeek" import SidePeekProvider from "@/components/SidePeeks/SidePeekProvider" import Alert from "@/components/TempDesignSystem/Alert" import SidePeek from "@/components/TempDesignSystem/SidePeek" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" import { getRestaurantHeading } from "@/utils/facilityCards" +import { generateHotelSchema } from "@/utils/jsonSchemas" import DynamicMap from "./Map/DynamicMap" import MapCard from "./Map/MapCard" import MapWithCardWrapper from "./Map/MapWithCard" import MobileMapToggle from "./Map/MobileMapToggle" import StaticMap from "./Map/StaticMap" -import WellnessAndExerciseSidePeek from "./SidePeeks/WellnessAndExercise" import AmenitiesList from "./AmenitiesList" import Facilities from "./Facilities" import IntroSection from "./IntroSection" import PreviewImages from "./PreviewImages" import { Rooms } from "./Rooms" +import { + AboutTheHotelSidePeek, + RoomSidePeek, + WellnessAndExerciseSidePeek, +} from "./SidePeeks" import TabNavigation from "./TabNavigation" import styles from "./hotelPage.module.css" +import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities" +import type { HotelPageProps } from "@/types/components/hotelPage/hotelPage" import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation" +import type { Facility } from "@/types/hotel" -export default async function HotelPage() { - const intl = await getIntl() +export default async function HotelPage({ hotelId }: HotelPageProps) { const lang = getLang() + const [intl, hotelPageData, hotelData] = await Promise.all([ + getIntl(), + getHotelPage(), + getHotelData({ hotelId, language: lang }), + ]) const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID - const hotelData = await serverClient().hotel.get() - if (!hotelData) { - return null + + if (!hotelData?.data || !hotelPageData) { + return notFound() } + const jsonSchema = generateHotelSchema(hotelData.data.attributes) + const { faq, content } = hotelPageData const { - hotelId, - hotelName, - hotelDescription, - hotelLocation, - hotelAddress, - hotelRatings, - hotelDetailedFacilities, - hotelImages, - roomCategories, - activitiesCard, + name, + address, pointsOfInterest, - facilities, - faq, - alerts, + gallery, + specialAlerts, + healthAndWellness, + restaurantImages, + conferencesAndMeetings, + hotelContent, + detailedFacilities, healthFacilities, - } = hotelData + contactInformation, + socialMedia, + hotelFacts, + location, + ratings, + } = hotelData.data.attributes + const roomCategories = + hotelData.included?.filter((item) => item.type === "roomcategories") || [] + const images = gallery?.smallerImages + const description = hotelContent.texts.descriptions.short + const activitiesCard = content?.[0]?.upcoming_activities_card || null + + const facilities: Facility[] = [ + { + ...restaurantImages, + id: FacilityCardTypeEnum.restaurant, + headingText: restaurantImages?.headingText ?? "", + heroImages: restaurantImages?.heroImages ?? [], + }, + { + ...conferencesAndMeetings, + id: FacilityCardTypeEnum.conference, + headingText: conferencesAndMeetings?.headingText ?? "", + heroImages: conferencesAndMeetings?.heroImages ?? [], + }, + { + ...healthAndWellness, + id: FacilityCardTypeEnum.wellness, + headingText: healthAndWellness?.headingText ?? "", + heroImages: healthAndWellness?.heroImages ?? [], + }, + ] const topThreePois = pointsOfInterest.slice(0, 3) const coordinates = { - lat: hotelLocation.latitude, - lng: hotelLocation.longitude, + lat: location.latitude, + lng: location.longitude, } return (
+