Merge master

This commit is contained in:
Linus Flood
2024-11-28 13:37:45 +01:00
225 changed files with 4488 additions and 3192 deletions

View File

@@ -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<LangParams & ContentTypeParams & UIDParams, {}>) {
setLang(params.lang)
@@ -57,7 +58,12 @@ export default function ContentTypePage({
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()
}
return <HotelPage />
const hotelPageData = await getHotelPage()
return hotelPageData ? (
<HotelPage hotelId={hotelPageData.hotel_page_id} />
) : (
notFound()
)
default:
const type: never = params.contentType
console.error(`Unsupported content type given: ${type}`)

View File

@@ -1,6 +1,8 @@
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
import BookingConfirmation from "@/components/HotelReservation/BookingConfirmation"
import Details from "@/components/HotelReservation/BookingConfirmation/Details"
import Header from "@/components/HotelReservation/BookingConfirmation/Header"
import TotalPrice from "@/components/HotelReservation/BookingConfirmation/TotalPrice"
import { setLang } from "@/i18n/serverContext"
import styles from "./page.module.css"
@@ -12,11 +14,14 @@ export default async function BookingConfirmationPage({
searchParams,
}: PageArgs<LangParams, { confirmationNumber: string }>) {
setLang(params.lang)
const confirmationNumber = searchParams.confirmationNumber
void getBookingConfirmation(confirmationNumber)
void getBookingConfirmation(searchParams.confirmationNumber)
const { confirmationNumber } = searchParams
return (
<main className={styles.main}>
<BookingConfirmation confirmationNumber={confirmationNumber} />
</main>
<div className={styles.main}>
<Header confirmationNumber={confirmationNumber} />
<Details confirmationNumber={confirmationNumber} />
<TotalPrice confirmationNumber={confirmationNumber} />
</div>
)
}

View File

@@ -8,8 +8,5 @@ import styles from "./layout.module.css"
export default function ConfirmedBookingLayout({
children,
}: React.PropsWithChildren) {
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()
}
return <div className={styles.layout}>{children}</div>
}

View File

@@ -9,8 +9,5 @@ import { LangParams, LayoutArgs } from "@/types/params"
export default function PaymentCallbackLayout({
children,
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()
}
return <div className={styles.layout}>{children}</div>
}

View File

@@ -42,12 +42,12 @@ export default async function PaymentCallbackPage({
const bookingStatus = await serverClient().booking.status({
confirmationNumber,
})
if (bookingStatus.metadata) {
searchObject.set(
"errorCode",
bookingStatus.metadata.errorCode?.toString() ?? ""
)
}
searchObject.set(
"errorCode",
bookingStatus?.metadata?.errorCode
? bookingStatus.metadata.errorCode.toString()
: PaymentErrorCodeEnum.Failed.toString()
)
} catch (error) {
console.error(
`[payment-callback] failed to get booking status for ${confirmationNumber}, status: ${status}`

View File

@@ -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<LangParams, { hotel: string }>) {
if (!searchParams.hotel) {
return <SidePeek hotel={null} />
}
const hotel = await getHotelData({
hotelId: searchParams.hotel,
language: params.lang,
})
return <SidePeek hotel={hotel} />
}

View File

@@ -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<LangParams, { hotel: string }>) {
const search = new URLSearchParams(searchParams)
const { hotel: hotelId } = getQueryParamsForEnterDetails(search)
if (!hotelId) {
return <SidePeek hotel={null} />
}
const hotel = await getHotelData({
hotelId: hotelId,
language: params.lang,
})
return <SidePeek hotel={hotel} />
export default function HotelSidePeekSlot() {
return null
}

View File

@@ -12,7 +12,7 @@ export default function HotelReservationLayout({
}: React.PropsWithChildren<LayoutArgs<LangParams>> & {
sidePeek: React.ReactNode
}) {
if (env.HIDE_FOR_NEXT_RELEASE) {
if (!env.ENABLE_BOOKING_FLOW) {
return notFound()
}
return (

View File

@@ -0,0 +1,8 @@
.page {
background-color: var(--Base-Background-Primary-Normal);
min-height: 50dvh;
max-width: var(--max-width);
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -1,8 +1,21 @@
import { env } from "@/env/server"
import { setLang } from "@/i18n/serverContext"
import styles from "./page.module.css"
import type { LangParams, PageArgs } from "@/types/params"
export default function HotelReservationPage({ params }: PageArgs<LangParams>) {
setLang(params.lang)
return null
if (!env.ENABLE_BOOKING_WIDGET_HOTELRESERVATION_PATH) {
return null
}
return (
<div className={styles.page}>
New booking flow! Please report errors/issues in Slack.
</div>
)
}

View File

@@ -29,10 +29,6 @@ export default async function SelectHotelMapPage({
params,
searchParams,
}: PageArgs<LangParams, SelectHotelSearchParams>) {
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()
}
setLang(params.lang)
const locations = await getLocations()
@@ -93,7 +89,10 @@ export default async function SelectHotelMapPage({
const hotelPins = getHotelPins(hotels)
const filterList = getFiltersFromHotels(hotels)
const cityCoordinates = await getCityCoordinates({ city: city.name })
const cityCoordinates = await getCityCoordinates({
city: city.name,
hotel: { address: hotels[0].hotelData.address.streetAddress },
})
return (
<MapModal>

View File

@@ -1,7 +1,3 @@
import { notFound } from "next/navigation"
import { env } from "@/env/server"
import styles from "./layout.module.css"
import { LangParams, LayoutArgs } from "@/types/params"
@@ -12,9 +8,6 @@ export default function HotelReservationLayout({
}: React.PropsWithChildren<
LayoutArgs<LangParams> & { modal: React.ReactNode }
>) {
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()
}
return (
<div className={styles.layout}>
{children}

View File

@@ -72,7 +72,7 @@
.header nav {
display: block;
max-width: var(--max-width-navigation);
padding-left: 0;
padding: 0;
}
.sorter {

View File

@@ -59,8 +59,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 =
@@ -159,6 +163,7 @@ export default async function SelectHotelPage({
<div className={styles.mapContainer}>
<StaticMap
city={searchParams.city}
country={isCityWithCountry(city) ? city.country : undefined}
width={340}
height={180}
zoomLevel={11}

View File

@@ -105,7 +105,7 @@ export default async function SelectRatePage({
fromDate={fromDate.toDate()}
toDate={toDate.toDate()}
adultCount={adults}
childArray={children ?? []}
childArray={children}
/>
<Suspense key={hotelId} fallback={<RoomsContainerSkeleton />}>
@@ -115,7 +115,7 @@ export default async function SelectRatePage({
fromDate={fromDate.toDate()}
toDate={toDate.toDate()}
adultCount={adults}
childArray={children ?? []}
childArray={children}
/>
</Suspense>
<TrackingSDK pageData={pageTrackingData} hotelInfo={hotelsTrackingData} />

View File

@@ -1,5 +0,0 @@
import LoadingSpinner from "@/components/LoadingSpinner"
export default function LoadingHotelHeader() {
return <LoadingSpinner />
}

View File

@@ -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);
}
}

View File

@@ -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<LangParams, { hotel: string }>) {
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 (
<header className={styles.header}>
<div className={styles.wrapper}>
<div className={styles.titleContainer}>
<Title as="h3" level="h1">
{hotel.name}
</Title>
<address className={styles.address}>
<Caption color="textMediumContrast">
{hotel.address.streetAddress}, {hotel.address.city}
</Caption>
<div>
<Divider variant="vertical" color="subtle" />
</div>
<Caption color="textMediumContrast">
{intl.formatMessage(
{ id: "Distance in km to city centre" },
{ number: hotel.location.distanceToCentre }
)}
</Caption>
</address>
</div>
<div className={styles.dividerContainer}>
<Divider variant="vertical" color="subtle" />
</div>
<div className={styles.descriptionContainer}>
<Body color="baseTextHighContrast">
{hotel.hotelContent.texts.descriptions.short}
</Body>
</div>
</div>
</header>
)
}

View File

@@ -1,5 +0,0 @@
import LoadingSpinner from "@/components/LoadingSpinner"
export default function LoadingSummaryHeader() {
return <LoadingSpinner />
}

View File

@@ -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;
}
}

View File

@@ -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<LangParams, SearchParams<SelectRateSearchParams>>) {
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 = {
public: {
local: {
amount: availability.publicRate.localPrice.pricePerStay,
currency: availability.publicRate.localPrice.currency,
},
euro: availability.publicRate?.requestedPrice
? {
amount: availability.publicRate?.requestedPrice.pricePerStay,
currency: availability.publicRate?.requestedPrice.currency,
}
: undefined,
},
member: availability.memberRate
? {
local: {
amount: availability.memberRate.localPrice.pricePerStay,
currency: availability.memberRate.localPrice.currency,
},
euro: availability.memberRate.requestedPrice
? {
amount: availability.memberRate.requestedPrice.pricePerStay,
currency: availability.memberRate.requestedPrice.currency,
}
: undefined,
}
: undefined,
}
return (
<>
<div className={styles.mobileSummary}>
<SummaryBottomSheet>
<div className={styles.summary}>
<Summary
showMemberPrice={!!(user && availability.memberRate)}
room={{
roomType: availability.selectedRoom.roomType,
prices,
adults,
children,
rateDetails: availability.rateDetails,
cancellationText: availability.cancellationText,
packages,
}}
/>
</div>
</SummaryBottomSheet>
</div>
<div className={styles.desktopSummary}>
<div className={styles.hider} />
<div className={styles.summary}>
<Summary
showMemberPrice={!!(user && availability.memberRate)}
room={{
roomType: availability.selectedRoom.roomType,
prices,
adults,
children,
rateDetails: availability.rateDetails,
cancellationText: availability.cancellationText,
packages,
}}
/>
</div>
<div className={styles.shadow} />
</div>
</>
)
}

View File

@@ -1,9 +0,0 @@
import {
getCreditCardsSafely,
getProfileSafely,
} from "@/lib/trpc/memoizedRequests"
export function preload() {
void getProfileSafely()
void getCreditCardsSafely()
}

View File

@@ -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;
}
}

View File

@@ -1,10 +1,10 @@
"use client"
import { usePathname } from "next/navigation"
import { useEffect, useMemo, useState } from "react"
import { useEffect, useMemo } from "react"
import { Lang } from "@/constants/languages"
import { useStepsStore } from "@/stores/steps"
import { useEnterDetailsStore } from "@/stores/enter-details"
import useTrackingStore from "@/stores/tracking"
import { createSDKPageObject } from "@/utils/tracking"
@@ -24,7 +24,7 @@ type Props = {
export default function EnterDetailsTracking(props: Props) {
const { initialHotelsTrackingData, userTrackingData, lang } = props
const currentStep = useStepsStore((state) => state.currentStep)
const currentStep = useEnterDetailsStore((state) => state.currentStep)
const { getPageLoadTime, hasRun } = useTrackingStore()
const pathName = usePathname()

View File

@@ -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<LangParams> & {
hotelHeader: React.ReactNode
summary: React.ReactNode
}
>) {
setLang(params.lang)
preload()
const user = await getProfileSafely()
return (
<DetailsProvider isMember={!!user}>
<main className="enter-details-layout__layout">
{hotelHeader}
<div className={"enter-details-layout__container"}>
<div className={"enter-details-layout__content"}>{children}</div>
<aside className={"enter-details-layout__summaryContainer"}>
{summary}
</aside>
</div>
</main>
</DetailsProvider>
)
}

View File

@@ -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;
}
}

View File

@@ -1,12 +1,11 @@
import "./enterDetailsLayout.css"
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
import { notFound } from "next/navigation"
import { Suspense } from "react"
import {
getBreakfastPackages,
getCreditCardsSafely,
getHotelData,
getPackages,
getProfileSafely,
getSelectedRoomAvailability,
getUserTracking,
@@ -15,20 +14,25 @@ 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 TrackingSDK from "@/components/TrackingSDK"
import { getIntl } from "@/i18n"
import StepsProvider from "@/providers/StepsProvider"
import { setLang } from "@/i18n/serverContext"
import EnterDetailsProvider from "@/providers/EnterDetailsProvider"
import EnterDetailsTracking from "./enterDetailsTracking"
import styles from "./page.module.css"
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import {
TrackingChannelEnum,
@@ -46,61 +50,77 @@ export default async function StepPage({
params: { lang },
searchParams,
}: PageArgs<LangParams, SelectRateSearchParams & { step: StepEnum }>) {
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 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()
const userTrackingData = await getUserTracking()
if (!isValidStep(searchParams.step) || !hotelData || !roomAvailability) {
if (!hotelData || !roomAvailability) {
return notFound()
}
@@ -162,67 +182,98 @@ export default async function StepPage({
}
return (
<StepsProvider
<EnterDetailsProvider
bedTypes={roomAvailability.bedTypes}
booking={booking}
breakfastPackages={breakfastPackages}
isMember={!!user}
searchParams={searchParamsString}
packages={packages}
roomRate={{
memberRate: roomAvailability.memberRate,
publicRate: roomAvailability.publicRate,
}}
searchParamsStr={selectRoomParams.toString()}
step={searchParams.step}
user={user}
>
<section>
<HistoryStateManager />
<SelectedRoom
hotelId={hotelId}
room={roomAvailability.selectedRoom}
rateDescription={roomAvailability.cancellationText}
/>
<main>
<HotelHeader hotelData={hotelData} />
<div className={styles.container}>
<div className={styles.content}>
<section>
<HistoryStateManager />
<SelectedRoom
hotelId={hotelId}
room={roomAvailability.selectedRoom}
rateDescription={roomAvailability.cancellationText}
/>
{/* TODO: How to handle no beds found? */}
{roomAvailability.bedTypes ? (
<SectionAccordion
header={intl.formatMessage({ id: "Select bed" })}
step={StepEnum.selectBed}
label={intl.formatMessage({ id: "Request bedtype" })}
>
<BedType bedTypes={roomAvailability.bedTypes} />
</SectionAccordion>
) : null}
{/* TODO: How to handle no beds found? */}
{roomAvailability.bedTypes ? (
<SectionAccordion
header={intl.formatMessage({ id: "Select bed" })}
label={intl.formatMessage({ id: "Request bedtype" })}
step={StepEnum.selectBed}
>
<BedType bedTypes={roomAvailability.bedTypes} />
</SectionAccordion>
) : null}
{breakfastPackages?.length ? (
<SectionAccordion
header={intl.formatMessage({ id: "Food options" })}
step={StepEnum.breakfast}
label={intl.formatMessage({ id: "Select breakfast options" })}
>
<Breakfast packages={breakfastPackages} />
</SectionAccordion>
) : null}
{breakfastPackages?.length ? (
<SectionAccordion
header={intl.formatMessage({ id: "Food options" })}
label={intl.formatMessage({ id: "Select breakfast options" })}
step={StepEnum.breakfast}
>
<Breakfast packages={breakfastPackages} />
</SectionAccordion>
) : null}
<SectionAccordion
header={intl.formatMessage({ id: "Details" })}
step={StepEnum.details}
label={intl.formatMessage({ id: "Enter your details" })}
>
<Details user={user} memberPrice={memberPrice} />
</SectionAccordion>
<SectionAccordion
header={intl.formatMessage({ id: "Details" })}
step={StepEnum.details}
label={intl.formatMessage({ id: "Enter your details" })}
>
<Details user={user} memberPrice={memberPrice} />
</SectionAccordion>
<SectionAccordion
header={mustBeGuaranteed ? paymentGuarantee : payment}
step={StepEnum.payment}
label={mustBeGuaranteed ? guaranteeWithCard : selectPaymentMethod}
>
<Payment
user={user}
roomPrice={roomPrice}
otherPaymentOptions={
hotelData.data.attributes.merchantInformationData
.alternatePaymentOptions
}
savedCreditCards={savedCreditCards}
mustBeGuaranteed={mustBeGuaranteed}
/>
</SectionAccordion>
</section>
<SectionAccordion
header={mustBeGuaranteed ? paymentGuarantee : payment}
step={StepEnum.payment}
label={
mustBeGuaranteed ? guaranteeWithCard : selectPaymentMethod
}
>
<Suspense>
<Payment
user={user}
roomPrice={roomPrice}
otherPaymentOptions={
hotelData.data.attributes.merchantInformationData
.alternatePaymentOptions
}
supportedCards={
hotelData.data.attributes.merchantInformationData.cards
}
mustBeGuaranteed={mustBeGuaranteed}
/>
</Suspense>
</SectionAccordion>
</section>
</div>
<aside className={styles.summary}>
<Summary
adults={adults}
fromDate={fromDate}
hotelId={hotelId}
kids={children}
packageCodes={packageCodes}
rateCode={rateCode}
roomTypeCode={roomTypeCode}
toDate={toDate}
/>
</aside>
</div>
</main>
<EnterDetailsTracking
initialHotelsTrackingData={initialHotelsTrackingData}
userTrackingData={userTrackingData}
@@ -232,6 +283,6 @@ export default async function StepPage({
pageData={initialPageTrackingData}
hotelInfo={initialHotelsTrackingData}
/>
</StepsProvider>
</EnterDetailsProvider>
)
}

View File

@@ -0,0 +1 @@
export { default } from "../page"

View File

@@ -0,0 +1,17 @@
import { env } from "@/env/server"
import BookingWidget, { preload } from "@/components/BookingWidget"
import { PageArgs } from "@/types/params"
export default async function BookingWidgetPage({
searchParams,
}: PageArgs<{}, URLSearchParams>) {
if (!env.ENABLE_BOOKING_WIDGET_HOTELRESERVATION_PATH) {
return null
}
preload()
return <BookingWidget searchParams={searchParams} />
}

View File

@@ -3,7 +3,7 @@ import { env } from "@/env/server"
import { BookingWidgetSkeleton } from "@/components/BookingWidget/Client"
export default function LoadingBookingWidget() {
if (env.HIDE_FOR_NEXT_RELEASE) {
if (!env.ENABLE_BOOKING_FLOW) {
return null
}

View File

@@ -8,7 +8,7 @@ import { PageArgs } from "@/types/params"
export default async function BookingWidgetPage({
searchParams,
}: PageArgs<{}, URLSearchParams>) {
if (env.HIDE_FOR_NEXT_RELEASE) {
if (!env.ENABLE_BOOKING_WIDGET) {
return null
}

View File

@@ -8,7 +8,7 @@ import { setLang } from "@/i18n/serverContext"
import type { LangParams, PageArgs } from "@/types/params"
export default function SitewideAlertPage({ params }: PageArgs<LangParams>) {
if (env.HIDE_FOR_NEXT_RELEASE) {
if (!env.SHOW_SITE_WIDE_ALERT) {
return null
}

View File

@@ -63,9 +63,9 @@ export default async function RootLayout({
<ServerIntlProvider intl={{ defaultLocale, locale, messages }}>
<TrpcProvider>
<RouterTracking />
{!env.HIDE_FOR_NEXT_RELEASE && <>{sitewidealert}</>}
{sitewidealert}
{header}
{!env.HIDE_FOR_NEXT_RELEASE && <>{bookingwidget}</>}
{bookingwidget}
{children}
{footer}
<ToastHandler />