Merge branch 'master' into feature/tracking
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import CurrentLoadingSpinner from "@/components/Current/LoadingSpinner"
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
|
||||
export default function Loading() {
|
||||
if (env.HIDE_FOR_NEXT_RELEASE) {
|
||||
return <CurrentLoadingSpinner />
|
||||
}
|
||||
return <LoadingSpinner />
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
|
||||
export default function Loading() {
|
||||
return <LoadingSpinner fullPage />
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
|
||||
export default function Loading() {
|
||||
return <LoadingSpinner fullPage />
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { redirect } from "next/navigation"
|
||||
|
||||
import { getHotelData } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader"
|
||||
|
||||
import type { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
export default async function HotelHeader({
|
||||
params,
|
||||
searchParams,
|
||||
}: PageArgs<LangParams, { hotel: string }>) {
|
||||
const home = `/${params.lang}`
|
||||
if (!searchParams.hotel) {
|
||||
redirect(home)
|
||||
}
|
||||
const hotel = await getHotelData({
|
||||
hotelId: searchParams.hotel,
|
||||
language: params.lang,
|
||||
})
|
||||
if (!hotel?.data) {
|
||||
redirect(home)
|
||||
}
|
||||
return <HotelSelectionHeader hotel={hotel.data.attributes} />
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
import "./enterDetailsLayout.css"
|
||||
|
||||
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
import {
|
||||
getBreakfastPackages,
|
||||
getCreditCardsSafely,
|
||||
getHotelData,
|
||||
getProfileSafely,
|
||||
getSelectedRoomAvailability,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import BedType from "@/components/HotelReservation/EnterDetails/BedType"
|
||||
import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast"
|
||||
import Details from "@/components/HotelReservation/EnterDetails/Details"
|
||||
import HistoryStateManager from "@/components/HotelReservation/EnterDetails/HistoryStateManager"
|
||||
import Payment from "@/components/HotelReservation/EnterDetails/Payment"
|
||||
import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion"
|
||||
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
|
||||
import {
|
||||
generateChildrenString,
|
||||
getQueryParamsForEnterDetails,
|
||||
} from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
||||
import TrackingSDK from "@/components/TrackingSDK"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
|
||||
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
import {
|
||||
TrackingChannelEnum,
|
||||
TrackingSDKHotelInfo,
|
||||
TrackingSDKPageData,
|
||||
} from "@/types/components/tracking"
|
||||
import type { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
function isValidStep(step: string): step is StepEnum {
|
||||
return Object.values(StepEnum).includes(step as StepEnum)
|
||||
}
|
||||
|
||||
export default async function StepPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: PageArgs<LangParams & { step: StepEnum }, SelectRateSearchParams>) {
|
||||
const { lang } = params
|
||||
|
||||
const intl = await getIntl()
|
||||
const selectRoomParams = new URLSearchParams(searchParams)
|
||||
const {
|
||||
hotel: hotelId,
|
||||
rooms,
|
||||
fromDate,
|
||||
toDate,
|
||||
} = getQueryParamsForEnterDetails(selectRoomParams)
|
||||
|
||||
const {
|
||||
adults,
|
||||
children,
|
||||
roomTypeCode,
|
||||
rateCode,
|
||||
packages: packageCodes,
|
||||
} = rooms[0] // TODO: Handle multiple rooms
|
||||
|
||||
const childrenAsString = children && generateChildrenString(children)
|
||||
|
||||
const breakfastInput = { adults, fromDate, hotelId, toDate }
|
||||
void getBreakfastPackages(breakfastInput)
|
||||
void getSelectedRoomAvailability({
|
||||
hotelId,
|
||||
adults,
|
||||
children: childrenAsString,
|
||||
roomStayStartDate: fromDate,
|
||||
roomStayEndDate: toDate,
|
||||
rateCode,
|
||||
roomTypeCode,
|
||||
packageCodes,
|
||||
})
|
||||
|
||||
const roomAvailability = await getSelectedRoomAvailability({
|
||||
hotelId,
|
||||
adults,
|
||||
children: childrenAsString,
|
||||
roomStayStartDate: fromDate,
|
||||
roomStayEndDate: toDate,
|
||||
rateCode,
|
||||
roomTypeCode,
|
||||
packageCodes,
|
||||
})
|
||||
const hotelData = await getHotelData({
|
||||
hotelId,
|
||||
language: lang,
|
||||
isCardOnlyPayment: roomAvailability?.mustBeGuaranteed,
|
||||
})
|
||||
const breakfastPackages = await getBreakfastPackages(breakfastInput)
|
||||
const user = await getProfileSafely()
|
||||
const savedCreditCards = await getCreditCardsSafely()
|
||||
|
||||
if (!isValidStep(params.step) || !hotelData || !roomAvailability) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const mustBeGuaranteed = roomAvailability?.mustBeGuaranteed ?? false
|
||||
|
||||
const paymentGuarantee = intl.formatMessage({
|
||||
id: "Payment Guarantee",
|
||||
})
|
||||
const payment = intl.formatMessage({
|
||||
id: "Payment",
|
||||
})
|
||||
const guaranteeWithCard = intl.formatMessage({
|
||||
id: "Guarantee booking with credit card",
|
||||
})
|
||||
const selectPaymentMethod = intl.formatMessage({
|
||||
id: "Select payment method",
|
||||
})
|
||||
|
||||
const roomPrice =
|
||||
user && roomAvailability.memberRate
|
||||
? roomAvailability.memberRate?.localPrice.pricePerStay
|
||||
: roomAvailability.publicRate!.localPrice.pricePerStay
|
||||
|
||||
const arrivalDate = new Date(searchParams.fromDate)
|
||||
const departureDate = new Date(searchParams.toDate)
|
||||
const hotelAttributes = hotelData?.data.attributes
|
||||
|
||||
const pageTrackingData: TrackingSDKPageData = {
|
||||
pageId: "select-rate",
|
||||
domainLanguage: params.lang as Lang,
|
||||
channel: TrackingChannelEnum["hotelreservation"],
|
||||
pageName: "hotelreservation|select-rate",
|
||||
siteSections: "hotelreservation|select-rate",
|
||||
pageType: "bookingroomsandratespage",
|
||||
}
|
||||
|
||||
const hotelsTrackingData: TrackingSDKHotelInfo = {
|
||||
searchTerm: searchParams.city,
|
||||
arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
|
||||
departureDate: format(departureDate, "yyyy-MM-dd"),
|
||||
noOfAdults: adults,
|
||||
noOfChildren: children?.length,
|
||||
//childBedPreference // "adults|adults|extra|adults"
|
||||
noOfRooms: 1, // // TODO: Handle multiple rooms
|
||||
duration: differenceInCalendarDays(departureDate, arrivalDate),
|
||||
leadTime: differenceInCalendarDays(arrivalDate, new Date()),
|
||||
searchType: "hotel",
|
||||
bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday",
|
||||
country: hotelAttributes?.address.country,
|
||||
region: hotelAttributes?.address.city,
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<HistoryStateManager />
|
||||
<SelectedRoom
|
||||
hotelId={hotelId}
|
||||
room={roomAvailability.selectedRoom}
|
||||
rateDescription={roomAvailability.cancellationText}
|
||||
/>
|
||||
|
||||
{/* TODO: How to handle no beds found? */}
|
||||
{roomAvailability.bedTypes ? (
|
||||
<SectionAccordion
|
||||
header="Select bed"
|
||||
step={StepEnum.selectBed}
|
||||
label={intl.formatMessage({ id: "Request bedtype" })}
|
||||
>
|
||||
<BedType bedTypes={roomAvailability.bedTypes} />
|
||||
</SectionAccordion>
|
||||
) : null}
|
||||
|
||||
<SectionAccordion
|
||||
header={intl.formatMessage({ id: "Food options" })}
|
||||
step={StepEnum.breakfast}
|
||||
label={intl.formatMessage({ id: "Select breakfast options" })}
|
||||
>
|
||||
<Breakfast packages={breakfastPackages} />
|
||||
</SectionAccordion>
|
||||
<SectionAccordion
|
||||
header={intl.formatMessage({ id: "Details" })}
|
||||
step={StepEnum.details}
|
||||
label={intl.formatMessage({ id: "Enter your details" })}
|
||||
>
|
||||
<Details user={user} />
|
||||
</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>
|
||||
<TrackingSDK pageData={pageTrackingData} hotelInfo={hotelsTrackingData} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
|
||||
export default function Loading() {
|
||||
return <LoadingSpinner fullPage />
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
|
||||
export default function LoadingModal() {
|
||||
return <LoadingSpinner />
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import { MapModal } from "@/components/MapModal"
|
||||
import TrackingSDK from "@/components/TrackingSDK"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
|
||||
import { fetchAvailableHotels } from "../../utils"
|
||||
import { fetchAvailableHotels, getFiltersFromHotels } from "../../utils"
|
||||
|
||||
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
||||
import {
|
||||
@@ -92,6 +92,7 @@ export default async function SelectHotelMapPage({
|
||||
}
|
||||
|
||||
const hotelPins = getHotelPins(hotels)
|
||||
const filterList = getFiltersFromHotels(hotels)
|
||||
|
||||
return (
|
||||
<MapModal>
|
||||
@@ -100,6 +101,7 @@ export default async function SelectHotelMapPage({
|
||||
hotelPins={hotelPins}
|
||||
mapId={googleMapId}
|
||||
hotels={hotels}
|
||||
filterList={filterList}
|
||||
/>
|
||||
<TrackingSDK pageData={pageTrackingData} hotelInfo={hotelsTrackingData} />
|
||||
</MapModal>
|
||||
|
||||
@@ -20,10 +20,13 @@
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.sorter {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sideBar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 340px;
|
||||
}
|
||||
|
||||
.link {
|
||||
@@ -47,6 +50,10 @@
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.filter {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.main {
|
||||
padding: var(--Spacing-x5);
|
||||
@@ -58,6 +65,11 @@
|
||||
var(--Spacing-x5);
|
||||
}
|
||||
|
||||
.sorter {
|
||||
display: block;
|
||||
width: 339px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
@@ -65,6 +77,14 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sideBar {
|
||||
max-width: 340px;
|
||||
}
|
||||
.filter {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
padding-bottom: var(--Spacing-x6);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
getFiltersFromHotels,
|
||||
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
||||
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
|
||||
import HotelCount from "@/components/HotelReservation/SelectHotel/HotelCount"
|
||||
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
|
||||
import HotelSorter from "@/components/HotelReservation/SelectHotel/HotelSorter"
|
||||
import MobileMapButtonContainer from "@/components/HotelReservation/SelectHotel/MobileMapButtonContainer"
|
||||
@@ -22,7 +23,6 @@ import StaticMap from "@/components/Maps/StaticMap"
|
||||
import Alert from "@/components/TempDesignSystem/Alert"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Preamble from "@/components/TempDesignSystem/Text/Preamble"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import TrackingSDK from "@/components/TrackingSDK"
|
||||
import { getIntl } from "@/i18n"
|
||||
@@ -76,6 +76,8 @@ export default async function SelectHotelPage({
|
||||
|
||||
const filterList = getFiltersFromHotels(hotels)
|
||||
|
||||
const isAllUnavailable = hotels.every((hotel) => hotel.price === undefined)
|
||||
|
||||
const pageTrackingData: TrackingSDKPageData = {
|
||||
pageId: "select-hotel",
|
||||
domainLanguage: params.lang as Lang,
|
||||
@@ -107,11 +109,13 @@ export default async function SelectHotelPage({
|
||||
<div className={styles.title}>
|
||||
<div className={styles.cityInformation}>
|
||||
<Subtitle>{city.name}</Subtitle>
|
||||
<Preamble>{hotels.length} hotels</Preamble>
|
||||
<HotelCount />
|
||||
</div>
|
||||
<div className={styles.sorter}>
|
||||
<HotelSorter discreet />
|
||||
</div>
|
||||
<HotelSorter />
|
||||
</div>
|
||||
<MobileMapButtonContainer city={searchParams.city} />
|
||||
<MobileMapButtonContainer filters={filterList} />
|
||||
</header>
|
||||
<main className={styles.main}>
|
||||
<div className={styles.sideBar}>
|
||||
@@ -119,7 +123,7 @@ export default async function SelectHotelPage({
|
||||
<Link
|
||||
className={styles.link}
|
||||
color="burgundy"
|
||||
href={selectHotelMap[params.lang]}
|
||||
href={selectHotelMap(params.lang)}
|
||||
keepSearchParams
|
||||
>
|
||||
<div className={styles.mapContainer}>
|
||||
@@ -153,10 +157,10 @@ export default async function SelectHotelPage({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<HotelFilter filters={filterList} />
|
||||
<HotelFilter filters={filterList} className={styles.filter} />
|
||||
</div>
|
||||
<div className={styles.hotelList}>
|
||||
{!hotels.length && (
|
||||
{isAllUnavailable && (
|
||||
<Alert
|
||||
type={AlertTypeEnum.Info}
|
||||
heading={intl.formatMessage({ id: "No availability" })}
|
||||
|
||||
@@ -9,8 +9,6 @@ import type {
|
||||
CategorizedFilters,
|
||||
Filter,
|
||||
} from "@/types/components/hotelReservation/selectHotel/hotelFilters"
|
||||
import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
import { HotelListingEnum } from "@/types/enums/hotelListing"
|
||||
|
||||
const hotelSurroundingsFilterNames = [
|
||||
"Hotel surroundings",
|
||||
@@ -21,6 +19,15 @@ const hotelSurroundingsFilterNames = [
|
||||
"Omgivningar",
|
||||
]
|
||||
|
||||
const hotelFacilitiesFilterNames = [
|
||||
"Hotel facilities",
|
||||
"Hotellfaciliteter",
|
||||
"Hotelfaciliteter",
|
||||
"Hotel faciliteter",
|
||||
"Hotel-Infos",
|
||||
"Hotellin palvelut",
|
||||
]
|
||||
|
||||
export async function fetchAvailableHotels(
|
||||
input: AvailabilityInput
|
||||
): Promise<HotelData[]> {
|
||||
@@ -29,24 +36,8 @@ export async function fetchAvailableHotels(
|
||||
if (!availableHotels) throw new Error()
|
||||
|
||||
const language = getLang()
|
||||
const hotelMap = new Map<number, any>()
|
||||
|
||||
availableHotels.availability.forEach((hotel) => {
|
||||
const existingHotel = hotelMap.get(hotel.hotelId)
|
||||
if (existingHotel) {
|
||||
if (hotel.ratePlanSet === HotelListingEnum.RatePlanSet.PUBLIC) {
|
||||
existingHotel.bestPricePerNight.regularAmount =
|
||||
hotel.bestPricePerNight?.regularAmount
|
||||
} else if (hotel.ratePlanSet === HotelListingEnum.RatePlanSet.MEMBER) {
|
||||
existingHotel.bestPricePerNight.memberAmount =
|
||||
hotel.bestPricePerNight?.memberAmount
|
||||
}
|
||||
} else {
|
||||
hotelMap.set(hotel.hotelId, { ...hotel })
|
||||
}
|
||||
})
|
||||
|
||||
const hotels = Array.from(hotelMap.values()).map(async (hotel) => {
|
||||
const hotels = availableHotels.availability.map(async (hotel) => {
|
||||
const hotelData = await getHotelData({
|
||||
hotelId: hotel.hotelId.toString(),
|
||||
language,
|
||||
@@ -56,7 +47,7 @@ export async function fetchAvailableHotels(
|
||||
|
||||
return {
|
||||
hotelData: hotelData.data.attributes,
|
||||
price: hotel.bestPricePerNight,
|
||||
price: hotel.productType,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -70,6 +61,7 @@ export function getFiltersFromHotels(hotels: HotelData[]): CategorizedFilters {
|
||||
const filterList: Filter[] = uniqueFilterIds
|
||||
.map((filterId) => filters.find((filter) => filter.id === filterId))
|
||||
.filter((filter): filter is Filter => filter !== undefined)
|
||||
.sort((a, b) => b.sortOrder - a.sortOrder)
|
||||
|
||||
return filterList.reduce<CategorizedFilters>(
|
||||
(acc, filter) => {
|
||||
@@ -79,10 +71,13 @@ export function getFiltersFromHotels(hotels: HotelData[]): CategorizedFilters {
|
||||
surroundingsFilters: [...acc.surroundingsFilters, filter],
|
||||
}
|
||||
|
||||
return {
|
||||
facilityFilters: [...acc.facilityFilters, filter],
|
||||
surroundingsFilters: acc.surroundingsFilters,
|
||||
}
|
||||
if (filter.filter && hotelFacilitiesFilterNames.includes(filter.filter))
|
||||
return {
|
||||
facilityFilters: [...acc.facilityFilters, filter],
|
||||
surroundingsFilters: acc.surroundingsFilters,
|
||||
}
|
||||
|
||||
return acc
|
||||
},
|
||||
{ facilityFilters: [], surroundingsFilters: [] }
|
||||
)
|
||||
|
||||
@@ -150,7 +150,7 @@ export default async function SelectRatePage({
|
||||
roomsAvailability={roomsAvailability}
|
||||
roomCategories={roomCategories ?? []}
|
||||
user={user}
|
||||
packages={packages ?? []}
|
||||
availablePackages={packages ?? []}
|
||||
/>
|
||||
<TrackingSDK pageData={pageTrackingData} hotelInfo={hotelsTrackingData} />
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
|
||||
export default function LoadingSummaryHeader() {
|
||||
return <LoadingSpinner />
|
||||
}
|
||||
@@ -61,7 +61,7 @@ export default async function SummaryPage({
|
||||
if (!availability || !availability.selectedRoom) {
|
||||
console.error("No hotel or availability data", availability)
|
||||
// TODO: handle this case
|
||||
redirect(selectRate[params.lang])
|
||||
redirect(selectRate(params.lang))
|
||||
}
|
||||
|
||||
const prices =
|
||||
@@ -71,20 +71,24 @@ export default async function SummaryPage({
|
||||
price: availability.memberRate.localPrice.pricePerStay,
|
||||
currency: availability.memberRate.localPrice.currency,
|
||||
},
|
||||
euro: {
|
||||
price: availability.memberRate.requestedPrice.pricePerStay,
|
||||
currency: availability.memberRate.requestedPrice.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: {
|
||||
price: availability.publicRate.requestedPrice.pricePerStay,
|
||||
currency: availability.publicRate.requestedPrice.currency,
|
||||
},
|
||||
euro: availability.publicRate?.requestedPrice
|
||||
? {
|
||||
price: availability.publicRate?.requestedPrice.pricePerStay,
|
||||
currency: availability.publicRate?.requestedPrice.currency,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -100,6 +104,7 @@ export default async function SummaryPage({
|
||||
euroPrice: prices.euro,
|
||||
adults,
|
||||
children,
|
||||
rateDetails: availability.rateDetails,
|
||||
cancellationText: availability.cancellationText,
|
||||
packages,
|
||||
}}
|
||||
@@ -118,6 +123,7 @@ export default async function SummaryPage({
|
||||
euroPrice: prices.euro,
|
||||
adults,
|
||||
children,
|
||||
rateDetails: availability.rateDetails,
|
||||
cancellationText: availability.cancellationText,
|
||||
packages,
|
||||
}}
|
||||
@@ -1,20 +1,19 @@
|
||||
import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
import DetailsProvider from "@/providers/DetailsProvider"
|
||||
|
||||
import { preload } from "./_preload"
|
||||
|
||||
import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
|
||||
import type { LangParams, LayoutArgs } from "@/types/params"
|
||||
|
||||
export default async function StepLayout({
|
||||
summary,
|
||||
children,
|
||||
hotelHeader,
|
||||
params,
|
||||
summary,
|
||||
}: React.PropsWithChildren<
|
||||
LayoutArgs<LangParams & { step: StepEnum }> & {
|
||||
LayoutArgs<LangParams> & {
|
||||
hotelHeader: React.ReactNode
|
||||
summary: React.ReactNode
|
||||
}
|
||||
@@ -25,7 +24,7 @@ export default async function StepLayout({
|
||||
const user = await getProfileSafely()
|
||||
|
||||
return (
|
||||
<EnterDetailsProvider step={params.step} isMember={!!user}>
|
||||
<DetailsProvider isMember={!!user}>
|
||||
<main className="enter-details-layout__layout">
|
||||
{hotelHeader}
|
||||
<div className={"enter-details-layout__container"}>
|
||||
@@ -35,6 +34,6 @@ export default async function StepLayout({
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
</EnterDetailsProvider>
|
||||
</DetailsProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import "./enterDetailsLayout.css"
|
||||
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import {
|
||||
getBreakfastPackages,
|
||||
getCreditCardsSafely,
|
||||
getHotelData,
|
||||
getProfileSafely,
|
||||
getSelectedRoomAvailability,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import BedType from "@/components/HotelReservation/EnterDetails/BedType"
|
||||
import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast"
|
||||
import Details from "@/components/HotelReservation/EnterDetails/Details"
|
||||
import HistoryStateManager from "@/components/HotelReservation/EnterDetails/HistoryStateManager"
|
||||
import Payment from "@/components/HotelReservation/EnterDetails/Payment"
|
||||
import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion"
|
||||
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
|
||||
import {
|
||||
generateChildrenString,
|
||||
getQueryParamsForEnterDetails,
|
||||
} from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
||||
import { getIntl } from "@/i18n"
|
||||
import StepsProvider from "@/providers/StepsProvider"
|
||||
|
||||
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
import { StepEnum } from "@/types/enums/step"
|
||||
import type { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
function isValidStep(step: string): step is StepEnum {
|
||||
return Object.values(StepEnum).includes(step as StepEnum)
|
||||
}
|
||||
|
||||
export default async function StepPage({
|
||||
params: { lang },
|
||||
searchParams,
|
||||
}: PageArgs<LangParams, SelectRateSearchParams & { step: StepEnum }>) {
|
||||
const intl = await getIntl()
|
||||
const selectRoomParams = new URLSearchParams(searchParams)
|
||||
selectRoomParams.delete("step")
|
||||
const searchParamsString = selectRoomParams.toString()
|
||||
const {
|
||||
hotel: hotelId,
|
||||
rooms,
|
||||
fromDate,
|
||||
toDate,
|
||||
} = getQueryParamsForEnterDetails(selectRoomParams)
|
||||
|
||||
const {
|
||||
adults,
|
||||
children,
|
||||
roomTypeCode,
|
||||
rateCode,
|
||||
packages: packageCodes,
|
||||
} = rooms[0] // TODO: Handle multiple rooms
|
||||
|
||||
const childrenAsString = children && generateChildrenString(children)
|
||||
|
||||
const breakfastInput = { adults, fromDate, hotelId, toDate }
|
||||
void getBreakfastPackages(breakfastInput)
|
||||
void getSelectedRoomAvailability({
|
||||
hotelId,
|
||||
adults,
|
||||
children: childrenAsString,
|
||||
roomStayStartDate: fromDate,
|
||||
roomStayEndDate: toDate,
|
||||
rateCode,
|
||||
roomTypeCode,
|
||||
packageCodes,
|
||||
})
|
||||
|
||||
const roomAvailability = await getSelectedRoomAvailability({
|
||||
hotelId,
|
||||
adults,
|
||||
children: childrenAsString,
|
||||
roomStayStartDate: fromDate,
|
||||
roomStayEndDate: toDate,
|
||||
rateCode,
|
||||
roomTypeCode,
|
||||
packageCodes,
|
||||
})
|
||||
const hotelData = await getHotelData({
|
||||
hotelId,
|
||||
language: lang,
|
||||
isCardOnlyPayment: roomAvailability?.mustBeGuaranteed,
|
||||
})
|
||||
const breakfastPackages = await getBreakfastPackages(breakfastInput)
|
||||
const user = await getProfileSafely()
|
||||
const savedCreditCards = await getCreditCardsSafely()
|
||||
|
||||
if (!isValidStep(searchParams.step) || !hotelData || !roomAvailability) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const mustBeGuaranteed = roomAvailability?.mustBeGuaranteed ?? false
|
||||
|
||||
const paymentGuarantee = intl.formatMessage({
|
||||
id: "Payment Guarantee",
|
||||
})
|
||||
const payment = intl.formatMessage({
|
||||
id: "Payment",
|
||||
})
|
||||
const guaranteeWithCard = intl.formatMessage({
|
||||
id: "Guarantee booking with credit card",
|
||||
})
|
||||
const selectPaymentMethod = intl.formatMessage({
|
||||
id: "Select payment method",
|
||||
})
|
||||
|
||||
const roomPrice = {
|
||||
memberPrice: roomAvailability.memberRate?.localPrice.pricePerStay,
|
||||
publicPrice: roomAvailability.publicRate!.localPrice.pricePerStay,
|
||||
}
|
||||
|
||||
const memberPrice = roomAvailability.memberRate
|
||||
? {
|
||||
price: roomAvailability.memberRate.localPrice.pricePerStay,
|
||||
currency: roomAvailability.memberRate.localPrice.currency,
|
||||
}
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<StepsProvider
|
||||
bedTypes={roomAvailability.bedTypes}
|
||||
breakfastPackages={breakfastPackages}
|
||||
isMember={!!user}
|
||||
searchParams={searchParamsString}
|
||||
step={searchParams.step}
|
||||
>
|
||||
<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}
|
||||
|
||||
{breakfastPackages?.length ? (
|
||||
<SectionAccordion
|
||||
header={intl.formatMessage({ id: "Food options" })}
|
||||
step={StepEnum.breakfast}
|
||||
label={intl.formatMessage({ id: "Select breakfast options" })}
|
||||
>
|
||||
<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={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>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../page"
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./page"
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../page"
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../page"
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../page"
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../page"
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./page"
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../page"
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../page"
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../page"
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./page"
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../page"
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../page"
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../page"
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./page"
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../page"
|
||||
@@ -13,7 +13,7 @@ export default function SitewideAlertPage({ params }: PageArgs<LangParams>) {
|
||||
}
|
||||
|
||||
setLang(params.lang)
|
||||
preload()
|
||||
void preload()
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
|
||||
import { BOOKING_CONFIRMATION_NUMBER } from "@/constants/booking"
|
||||
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(
|
||||
@@ -22,7 +26,7 @@ export async function GET(
|
||||
const confirmationNumber = queryParams.get(BOOKING_CONFIRMATION_NUMBER)
|
||||
|
||||
if (status === "success" && confirmationNumber) {
|
||||
const confirmationUrl = new URL(`${publicURL}/${bookingConfirmation[lang]}`)
|
||||
const confirmationUrl = new URL(`${publicURL}/${bookingConfirmation(lang)}`)
|
||||
confirmationUrl.searchParams.set(
|
||||
BOOKING_CONFIRMATION_NUMBER,
|
||||
confirmationNumber
|
||||
@@ -32,15 +36,38 @@ export async function GET(
|
||||
return NextResponse.redirect(confirmationUrl)
|
||||
}
|
||||
|
||||
const returnUrl = new URL(`${publicURL}/${payment[lang]}`)
|
||||
const returnUrl = new URL(`${publicURL}/${payment(lang)}`)
|
||||
returnUrl.search = queryParams.toString()
|
||||
|
||||
if (status === "cancel") {
|
||||
returnUrl.searchParams.set("cancel", "true")
|
||||
}
|
||||
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 === "error") {
|
||||
returnUrl.searchParams.set("error", "true")
|
||||
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}`)
|
||||
|
||||
Reference in New Issue
Block a user