Merge branch 'master' into feature/tracking
This commit is contained in:
@@ -18,7 +18,7 @@ export default async function MyPages({
|
||||
setLang(params.lang)
|
||||
|
||||
const accountPageRes = await serverClient().contentstack.accountPage.get()
|
||||
const { formatMessage } = await getIntl()
|
||||
const intl = await getIntl()
|
||||
|
||||
if (!accountPageRes) {
|
||||
return null
|
||||
@@ -33,7 +33,7 @@ export default async function MyPages({
|
||||
{accountPage.content?.length ? (
|
||||
<Blocks blocks={accountPage.content} />
|
||||
) : (
|
||||
<p>{formatMessage({ id: "No content published" })}</p>
|
||||
<p>{intl.formatMessage({ id: "No content published" })}</p>
|
||||
)}
|
||||
</main>
|
||||
<TrackingSDK pageData={tracking} />
|
||||
|
||||
@@ -13,15 +13,15 @@ export default async function CommunicationSlot({
|
||||
}: PageArgs<LangParams>) {
|
||||
setLang(params.lang)
|
||||
|
||||
const { formatMessage } = await getIntl()
|
||||
const intl = await getIntl()
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<article className={styles.content}>
|
||||
<Subtitle type="two" color="black">
|
||||
{formatMessage({ id: "My communication preferences" })}
|
||||
{intl.formatMessage({ id: "My communication preferences" })}
|
||||
</Subtitle>
|
||||
<Body color="black">
|
||||
{formatMessage({
|
||||
{intl.formatMessage({
|
||||
id: "Tell us what information and updates you'd like to receive, and how, by clicking the link below.",
|
||||
})}
|
||||
</Body>
|
||||
|
||||
@@ -13,17 +13,17 @@ import { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
export default async function CreditCardSlot({ params }: PageArgs<LangParams>) {
|
||||
setLang(params.lang)
|
||||
const { formatMessage } = await getIntl()
|
||||
const intl = await getIntl()
|
||||
const creditCards = await serverClient().user.creditCards()
|
||||
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<article className={styles.content}>
|
||||
<Subtitle type="two" color="black">
|
||||
{formatMessage({ id: "My payment cards" })}
|
||||
{intl.formatMessage({ id: "My payment cards" })}
|
||||
</Subtitle>
|
||||
<Body color="black">
|
||||
{formatMessage({
|
||||
{intl.formatMessage({
|
||||
id: "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.",
|
||||
})}
|
||||
</Body>
|
||||
|
||||
@@ -15,14 +15,14 @@ export default async function MembershipCardSlot({
|
||||
params,
|
||||
}: PageArgs<LangParams>) {
|
||||
setLang(params.lang)
|
||||
const { formatMessage } = await getIntl()
|
||||
const intl = await getIntl()
|
||||
const membershipCards = await getMembershipCards()
|
||||
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<article className={styles.content}>
|
||||
<Subtitle color="black">
|
||||
{formatMessage({ id: "My membership cards" })}
|
||||
{intl.formatMessage({ id: "My membership cards" })}
|
||||
</Subtitle>
|
||||
</article>
|
||||
{membershipCards &&
|
||||
@@ -41,7 +41,7 @@ export default async function MembershipCardSlot({
|
||||
<Link href="#" variant="icon">
|
||||
<PlusCircleIcon color="burgundy" />
|
||||
<Body color="burgundy" textTransform="underlined">
|
||||
{formatMessage({ id: "Add new card" })}
|
||||
{intl.formatMessage({ id: "Add new card" })}
|
||||
</Body>
|
||||
</Link>
|
||||
</section>
|
||||
|
||||
@@ -24,7 +24,7 @@ import { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
export default async function Profile({ params }: PageArgs<LangParams>) {
|
||||
setLang(params.lang)
|
||||
const { formatMessage } = await getIntl()
|
||||
const intl = await getIntl()
|
||||
const user = await getProfile()
|
||||
if (!user || "error" in user) {
|
||||
return null
|
||||
@@ -37,7 +37,7 @@ export default async function Profile({ params }: PageArgs<LangParams>) {
|
||||
<Header>
|
||||
<hgroup>
|
||||
<Title as="h4" color="red" level="h1" textTransform="capitalize">
|
||||
{formatMessage({ id: "Welcome" })}
|
||||
{intl.formatMessage({ id: "Welcome" })}
|
||||
</Title>
|
||||
<Title as="h4" color="burgundy" level="h2" textTransform="capitalize">
|
||||
{user.name}
|
||||
@@ -45,7 +45,7 @@ export default async function Profile({ params }: PageArgs<LangParams>) {
|
||||
</hgroup>
|
||||
<Button asChild intent="primary" size="small" theme="base">
|
||||
<Link prefetch={false} color="none" href={profileEdit[params.lang]}>
|
||||
{formatMessage({ id: "Edit profile" })}
|
||||
{intl.formatMessage({ id: "Edit profile" })}
|
||||
</Link>
|
||||
</Button>
|
||||
</Header>
|
||||
@@ -54,35 +54,35 @@ export default async function Profile({ params }: PageArgs<LangParams>) {
|
||||
<div className={styles.item}>
|
||||
<CalendarIcon color="burgundy" />
|
||||
<Body color="burgundy" textTransform="bold">
|
||||
{formatMessage({ id: "Date of Birth" })}
|
||||
{intl.formatMessage({ id: "Date of Birth" })}
|
||||
</Body>
|
||||
<Body color="burgundy">{user.dateOfBirth}</Body>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<PhoneIcon color="burgundy" />
|
||||
<Body color="burgundy" textTransform="bold">
|
||||
{formatMessage({ id: "Phone number" })}
|
||||
{intl.formatMessage({ id: "Phone number" })}
|
||||
</Body>
|
||||
<Body color="burgundy">{user.phoneNumber}</Body>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<GlobeIcon color="burgundy" />
|
||||
<Body color="burgundy" textTransform="bold">
|
||||
{formatMessage({ id: "Language" })}
|
||||
{intl.formatMessage({ id: "Language" })}
|
||||
</Body>
|
||||
<Body color="burgundy">{language?.label ?? defaultLanguage}</Body>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<EmailIcon color="burgundy" />
|
||||
<Body color="burgundy" textTransform="bold">
|
||||
{formatMessage({ id: "Email" })}
|
||||
{intl.formatMessage({ id: "Email" })}
|
||||
</Body>
|
||||
<Body color="burgundy">{user.email}</Body>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<LocationIcon color="burgundy" />
|
||||
<Body color="burgundy" textTransform="bold">
|
||||
{formatMessage({ id: "Address" })}
|
||||
{intl.formatMessage({ id: "Address" })}
|
||||
</Body>
|
||||
<Body color="burgundy">
|
||||
{user.address.streetAddress
|
||||
@@ -100,7 +100,7 @@ export default async function Profile({ params }: PageArgs<LangParams>) {
|
||||
<div className={styles.item}>
|
||||
<LockIcon color="burgundy" />
|
||||
<Body color="burgundy" textTransform="bold">
|
||||
{formatMessage({ id: "Password" })}
|
||||
{intl.formatMessage({ id: "Password" })}
|
||||
</Body>
|
||||
<Body color="burgundy">**********</Body>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
.profile-layout {
|
||||
background-color: var(--Main-Grey-White);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { setPreviewData } from "@/lib/previewContext"
|
||||
|
||||
import InitLivePreview from "@/components/LivePreview"
|
||||
|
||||
import { PageArgs, UIDParams } from "@/types/params"
|
||||
|
||||
export default function PreviewPage({
|
||||
searchParams,
|
||||
params,
|
||||
}: PageArgs<UIDParams, URLSearchParams>) {
|
||||
const shouldInitializePreview = searchParams.isPreview === "true"
|
||||
|
||||
if (searchParams.live_preview) {
|
||||
setPreviewData({ hash: searchParams.live_preview, uid: params.uid })
|
||||
}
|
||||
|
||||
return shouldInitializePreview ? <InitLivePreview /> : null
|
||||
}
|
||||
@@ -9,15 +9,18 @@ import {
|
||||
|
||||
export default function ContentTypeLayout({
|
||||
breadcrumbs,
|
||||
preview,
|
||||
children,
|
||||
}: React.PropsWithChildren<
|
||||
LayoutArgs<LangParams & ContentTypeParams & UIDParams> & {
|
||||
breadcrumbs: React.ReactNode
|
||||
preview: React.ReactNode
|
||||
}
|
||||
>) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<section className={styles.layout}>
|
||||
{preview}
|
||||
{breadcrumbs}
|
||||
{children}
|
||||
</section>
|
||||
|
||||
@@ -1,157 +1,7 @@
|
||||
.details,
|
||||
.guest,
|
||||
.header,
|
||||
.hgroup,
|
||||
.hotel,
|
||||
.list,
|
||||
.main,
|
||||
.section,
|
||||
.receipt,
|
||||
.total {
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main {
|
||||
gap: var(--Spacing-x5);
|
||||
margin: 0 auto;
|
||||
width: min(calc(100dvw - (var(--Spacing-x3) * 2)), 708px);
|
||||
}
|
||||
|
||||
.header,
|
||||
.hgroup {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.hgroup {
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.body {
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x9);
|
||||
}
|
||||
|
||||
.booking {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
grid-template-areas:
|
||||
"image"
|
||||
"details"
|
||||
"actions";
|
||||
}
|
||||
|
||||
.actions,
|
||||
.details {
|
||||
background-color: var(--Base-Surface-Subtle-Normal);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
}
|
||||
|
||||
.details {
|
||||
gap: var(--Spacing-x3);
|
||||
grid-area: details;
|
||||
padding: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.tempImage {
|
||||
align-items: center;
|
||||
background-color: lightgrey;
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
display: flex;
|
||||
grid-area: image;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: grid;
|
||||
grid-area: actions;
|
||||
padding: var(--Spacing-x1) var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.list {
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.listItem {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.summary {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.guest,
|
||||
.hotel {
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.receipt,
|
||||
.total {
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.divider {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.actions {
|
||||
& > button[class*="btn"][class*="icon"][class*="small"] {
|
||||
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||
border-radius: 0;
|
||||
justify-content: space-between;
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
& > svg {
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tempImage {
|
||||
min-height: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.booking {
|
||||
grid-template-areas:
|
||||
"details image"
|
||||
"actions actions";
|
||||
grid-template-columns: 1fr minmax(256px, min(256px, 100%));
|
||||
}
|
||||
|
||||
.actions {
|
||||
gap: var(--Spacing-x7);
|
||||
grid-template-columns: repeat(auto-fit, minmax(50px, auto));
|
||||
justify-content: center;
|
||||
padding: var(--Spacing-x1) var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.details {
|
||||
padding: var(--Spacing-x3) var(--Spacing-x3) var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.summary {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
width: min(calc(100dvw - (var(--Spacing-x3) * 2)), 948px);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,7 @@
|
||||
import { dt } from "@/lib/dt"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import {
|
||||
CalendarIcon,
|
||||
DownloadIcon,
|
||||
ImageIcon,
|
||||
PrinterIcon,
|
||||
} from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { getIntl } from "@/i18n"
|
||||
import BookingConfirmation from "@/components/HotelReservation/BookingConfirmation"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
|
||||
import styles from "./page.module.css"
|
||||
|
||||
@@ -24,269 +11,12 @@ export default async function BookingConfirmationPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: PageArgs<LangParams, { confirmationNumber: string }>) {
|
||||
setLang(params.lang)
|
||||
const confirmationNumber = searchParams.confirmationNumber
|
||||
const booking = await serverClient().booking.confirmation({
|
||||
confirmationNumber,
|
||||
})
|
||||
|
||||
if (!booking) {
|
||||
return null
|
||||
}
|
||||
|
||||
const intl = await getIntl()
|
||||
const text = intl.formatMessage<React.ReactNode>(
|
||||
{ id: "booking.confirmation.text" },
|
||||
{
|
||||
emailLink: (str) => (
|
||||
<Link color="burgundy" href="#" textDecoration="underline">
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
const fromDate = dt(booking.checkInDate).locale(params.lang)
|
||||
const toDate = dt(booking.checkOutDate).locale(params.lang)
|
||||
const nights = intl.formatMessage(
|
||||
{ id: "booking.nights" },
|
||||
{
|
||||
totalNights: dt(toDate.format("YYYY-MM-DD")).diff(
|
||||
dt(fromDate.format("YYYY-MM-DD")),
|
||||
"days"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
void getBookingConfirmation(confirmationNumber)
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<header className={styles.header}>
|
||||
<hgroup className={styles.hgroup}>
|
||||
<Title
|
||||
as="h4"
|
||||
color="red"
|
||||
textAlign="center"
|
||||
textTransform="regular"
|
||||
type="h2"
|
||||
>
|
||||
{intl.formatMessage({ id: "booking.confirmation.title" })}
|
||||
</Title>
|
||||
<Title
|
||||
as="h4"
|
||||
color="burgundy"
|
||||
textAlign="center"
|
||||
textTransform="regular"
|
||||
type="h1"
|
||||
>
|
||||
{booking.hotel?.data.attributes.name}
|
||||
</Title>
|
||||
</hgroup>
|
||||
<Body className={styles.body} textAlign="center">
|
||||
{text}
|
||||
</Body>
|
||||
</header>
|
||||
<section className={styles.section}>
|
||||
<div className={styles.booking}>
|
||||
<article className={styles.details}>
|
||||
<header>
|
||||
<Subtitle color="burgundy" type="two">
|
||||
{intl.formatMessage(
|
||||
{ id: "Reference #{bookingNr}" },
|
||||
{ bookingNr: booking.confirmationNumber }
|
||||
)}
|
||||
</Subtitle>
|
||||
</header>
|
||||
<ul className={styles.list}>
|
||||
<li className={styles.listItem}>
|
||||
<Body>{intl.formatMessage({ id: "Check-in" })}</Body>
|
||||
<Body>
|
||||
{`${fromDate.format("ddd, D MMM")} ${intl.formatMessage({ id: "from" })} ${fromDate.format("HH:mm")}`}
|
||||
</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body>{intl.formatMessage({ id: "Check-out" })}</Body>
|
||||
<Body>
|
||||
{`${toDate.format("ddd, D MMM")} ${intl.formatMessage({ id: "from" })} ${toDate.format("HH:mm")}`}
|
||||
</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body>{intl.formatMessage({ id: "Breakfast" })}</Body>
|
||||
<Body>
|
||||
{booking.temp.breakfastFrom} - {booking.temp.breakfastTo}
|
||||
</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body>{intl.formatMessage({ id: "Cancellation policy" })}</Body>
|
||||
<Body>
|
||||
{intl.formatMessage({ id: booking.temp.cancelPolicy })}
|
||||
</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body>{intl.formatMessage({ id: "Rebooking" })}</Body>
|
||||
<Body>{`${intl.formatMessage({ id: "Free until" })} ${fromDate.subtract(3, "day").format("ddd, D MMM")}`}</Body>
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
<aside className={styles.tempImage}>
|
||||
<ImageIcon height={80} width={80} />
|
||||
</aside>
|
||||
<div className={styles.actions}>
|
||||
<Button
|
||||
intent="text"
|
||||
size="small"
|
||||
theme="base"
|
||||
variant="icon"
|
||||
wrapping
|
||||
>
|
||||
<CalendarIcon />
|
||||
{intl.formatMessage({ id: "Add to calendar" })}
|
||||
</Button>
|
||||
<Button
|
||||
intent="text"
|
||||
size="small"
|
||||
theme="base"
|
||||
variant="icon"
|
||||
wrapping
|
||||
>
|
||||
<PrinterIcon />
|
||||
{intl.formatMessage({ id: "Print confirmation" })}
|
||||
</Button>
|
||||
<Button
|
||||
intent="text"
|
||||
size="small"
|
||||
theme="base"
|
||||
variant="icon"
|
||||
wrapping
|
||||
>
|
||||
<DownloadIcon />
|
||||
{intl.formatMessage({ id: "Download invoice" })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.summary}>
|
||||
<div className={styles.guest}>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Guest" })}
|
||||
</Caption>
|
||||
<div>
|
||||
<Body color="burgundy" textTransform="bold">
|
||||
{`${booking.guest.firstName} ${booking.guest.lastName}${booking.guest.memberbershipNumber ? ` (${intl.formatMessage({ id: "member no" })} ${booking.guest.memberbershipNumber})` : ""}`}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{booking.guest.email}</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{booking.guest.phoneNumber}
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.hotel}>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Your hotel" })}
|
||||
</Caption>
|
||||
<div>
|
||||
<Body color="burgundy" textTransform="bold">
|
||||
{booking.hotel?.data.attributes.name}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{booking.hotel?.data.attributes.contactInformation.email}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{booking.hotel?.data.attributes.contactInformation.phoneNumber}
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
<Divider className={styles.divider} color="primaryLightSubtle" />
|
||||
<div className={styles.receipt}>
|
||||
<div>
|
||||
<Body color="burgundy" textTransform="bold">
|
||||
{`${booking.temp.room.type}, ${nights}`}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{booking.temp.room.price}</Body>
|
||||
</div>
|
||||
{booking.temp.packages.map((pkg) => (
|
||||
<div key={pkg.name}>
|
||||
<Body color="burgundy" textTransform="bold">
|
||||
{pkg.name}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{pkg.price}</Body>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.total}>
|
||||
<div>
|
||||
<Body color="burgundy" textTransform="bold">
|
||||
{intl.formatMessage({ id: "VAT" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{booking.temp.room.vat}</Body>
|
||||
</div>
|
||||
<div>
|
||||
<Body color="burgundy" textTransform="bold">
|
||||
{intl.formatMessage({ id: "Total cost" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{" "}
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{
|
||||
amount: intl.formatNumber(booking.totalPrice),
|
||||
currency: booking.currencyCode,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{`${intl.formatMessage({ id: "Approx." })} ${booking.temp.totalInEuro}`}
|
||||
</Caption>
|
||||
</div>
|
||||
</div>
|
||||
<Divider className={styles.divider} color="primaryLightSubtle" />
|
||||
<div>
|
||||
<Body color="burgundy" textTransform="bold">
|
||||
{`${intl.formatMessage({ id: "Payment received" })} ${dt(booking.temp.payment).locale(params.lang).format("D MMM YYYY, h:mm z")}`}
|
||||
</Body>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{intl.formatMessage(
|
||||
{ id: "{card} ending with {cardno}" },
|
||||
{
|
||||
card: "Mastercard",
|
||||
cardno: "2202",
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<BookingConfirmation confirmationNumber={confirmationNumber} />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
// const { email, hotel, stay, summary } = tempConfirmationData
|
||||
|
||||
// const confirmationNumber = useMemo(() => {
|
||||
// if (typeof window === "undefined") return ""
|
||||
|
||||
// const storedConfirmationNumber = sessionStorage.getItem(
|
||||
// BOOKING_CONFIRMATION_NUMBER
|
||||
// )
|
||||
// TODO: cleanup stored values
|
||||
// sessionStorage.removeItem(BOOKING_CONFIRMATION_NUMBER)
|
||||
// return storedConfirmationNumber
|
||||
// }, [])
|
||||
|
||||
// const bookingStatus = useHandleBookingStatus(
|
||||
// confirmationNumber,
|
||||
// BookingStatusEnum.BookingCompleted,
|
||||
// maxRetries,
|
||||
// retryInterval
|
||||
// )
|
||||
|
||||
// if (
|
||||
// confirmationNumber === null ||
|
||||
// bookingStatus.isError ||
|
||||
// (bookingStatus.isFetched && !bookingStatus.data)
|
||||
// ) {
|
||||
// // TODO: handle error
|
||||
// throw new Error("Error fetching booking status")
|
||||
// }
|
||||
|
||||
// if (
|
||||
// bookingStatus.data?.reservationStatus === BookingStatusEnum.BookingCompleted
|
||||
// ) {
|
||||
// return <LoadingSpinner />
|
||||
|
||||
@@ -7,7 +7,6 @@ import Summary from "@/components/HotelReservation/EnterDetails/Summary"
|
||||
import {
|
||||
generateChildrenString,
|
||||
getQueryParamsForEnterDetails,
|
||||
mapChildrenFromString,
|
||||
} from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
||||
|
||||
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
@@ -17,9 +16,11 @@ export default async function SummaryPage({
|
||||
searchParams,
|
||||
}: PageArgs<LangParams, SearchParams<SelectRateSearchParams>>) {
|
||||
const selectRoomParams = new URLSearchParams(searchParams)
|
||||
const { hotel, adults, children, roomTypeCode, rateCode, fromDate, toDate } =
|
||||
const { hotel, rooms, fromDate, toDate } =
|
||||
getQueryParamsForEnterDetails(selectRoomParams)
|
||||
|
||||
const { adults, children, roomTypeCode, rateCode } = rooms[0] // TODO: Handle multiple rooms
|
||||
|
||||
const availability = await getSelectedRoomAvailability({
|
||||
hotelId: hotel,
|
||||
adults,
|
||||
@@ -37,31 +38,32 @@ export default async function SummaryPage({
|
||||
return null
|
||||
}
|
||||
|
||||
const prices = user
|
||||
? {
|
||||
local: {
|
||||
price: availability.memberRate?.localPrice.pricePerStay,
|
||||
currency: availability.memberRate?.localPrice.currency,
|
||||
},
|
||||
euro: {
|
||||
price: availability.memberRate?.requestedPrice?.pricePerStay,
|
||||
currency: availability.memberRate?.requestedPrice?.currency,
|
||||
},
|
||||
}
|
||||
: {
|
||||
local: {
|
||||
price: availability.publicRate?.localPrice.pricePerStay,
|
||||
currency: availability.publicRate?.localPrice.currency,
|
||||
},
|
||||
euro: {
|
||||
price: availability.publicRate?.requestedPrice?.pricePerStay,
|
||||
currency: availability.publicRate?.requestedPrice?.currency,
|
||||
},
|
||||
}
|
||||
const prices =
|
||||
user && availability.memberRate
|
||||
? {
|
||||
local: {
|
||||
price: availability.memberRate?.localPrice.pricePerStay,
|
||||
currency: availability.memberRate?.localPrice.currency,
|
||||
},
|
||||
euro: {
|
||||
price: availability.memberRate?.requestedPrice?.pricePerStay,
|
||||
currency: availability.memberRate?.requestedPrice?.currency,
|
||||
},
|
||||
}
|
||||
: {
|
||||
local: {
|
||||
price: availability.publicRate?.localPrice.pricePerStay,
|
||||
currency: availability.publicRate?.localPrice.currency,
|
||||
},
|
||||
euro: {
|
||||
price: availability.publicRate?.requestedPrice?.pricePerStay,
|
||||
currency: availability.publicRate?.requestedPrice?.currency,
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<Summary
|
||||
isMember={!!user}
|
||||
showMemberPrice={!!(user && availability.memberRate)}
|
||||
room={{
|
||||
roomType: availability.selectedRoom.roomType,
|
||||
localPrice: prices.local,
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
.layout {
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
.content {
|
||||
.enter-details-layout__content {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3) var(--Spacing-x9);
|
||||
grid-template-columns: 1fr 340px;
|
||||
@@ -15,12 +21,12 @@
|
||||
);
|
||||
}
|
||||
|
||||
.summaryContainer {
|
||||
.enter-details-layout__summaryContainer {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 1/-1;
|
||||
}
|
||||
|
||||
.summary {
|
||||
.enter-details-layout__summary {
|
||||
background-color: var(--Main-Grey-White);
|
||||
|
||||
border-color: var(--Primary-Light-On-Surface-Divider-subtle);
|
||||
@@ -31,22 +37,22 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hider {
|
||||
.enter-details-layout__hider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
.enter-details-layout__shadow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 950px) {
|
||||
.summaryContainer {
|
||||
.enter-details-layout__summaryContainer {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
margin-top: calc(0px - var(--Spacing-x9));
|
||||
}
|
||||
|
||||
.summary {
|
||||
.enter-details-layout__summary {
|
||||
position: sticky;
|
||||
top: calc(
|
||||
var(--booking-widget-desktop-height) +
|
||||
@@ -57,7 +63,7 @@
|
||||
border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0;
|
||||
}
|
||||
|
||||
.hider {
|
||||
.enter-details-layout__hider {
|
||||
display: block;
|
||||
background-color: var(--Scandic-Brand-Warm-White);
|
||||
position: sticky;
|
||||
@@ -69,7 +75,7 @@
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
.enter-details-layout__shadow {
|
||||
display: block;
|
||||
background-color: var(--Main-Grey-White);
|
||||
border-color: var(--Primary-Light-On-Surface-Divider-subtle);
|
||||
@@ -82,14 +88,14 @@
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.summary {
|
||||
.enter-details-layout__summary {
|
||||
top: calc(
|
||||
var(--booking-widget-desktop-height) + var(--Spacing-x2) +
|
||||
var(--Spacing-x-half)
|
||||
);
|
||||
}
|
||||
|
||||
.hider {
|
||||
.enter-details-layout__hider {
|
||||
top: calc(var(--booking-widget-desktop-height) - 6px);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,10 @@
|
||||
import {
|
||||
getCreditCardsSafely,
|
||||
getProfileSafely,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
|
||||
import { preload } from "./_preload"
|
||||
|
||||
import styles from "./layout.module.css"
|
||||
|
||||
import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
|
||||
import type { LangParams, LayoutArgs } from "@/types/params"
|
||||
|
||||
@@ -31,14 +26,14 @@ export default async function StepLayout({
|
||||
|
||||
return (
|
||||
<EnterDetailsProvider step={params.step} isMember={!!user}>
|
||||
<main className={styles.layout}>
|
||||
<main className="enter-details-layout__layout">
|
||||
{hotelHeader}
|
||||
<div className={styles.content}>
|
||||
<div className={"enter-details-layout__content"}>
|
||||
{children}
|
||||
<aside className={styles.summaryContainer}>
|
||||
<div className={styles.hider} />
|
||||
<div className={styles.summary}>{summary}</div>
|
||||
<div className={styles.shadow} />
|
||||
<aside className="enter-details-layout__summaryContainer">
|
||||
<div className="enter-details-layout__hider" />
|
||||
<div className="enter-details-layout__summary">{summary}</div>
|
||||
<div className="enter-details-layout__shadow" />
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import "./enterDetailsLayout.css"
|
||||
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import {
|
||||
@@ -39,14 +41,13 @@ export default async function StepPage({
|
||||
const selectRoomParams = new URLSearchParams(searchParams)
|
||||
const {
|
||||
hotel: hotelId,
|
||||
adults,
|
||||
children,
|
||||
roomTypeCode,
|
||||
rateCode,
|
||||
rooms,
|
||||
fromDate,
|
||||
toDate,
|
||||
} = getQueryParamsForEnterDetails(selectRoomParams)
|
||||
|
||||
const { adults, children, roomTypeCode, rateCode } = rooms[0] // TODO: Handle multiple rooms
|
||||
|
||||
const childrenAsString = children && generateChildrenString(children)
|
||||
|
||||
const breakfastInput = { adults, fromDate, hotelId, toDate }
|
||||
@@ -97,6 +98,11 @@ export default async function StepPage({
|
||||
id: "Select payment method",
|
||||
})
|
||||
|
||||
const roomPrice =
|
||||
user && roomAvailability.memberRate
|
||||
? roomAvailability.memberRate?.localPrice.pricePerStay
|
||||
: roomAvailability.publicRate!.localPrice.pricePerStay
|
||||
|
||||
return (
|
||||
<section>
|
||||
<HistoryStateManager />
|
||||
@@ -137,7 +143,7 @@ export default async function StepPage({
|
||||
label={mustBeGuaranteed ? guaranteeWithCard : selectPaymentMethod}
|
||||
>
|
||||
<Payment
|
||||
hotelId={searchParams.hotel}
|
||||
roomPrice={roomPrice}
|
||||
otherPaymentOptions={
|
||||
hotelData.data.attributes.merchantInformationData
|
||||
.alternatePaymentOptions
|
||||
|
||||
@@ -4,15 +4,17 @@ import { env } from "@/env/server"
|
||||
import { getLocations } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap"
|
||||
import { getHotelReservationQueryParams } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
||||
import {
|
||||
generateChildrenString,
|
||||
getHotelReservationQueryParams,
|
||||
} from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
||||
import { MapModal } from "@/components/MapModal"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
|
||||
import {
|
||||
fetchAvailableHotels,
|
||||
generateChildrenString,
|
||||
getCentralCoordinates,
|
||||
getPointOfInterests,
|
||||
getHotelPins,
|
||||
} from "../../utils"
|
||||
|
||||
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
||||
@@ -57,18 +59,19 @@ export default async function SelectHotelMapPage({
|
||||
children,
|
||||
})
|
||||
|
||||
const pointOfInterests = getPointOfInterests(hotels)
|
||||
const hotelPins = getHotelPins(hotels)
|
||||
|
||||
const centralCoordinates = getCentralCoordinates(pointOfInterests)
|
||||
const centralCoordinates = getCentralCoordinates(hotelPins)
|
||||
|
||||
return (
|
||||
<MapModal>
|
||||
<SelectHotelMap
|
||||
apiKey={googleMapsApiKey}
|
||||
coordinates={centralCoordinates}
|
||||
pointsOfInterest={pointOfInterests}
|
||||
hotelPins={hotelPins}
|
||||
mapId={googleMapId}
|
||||
isModal={true}
|
||||
hotels={hotels}
|
||||
/>
|
||||
</MapModal>
|
||||
)
|
||||
|
||||
@@ -9,17 +9,22 @@
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.section {
|
||||
.header {
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
padding: var(--Spacing-x4) var(--Spacing-x5) var(--Spacing-x3)
|
||||
var(--Spacing-x5);
|
||||
justify-content: space-between;
|
||||
max-width: var(--max-width);
|
||||
}
|
||||
|
||||
.sideBar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 340px;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
padding: var(--Spacing-x2) var(--Spacing-x0);
|
||||
}
|
||||
|
||||
.mapContainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -34,11 +39,27 @@
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.link {
|
||||
display: flex;
|
||||
padding-bottom: var(--Spacing-x6);
|
||||
}
|
||||
.mapContainer {
|
||||
display: block;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--Base-Surface-Primary-light-Normal);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
}
|
||||
.mapLinkText {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--Spacing-x-half);
|
||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x0);
|
||||
}
|
||||
.main {
|
||||
flex-direction: row;
|
||||
gap: var(--Spacing-x5);
|
||||
}
|
||||
.buttonContainer {
|
||||
display: none;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
||||
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
|
||||
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
|
||||
import HotelSorter from "@/components/HotelReservation/SelectHotel/HotelSorter"
|
||||
import MobileMapButtonContainer from "@/components/HotelReservation/SelectHotel/MobileMapButtonContainer"
|
||||
import {
|
||||
generateChildrenString,
|
||||
@@ -96,34 +97,43 @@ export default async function SelectHotelPage({
|
||||
}
|
||||
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<section className={styles.section}>
|
||||
<div className={styles.mapContainer}>
|
||||
<Link href={selectHotelMap[params.lang]} keepSearchParams>
|
||||
<StaticMap
|
||||
city={searchParams.city}
|
||||
width={340}
|
||||
height={180}
|
||||
zoomLevel={11}
|
||||
mapType="roadmap"
|
||||
altText={`Map of ${searchParams.city} city center`}
|
||||
/>
|
||||
</Link>
|
||||
<>
|
||||
<header className={styles.header}>
|
||||
<div>{city.name}</div>
|
||||
<HotelSorter />
|
||||
</header>
|
||||
<main className={styles.main}>
|
||||
<div className={styles.sideBar}>
|
||||
<Link
|
||||
className={styles.link}
|
||||
color="burgundy"
|
||||
href={selectHotelMap[params.lang]}
|
||||
keepSearchParams
|
||||
>
|
||||
{intl.formatMessage({ id: "Show map" })}
|
||||
<ChevronRightIcon color="burgundy" />
|
||||
<div className={styles.mapContainer}>
|
||||
<StaticMap
|
||||
city={searchParams.city}
|
||||
width={340}
|
||||
height={180}
|
||||
zoomLevel={11}
|
||||
mapType="roadmap"
|
||||
altText={`Map of ${searchParams.city} city center`}
|
||||
/>
|
||||
<div className={styles.mapLinkText}>
|
||||
{intl.formatMessage({ id: "Show map" })}
|
||||
<ChevronRightIcon color="burgundy" width={20} height={20} />
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<MobileMapButtonContainer city={searchParams.city} />
|
||||
<HotelFilter filters={filterList} />
|
||||
</div>
|
||||
<MobileMapButtonContainer city={searchParams.city} />
|
||||
<HotelFilter filters={filterList} />
|
||||
</section>
|
||||
<HotelCardListing hotelData={hotels} />
|
||||
<TrackingSDK pageData={pageTrackingData} hotelInfo={hotelsTrackingData} />
|
||||
</main>
|
||||
<HotelCardListing hotelData={hotels} />
|
||||
<TrackingSDK
|
||||
pageData={pageTrackingData}
|
||||
hotelInfo={hotelsTrackingData}
|
||||
/>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,16 +3,23 @@ import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import { BedTypeEnum } from "@/types/components/bookingWidget/enums"
|
||||
import type { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput"
|
||||
import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
import type { Filter } from "@/types/components/hotelReservation/selectHotel/hotelFilters"
|
||||
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
import {
|
||||
type PointOfInterest,
|
||||
PointOfInterestCategoryNameEnum,
|
||||
PointOfInterestGroupEnum,
|
||||
} from "@/types/hotel"
|
||||
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",
|
||||
"Hotel omgivelser",
|
||||
"Hotelumgebung",
|
||||
"Hotellia lähellä",
|
||||
"Hotellomgivelser",
|
||||
"Omgivningar",
|
||||
]
|
||||
|
||||
export async function fetchAvailableHotels(
|
||||
input: AvailabilityInput
|
||||
@@ -22,7 +29,24 @@ export async function fetchAvailableHotels(
|
||||
if (!availableHotels) throw new Error()
|
||||
|
||||
const language = getLang()
|
||||
const hotels = availableHotels.availability.map(async (hotel) => {
|
||||
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 hotelData = await getHotelData({
|
||||
hotelId: hotel.hotelId.toString(),
|
||||
language,
|
||||
@@ -39,7 +63,7 @@ export async function fetchAvailableHotels(
|
||||
return await Promise.all(hotels)
|
||||
}
|
||||
|
||||
export function getFiltersFromHotels(hotels: HotelData[]) {
|
||||
export function getFiltersFromHotels(hotels: HotelData[]): CategorizedFilters {
|
||||
const filters = hotels.flatMap((hotel) => hotel.hotelData.detailedFacilities)
|
||||
|
||||
const uniqueFilterIds = [...new Set(filters.map((filter) => filter.id))]
|
||||
@@ -47,51 +71,54 @@ export function getFiltersFromHotels(hotels: HotelData[]) {
|
||||
.map((filterId) => filters.find((filter) => filter.id === filterId))
|
||||
.filter((filter): filter is Filter => filter !== undefined)
|
||||
|
||||
return filterList
|
||||
return filterList.reduce<CategorizedFilters>(
|
||||
(acc, filter) => {
|
||||
if (filter.filter && hotelSurroundingsFilterNames.includes(filter.filter))
|
||||
return {
|
||||
facilityFilters: acc.facilityFilters,
|
||||
surroundingsFilters: [...acc.surroundingsFilters, filter],
|
||||
}
|
||||
|
||||
return {
|
||||
facilityFilters: [...acc.facilityFilters, filter],
|
||||
surroundingsFilters: acc.surroundingsFilters,
|
||||
}
|
||||
},
|
||||
{ facilityFilters: [], surroundingsFilters: [] }
|
||||
)
|
||||
}
|
||||
|
||||
const bedTypeMap: Record<number, string> = {
|
||||
[BedTypeEnum.IN_ADULTS_BED]: "ParentsBed",
|
||||
[BedTypeEnum.IN_CRIB]: "Crib",
|
||||
[BedTypeEnum.IN_EXTRA_BED]: "ExtraBed",
|
||||
}
|
||||
|
||||
export function generateChildrenString(children: Child[]): string {
|
||||
return `[${children
|
||||
?.map((child) => {
|
||||
const age = child.age
|
||||
const bedType = bedTypeMap[+child.bed]
|
||||
return `${age}:${bedType}`
|
||||
})
|
||||
.join(",")}]`
|
||||
}
|
||||
|
||||
export function getPointOfInterests(hotels: HotelData[]): PointOfInterest[] {
|
||||
// TODO: this is just a quick transformation to get something there. May need rework
|
||||
export function getHotelPins(hotels: HotelData[]): HotelPin[] {
|
||||
return hotels.map((hotel) => ({
|
||||
coordinates: {
|
||||
lat: hotel.hotelData.location.latitude,
|
||||
lng: hotel.hotelData.location.longitude,
|
||||
},
|
||||
name: hotel.hotelData.name,
|
||||
distance: hotel.hotelData.location.distanceToCentre,
|
||||
categoryName: PointOfInterestCategoryNameEnum.HOTEL,
|
||||
group: PointOfInterestGroupEnum.LOCATION,
|
||||
publicPrice: hotel.price?.regularAmount ?? null,
|
||||
memberPrice: hotel.price?.memberAmount ?? null,
|
||||
currency: hotel.price?.currency || null,
|
||||
images: [
|
||||
hotel.hotelData.hotelContent.images,
|
||||
...(hotel.hotelData.gallery?.heroImages ?? []),
|
||||
],
|
||||
amenities: hotel.hotelData.detailedFacilities.slice(0, 3),
|
||||
ratings: hotel.hotelData.ratings?.tripAdvisor.rating ?? null,
|
||||
}))
|
||||
}
|
||||
|
||||
export function getCentralCoordinates(pointOfInterests: PointOfInterest[]) {
|
||||
const centralCoordinates = pointOfInterests.reduce(
|
||||
(acc, poi) => {
|
||||
acc.lat += poi.coordinates.lat
|
||||
acc.lng += poi.coordinates.lng
|
||||
export function getCentralCoordinates(hotels: HotelPin[]) {
|
||||
const centralCoordinates = hotels.reduce(
|
||||
(acc, pin) => {
|
||||
acc.lat += pin.coordinates.lat
|
||||
acc.lng += pin.coordinates.lng
|
||||
return acc
|
||||
},
|
||||
{ lat: 0, lng: 0 }
|
||||
)
|
||||
|
||||
centralCoordinates.lat /= pointOfInterests.length
|
||||
centralCoordinates.lng /= pointOfInterests.length
|
||||
centralCoordinates.lat /= hotels.length
|
||||
centralCoordinates.lng /= hotels.length
|
||||
|
||||
return centralCoordinates
|
||||
}
|
||||
|
||||
@@ -49,11 +49,11 @@ export default async function SelectRatePage({
|
||||
searchParams.fromDate &&
|
||||
dt(searchParams.fromDate).isAfter(dt().subtract(1, "day"))
|
||||
? searchParams.fromDate
|
||||
: dt().utc().format("YYYY-MM-DD")
|
||||
: dt().utc().format("YYYY-MM-D")
|
||||
const validToDate =
|
||||
searchParams.toDate && dt(searchParams.toDate).isAfter(validFromDate)
|
||||
? searchParams.toDate
|
||||
: dt().utc().add(1, "day").format("YYYY-MM-DD")
|
||||
: dt().utc().add(1, "day").format("YYYY-MM-D")
|
||||
const adults = selectRoomParamsObject.room[0].adults || 1 // TODO: Handle multiple rooms
|
||||
const childrenCount = selectRoomParamsObject.room[0].child?.length
|
||||
const children = selectRoomParamsObject.room[0].child
|
||||
|
||||
Reference in New Issue
Block a user