Merge branch 'master' into feat/sw-929-release-preps

This commit is contained in:
Linus Flood
2024-11-27 19:13:21 +01:00
310 changed files with 7543 additions and 4417 deletions

View File

@@ -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"

View File

@@ -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,

View File

@@ -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({
</section>
</section>
{/* TODO: Waiting on new API stuff
<Surprises />
*/}
<Surprises />
</div>
)
}

View File

@@ -1,5 +1,5 @@
import ProfilePage from "../page"
export { generateMetadataAccountPage as generateMetadata } from "@/utils/generateMetadata"
export { generateMetadata } from "@/utils/generateMetadata"
export default ProfilePage

View File

@@ -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<LangParams>) {
setLang(params.lang)

View File

@@ -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"

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 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<LangParams, { confirmationNumber: string }>) {
setLang(params.lang)
const confirmationNumber = searchParams.confirmationNumber
void getBookingConfirmation(confirmationNumber)
void getBookingConfirmation(searchParams.confirmationNumber)
return (
<main className={styles.main}>
<BookingConfirmation confirmationNumber={confirmationNumber} />
</main>
<div className={styles.main}>
<Header confirmationNumber={searchParams.confirmationNumber} />
<Rooms />
<Summary />
</div>
)
}

View File

@@ -0,0 +1,3 @@
.layout {
background-color: var(--Base-Background-Primary-Normal);
}

View File

@@ -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<LayoutArgs<LangParams>>) {
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()
}
return <div className={styles.layout}>{children}</div>
}

View File

@@ -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 (
<PaymentCallback
returnUrl={returnUrl.toString()}
searchObject={searchObject}
/>
)
}

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

@@ -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 (
<MapModal>
@@ -67,6 +71,7 @@ export default async function SelectHotelMapPage({
mapId={googleMapId}
hotels={hotels}
filterList={filterList}
cityCoordinates={cityCoordinates}
/>
</MapModal>
)

View File

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

View File

@@ -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 (
<>
<header className={styles.header}>
<Suspense fallback={<BreadcrumbsSkeleton />}>
<Breadcrumbs breadcrumbs={breadcrumbs} />
</Suspense>
<div className={styles.title}>
<div className={styles.cityInformation}>
<Subtitle>{city.name}</Subtitle>
@@ -94,6 +128,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

@@ -0,0 +1,55 @@
import { beforeAll, describe, expect, it } from "@jest/globals"
import { getValidFromDate, getValidToDate } from "./getValidDates"
const NOW = new Date("2020-10-01T00:00:00Z")
let originalTz: string | undefined
describe("getValidFromDate", () => {
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")
})
})
})

View File

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

View File

@@ -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 (
<>
<HotelInfoCard hotelData={hotelData} noAvailability={noRoomsAvailable} />
<Rooms
roomsAvailability={roomsAvailability}
roomCategories={roomCategories ?? []}
user={user}
packages={packages ?? []}
<HotelInfoCard
hotelId={hotelId}
lang={params.lang}
fromDate={fromDate.toDate()}
toDate={toDate.toDate()}
adultCount={adults}
childArray={children}
/>
<Suspense key={hotelId} fallback={<RoomsContainerSkeleton />}>
<RoomsContainer
hotelId={hotelId}
lang={params.lang}
fromDate={fromDate.toDate()}
toDate={toDate.toDate()}
adultCount={adults}
childArray={children}
/>
</Suspense>
</>
)
}

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 =
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 (
<>
<div className={styles.mobileSummary}>
<SummaryBottomSheet>
<div className={styles.summary}>
<Summary
showMemberPrice={!!(user && availability.memberRate)}
room={{
roomType: availability.selectedRoom.roomType,
localPrice: prices.local,
euroPrice: prices.euro,
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,
localPrice: prices.local,
euroPrice: prices.euro,
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,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,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<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 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 (
<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
roomPrice={roomPrice}
otherPaymentOptions={
hotelData.data.attributes.merchantInformationData
.alternatePaymentOptions
}
savedCreditCards={savedCreditCards}
mustBeGuaranteed={mustBeGuaranteed}
/>
</SectionAccordion>
</section>
</StepsProvider>
<SectionAccordion
header={mustBeGuaranteed ? paymentGuarantee : payment}
step={StepEnum.payment}
label={
mustBeGuaranteed ? guaranteeWithCard : selectPaymentMethod
}
>
<Suspense>
<Payment
user={user}
roomPrice={roomPrice}
otherPaymentOptions={
hotelData.data.attributes.merchantInformationData
.alternatePaymentOptions
}
savedCreditCards={savedCreditCards}
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>
</EnterDetailsProvider>
)
}

View File

@@ -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}
<ToastHandler />
<TokenRefresher />
<StorageCleaner />
</TrpcProvider>
</ServerIntlProvider>
</body>