Merge branch 'master' into feature/tracking

This commit is contained in:
Linus Flood
2024-11-25 10:14:12 +01:00
181 changed files with 3840 additions and 1723 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

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

@@ -3,7 +3,7 @@ import { notFound } from "next/navigation"
import { Lang } from "@/constants/languages"
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"
@@ -93,6 +93,7 @@ export default async function SelectHotelMapPage({
const hotelPins = getHotelPins(hotels)
const filterList = getFiltersFromHotels(hotels)
const cityCoordinates = await getCityCoordinates({ city: city.name })
return (
<MapModal>
@@ -102,6 +103,7 @@ export default async function SelectHotelMapPage({
mapId={googleMapId}
hotels={hotels}
filterList={filterList}
cityCoordinates={cityCoordinates}
/>
<TrackingSDK pageData={pageTrackingData} hotelInfo={hotelsTrackingData} />
</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,8 +1,12 @@
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
import { notFound } from "next/navigation"
import { Suspense } from "react"
import { Lang } from "@/constants/languages"
import { selectHotelMap } from "@/constants/routes/hotelReservation"
import {
selectHotel,
selectHotelMap,
} from "@/constants/routes/hotelReservation"
import { getLocations } from "@/lib/trpc/memoizedRequests"
import {
@@ -21,6 +25,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"
@@ -75,6 +81,27 @@ export default async function SelectHotelPage({
const departureDate = new Date(searchParams.toDate)
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)
@@ -106,6 +133,9 @@ export default async function SelectHotelPage({
return (
<>
<header className={styles.header}>
<Suspense fallback={<BreadcrumbsSkeleton />}>
<Breadcrumbs breadcrumbs={breadcrumbs} />
</Suspense>
<div className={styles.title}>
<div className={styles.cityInformation}>
<Subtitle>{city.name}</Subtitle>

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,25 +1,20 @@
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
import { notFound } from "next/navigation"
import { Suspense } from "react"
import { Lang } from "@/constants/languages"
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 TrackingSDK from "@/components/TrackingSDK"
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 {
TrackingChannelEnum,
@@ -53,58 +48,27 @@ 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 { fromDate, toDate } = getValidDates(
searchParams.fromDate,
searchParams.toDate
)
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 children = selectRoomParamsObject.room[0].child // 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(),
])
const [hotelData, hotelDataError] = await safeTry(
getHotelData({ hotelId: searchParams.hotel, language: params.lang })
)
if (!hotelData && !hotelDataError) {
return notFound()
}
const arrivalDate = new Date(searchParams.fromDate)
const departureDate = new Date(searchParams.toDate)
const hotelAttributes = hotelData?.data.attributes
const roomCategories = hotelData?.included
const noRoomsAvailable = roomsAvailability?.roomConfigurations.reduce(
(acc, room) => {
return acc && room.status === "NotAvailable"
},
true
)
const pageTrackingData: TrackingSDKPageData = {
pageId: "select-rate",
domainLanguage: params.lang as Lang,
@@ -119,7 +83,7 @@ export default async function SelectRatePage({
arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
departureDate: format(departureDate, "yyyy-MM-dd"),
noOfAdults: adults,
noOfChildren: childrenCount,
noOfChildren: children?.length,
//childBedPreference // "adults|adults|extra|adults"
noOfRooms: 1, // // TODO: Handle multiple rooms
duration: differenceInCalendarDays(departureDate, arrivalDate),
@@ -132,26 +96,28 @@ export default async function SelectRatePage({
//lowestRoomPrice:
}
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 hotelId = +searchParams.hotel
return (
<>
<HotelInfoCard
hotelData={hotelData}
noAvailability={!!noRoomsAvailable}
/>
<Rooms
roomsAvailability={roomsAvailability}
roomCategories={roomCategories ?? []}
user={user}
availablePackages={packages ?? []}
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>
<TrackingSDK pageData={pageTrackingData} hotelInfo={hotelsTrackingData} />
</>
)

View File

@@ -64,32 +64,34 @@ export default async function SummaryPage({
redirect(selectRate(params.lang))
}
const prices =
user && availability.memberRate
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: {
price: availability.memberRate.localPrice.pricePerStay,
amount: availability.memberRate.localPrice.pricePerStay,
currency: availability.memberRate.localPrice.currency,
},
euro: availability.memberRate.requestedPrice
? {
price: availability.memberRate.requestedPrice.pricePerStay,
amount: 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,
}
: undefined,
}
return (
<>
@@ -100,8 +102,7 @@ export default async function SummaryPage({
showMemberPrice={!!(user && availability.memberRate)}
room={{
roomType: availability.selectedRoom.roomType,
localPrice: prices.local,
euroPrice: prices.euro,
prices,
adults,
children,
rateDetails: availability.rateDetails,
@@ -119,8 +120,7 @@ export default async function SummaryPage({
showMemberPrice={!!(user && availability.memberRate)}
room={{
roomType: availability.selectedRoom.roomType,
localPrice: prices.local,
euroPrice: prices.euro,
prices,
adults,
children,
rateDetails: availability.rateDetails,

View File

@@ -171,6 +171,7 @@ export default async function StepPage({
label={mustBeGuaranteed ? guaranteeWithCard : selectPaymentMethod}
>
<Payment
user={user}
roomPrice={roomPrice}
otherPaymentOptions={
hotelData.data.attributes.merchantInformationData

View File

@@ -9,6 +9,7 @@ import TrpcProvider from "@/lib/trpc/Provider"
import TokenRefresher from "@/components/Auth/TokenRefresher"
import CookieBotConsent from "@/components/CookieBot"
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 AdobeSDKScript from "@/components/TrackingSDK/AdobeSDKScript"
@@ -69,6 +70,7 @@ export default async function RootLayout({
{footer}
<ToastHandler />
<TokenRefresher />
<StorageCleaner />
<CookieBotConsent />
</TrpcProvider>
</ServerIntlProvider>