Merged in monorepo-step-1 (pull request #1080)
Migrate to a monorepo setup - step 1 * Move web to subfolder /apps/scandic-web * Yarn + transitive deps - Move to yarn - design-system package removed for now since yarn doesn't support the parameter for token (ie project currently broken) - Add missing transitive dependencies as Yarn otherwise prevents these imports - VS Code doesn't pick up TS path aliases unless you open /apps/scandic-web instead of root (will be fixed with monorepo) * Pin framer-motion to temporarily fix typing issue https://github.com/adobe/react-spectrum/issues/7494 * Pin zod to avoid typ error There seems to have been a breaking change in the types returned by zod where error is now returned as undefined instead of missing in the type. We should just handle this but to avoid merge conflicts just pin the dependency for now. * Pin react-intl version Pin version of react-intl to avoid tiny type issue where formatMessage does not accept a generic any more. This will be fixed in a future commit, but to avoid merge conflicts just pin for now. * Pin typescript version Temporarily pin version as newer versions as stricter and results in a type error. Will be fixed in future commit after merge. * Setup workspaces * Add design-system as a monorepo package * Remove unused env var DESIGN_SYSTEM_ACCESS_TOKEN * Fix husky for monorepo setup * Update netlify.toml * Add lint script to root package.json * Add stub readme * Fix react-intl formatMessage types * Test netlify.toml in root * Remove root toml * Update netlify.toml publish path * Remove package-lock.json * Update build for branch/preview builds Approved-by: Linus Flood
This commit is contained in:
committed by
Linus Flood
parent
667cab6fb6
commit
80100e7631
@@ -0,0 +1,42 @@
|
||||
.main {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
display: grid;
|
||||
gap: var(--Spacing-x5);
|
||||
grid-template-areas: "header" "booking";
|
||||
margin: 0 auto;
|
||||
min-height: 100dvh;
|
||||
padding-top: var(--Spacing-x5);
|
||||
width: var(--max-width-page);
|
||||
}
|
||||
|
||||
.booking {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x5);
|
||||
grid-area: booking;
|
||||
padding-bottom: var(--Spacing-x9);
|
||||
}
|
||||
|
||||
.aside {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.main {
|
||||
grid-template-areas:
|
||||
"header receipt"
|
||||
"booking receipt";
|
||||
grid-template-columns: 1fr 340px;
|
||||
grid-template-rows: auto 1fr;
|
||||
padding-top: var(--Spacing-x9);
|
||||
}
|
||||
|
||||
.mobileReceipt {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.aside {
|
||||
display: grid;
|
||||
grid-area: receipt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
"use client"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { useRef } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { MEMBERSHIP_FAILED_ERROR } from "@/constants/booking"
|
||||
|
||||
import Header from "@/components/HotelReservation/BookingConfirmation/Header"
|
||||
import HotelDetails from "@/components/HotelReservation/BookingConfirmation/HotelDetails"
|
||||
import PaymentDetails from "@/components/HotelReservation/BookingConfirmation/PaymentDetails"
|
||||
import Promos from "@/components/HotelReservation/BookingConfirmation/Promos"
|
||||
import Receipt from "@/components/HotelReservation/BookingConfirmation/Receipt"
|
||||
import Rooms from "@/components/HotelReservation/BookingConfirmation/Rooms"
|
||||
import SidePanel from "@/components/HotelReservation/SidePanel"
|
||||
import Alert from "@/components/TempDesignSystem/Alert"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
|
||||
import styles from "./confirmation.module.css"
|
||||
|
||||
import type { ConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||
|
||||
export default function Confirmation({
|
||||
booking,
|
||||
hotel,
|
||||
room,
|
||||
}: ConfirmationProps) {
|
||||
const searchParams = useSearchParams()
|
||||
const intl = useIntl()
|
||||
const mainRef = useRef<HTMLElement | null>(null)
|
||||
const membershipFailedError =
|
||||
searchParams.get("errorCode") === MEMBERSHIP_FAILED_ERROR
|
||||
const failedToVerifyMembership =
|
||||
booking.rateDefinition.isMemberRate && !booking.guest.membershipNumber
|
||||
|
||||
return (
|
||||
<main className={styles.main} ref={mainRef}>
|
||||
<Header booking={booking} hotel={hotel} mainRef={mainRef} />
|
||||
<div className={styles.booking}>
|
||||
{/* Customer has manually entered a membership number for which verification failed */}
|
||||
{membershipFailedError && (
|
||||
<Alert
|
||||
type={AlertTypeEnum.Info}
|
||||
heading={intl.formatMessage({
|
||||
id: "Failed to verify membership",
|
||||
})}
|
||||
text={intl.formatMessage({
|
||||
id: "The first or last name doesn't match the membership number you provided. Your booking(s) is confirmed but to get the membership attached you'll need to present your existing membership number upon check-in. If you have booked with a member discount, you'll either need to present your existing membership number upon check-in, become a member or pay the price difference at the hotel. Signing up is preferably done online before the stay, or we can assist upon arrival.",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{/* For some other reason membership could not be verified */}
|
||||
{!membershipFailedError && failedToVerifyMembership && (
|
||||
<Alert
|
||||
type={AlertTypeEnum.Info}
|
||||
heading={intl.formatMessage({
|
||||
id: "Failed to verify membership",
|
||||
})}
|
||||
text={intl.formatMessage({
|
||||
id: "Your booking(s) is confirmed but we could not verify your membership. If you have booked with a member discount, you'll either need to present your existing membership number upon check-in, become a member or pay the price difference at the hotel. Signing up is preferably done online before the stay.",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
<Rooms
|
||||
booking={booking}
|
||||
mainRoom={room}
|
||||
linkedReservations={booking.linkedReservations}
|
||||
/>
|
||||
<PaymentDetails booking={booking} />
|
||||
<Divider color="primaryLightSubtle" />
|
||||
<HotelDetails hotel={hotel} />
|
||||
<Promos
|
||||
confirmationNumber={booking.confirmationNumber}
|
||||
hotelId={hotel.operaId}
|
||||
lastName={booking.guest.lastName}
|
||||
/>
|
||||
<div className={styles.mobileReceipt}>
|
||||
<Receipt booking={booking} hotel={hotel} room={room} />
|
||||
</div>
|
||||
</div>
|
||||
<aside className={styles.aside}>
|
||||
<SidePanel variant="receipt">
|
||||
<Receipt booking={booking} hotel={hotel} room={room} />
|
||||
</SidePanel>
|
||||
</aside>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { CalendarAddIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
|
||||
export default function AddToCalendarButton({
|
||||
onPress,
|
||||
}: {
|
||||
onPress: () => void
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<Button intent="text" size="small" theme="base" wrapping onPress={onPress}>
|
||||
<CalendarAddIcon />
|
||||
{intl.formatMessage({ id: "Add to calendar" })}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
import { useReactToPrint } from "react-to-print"
|
||||
|
||||
import { DownloadIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
|
||||
import type { DownloadInvoiceProps } from "@/types/components/hotelReservation/bookingConfirmation/actions/downloadInvoice"
|
||||
|
||||
export default function DownloadInvoice({ mainRef }: DownloadInvoiceProps) {
|
||||
const intl = useIntl()
|
||||
const reactToPrintFn = useReactToPrint({ contentRef: mainRef })
|
||||
|
||||
function downloadBooking() {
|
||||
reactToPrintFn()
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
intent="text"
|
||||
onPress={downloadBooking}
|
||||
size="small"
|
||||
theme="base"
|
||||
variant="icon"
|
||||
wrapping
|
||||
>
|
||||
<DownloadIcon />
|
||||
{intl.formatMessage({ id: "Download invoice" })}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { myBooking } from "@/constants/myBooking"
|
||||
import { env } from "@/env/client"
|
||||
|
||||
import { EditIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import type { ManageBookingProps } from "@/types/components/hotelReservation/bookingConfirmation/actions/manageBooking"
|
||||
|
||||
export default function ManageBooking({
|
||||
confirmationNumber,
|
||||
lastName,
|
||||
}: ManageBookingProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const myBookingUrl = myBooking[env.NEXT_PUBLIC_NODE_ENV][lang]
|
||||
return (
|
||||
<Button
|
||||
asChild
|
||||
intent="text"
|
||||
size="small"
|
||||
theme="base"
|
||||
variant="icon"
|
||||
wrapping
|
||||
>
|
||||
<Link
|
||||
color="none"
|
||||
href={`${myBookingUrl}?bookingId=${confirmationNumber}&lastName=${lastName}`}
|
||||
weight="bold"
|
||||
>
|
||||
<EditIcon />
|
||||
{intl.formatMessage({ id: "Manage booking" })}
|
||||
</Link>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import type { DateTime } from "ics"
|
||||
|
||||
export function generateDateTime(d: Date): DateTime {
|
||||
const _d = dt(d).utc()
|
||||
return [
|
||||
_d.year(),
|
||||
// Need to add +1 since month is 0 based
|
||||
_d.month() + 1,
|
||||
_d.date(),
|
||||
_d.hour(),
|
||||
_d.minute(),
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
.header,
|
||||
.hgroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
gap: var(--Spacing-x2);
|
||||
grid-area: header;
|
||||
}
|
||||
|
||||
.hgroup {
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.body {
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
display: grid;
|
||||
grid-area: actions;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.actions {
|
||||
gap: var(--Spacing-x3);
|
||||
grid-auto-columns: auto;
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.header {
|
||||
padding-bottom: var(--Spacing-x4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import AddToCalendar from "../../AddToCalendar"
|
||||
import AddToCalendarButton from "./Actions/AddToCalendarButton"
|
||||
import DownloadInvoice from "./Actions/DownloadInvoice"
|
||||
import { generateDateTime } from "./Actions/helpers"
|
||||
import ManageBooking from "./Actions/ManageBooking"
|
||||
|
||||
import styles from "./header.module.css"
|
||||
|
||||
import type { EventAttributes } from "ics"
|
||||
|
||||
import type { BookingConfirmationHeaderProps } from "@/types/components/hotelReservation/bookingConfirmation/header"
|
||||
|
||||
export default function Header({
|
||||
booking,
|
||||
hotel,
|
||||
mainRef,
|
||||
}: BookingConfirmationHeaderProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
const text = intl.formatMessage(
|
||||
{
|
||||
id: "Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please <emailLink>contact us.</emailLink>",
|
||||
},
|
||||
{
|
||||
emailLink: (str) => (
|
||||
<Link color="burgundy" href="#" textDecoration="underline">
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
const event: EventAttributes = {
|
||||
busyStatus: "FREE",
|
||||
categories: ["booking", "hotel", "stay"],
|
||||
created: generateDateTime(booking.createDateTime),
|
||||
description: hotel.hotelContent.texts.descriptions?.medium,
|
||||
end: generateDateTime(booking.checkOutDate),
|
||||
endInputType: "utc",
|
||||
geo: {
|
||||
lat: hotel.location.latitude,
|
||||
lon: hotel.location.longitude,
|
||||
},
|
||||
location: `${hotel.address.streetAddress}, ${hotel.address.zipCode} ${hotel.address.city} ${hotel.address.country}`,
|
||||
start: generateDateTime(booking.checkInDate),
|
||||
startInputType: "utc",
|
||||
status: "CONFIRMED",
|
||||
title: hotel.name,
|
||||
url: hotel.contactInformation.websiteUrl,
|
||||
}
|
||||
|
||||
return (
|
||||
<header className={styles.header}>
|
||||
<hgroup className={styles.hgroup}>
|
||||
<Title as="h2" color="red" textTransform="uppercase" type="h2">
|
||||
{intl.formatMessage({ id: "Booking confirmation" })}
|
||||
</Title>
|
||||
<Title as="h2" color="burgundy" textTransform="uppercase" type="h1">
|
||||
{hotel.name}
|
||||
</Title>
|
||||
</hgroup>
|
||||
<Subtitle color="uiTextHighContrast" type="two">
|
||||
{intl.formatMessage(
|
||||
{ id: "Reservation number {value}" },
|
||||
{
|
||||
value: booking.confirmationNumber,
|
||||
}
|
||||
)}
|
||||
</Subtitle>
|
||||
<Body className={styles.body}>{text}</Body>
|
||||
<div className={styles.actions}>
|
||||
<AddToCalendar
|
||||
checkInDate={booking.checkInDate}
|
||||
event={event}
|
||||
hotelName={hotel.name}
|
||||
renderButton={(onPress) => <AddToCalendarButton onPress={onPress} />}
|
||||
/>
|
||||
<ManageBooking
|
||||
confirmationNumber={booking.confirmationNumber}
|
||||
lastName={booking.guest.lastName}
|
||||
/>
|
||||
<DownloadInvoice mainRef={mainRef} />
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
.contact,
|
||||
.container,
|
||||
.details,
|
||||
.hotel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.container {
|
||||
gap: var(--Spacing-x4);
|
||||
}
|
||||
|
||||
.details {
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.contact,
|
||||
.hotel {
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.coordinates {
|
||||
margin-top: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.toast {
|
||||
align-self: flex-start;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.list {
|
||||
padding-left: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.link {
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { Toast } from "@/components/TempDesignSystem/Toasts"
|
||||
|
||||
import styles from "./hotelDetails.module.css"
|
||||
|
||||
import type { BookingConfirmationHotelDetailsProps } from "@/types/components/hotelReservation/bookingConfirmation/hotelDetails"
|
||||
|
||||
export default function HotelDetails({
|
||||
hotel,
|
||||
}: BookingConfirmationHotelDetailsProps) {
|
||||
const intl = useIntl()
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.details}>
|
||||
<Subtitle color="uiTextHighContrast" type="two">
|
||||
{intl.formatMessage({ id: "Hotel details" })}
|
||||
</Subtitle>
|
||||
<div className={styles.hotel}>
|
||||
<Body color="uiTextHighContrast">{hotel.name}</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{streetAddress}, {zipCode} {city}" },
|
||||
{
|
||||
streetAddress: hotel.address.streetAddress,
|
||||
zipCode: hotel.address.zipCode,
|
||||
city: hotel.address.city,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
<Body asChild color="uiTextHighContrast">
|
||||
<Link
|
||||
className={styles.link}
|
||||
href={`tel:${hotel.contactInformation.phoneNumber}`}
|
||||
>
|
||||
{hotel.contactInformation.phoneNumber}
|
||||
</Link>
|
||||
</Body>
|
||||
</div>
|
||||
<Body color="uiTextPlaceholder" className={styles.coordinates}>
|
||||
{intl.formatMessage(
|
||||
{ id: "Long {long} ∙ Lat {lat}" },
|
||||
{
|
||||
lat: hotel.location.latitude,
|
||||
long: hotel.location.longitude,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
</div>
|
||||
<div className={styles.contact}>
|
||||
<Link
|
||||
className={styles.link}
|
||||
color="baseTextMediumContrast"
|
||||
href={`mailto:${hotel.contactInformation.email}`}
|
||||
>
|
||||
{hotel.contactInformation.email}
|
||||
</Link>
|
||||
<Link
|
||||
className={styles.link}
|
||||
color="baseTextMediumContrast"
|
||||
href={hotel.contactInformation.websiteUrl}
|
||||
>
|
||||
{hotel.contactInformation.websiteUrl}
|
||||
</Link>
|
||||
</div>
|
||||
<div className={styles.toast}>
|
||||
<Toast variant="info">
|
||||
<ul className={styles.list}>
|
||||
<li>{intl.formatMessage({ id: "N/A" })}</li>
|
||||
</ul>
|
||||
</Toast>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import { CreditCardAddIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import styles from "./paymentDetails.module.css"
|
||||
|
||||
import type { BookingConfirmationPaymentDetailsProps } from "@/types/components/hotelReservation/bookingConfirmation/paymentDetails"
|
||||
|
||||
export default function PaymentDetails({
|
||||
booking,
|
||||
}: BookingConfirmationPaymentDetailsProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
return (
|
||||
<div className={styles.details}>
|
||||
<Subtitle color="uiTextHighContrast" type="two">
|
||||
{intl.formatMessage({ id: "Payment details" })}
|
||||
</Subtitle>
|
||||
<div className={styles.payment}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} has been paid" },
|
||||
{
|
||||
amount: formatPrice(
|
||||
intl,
|
||||
booking.totalPrice,
|
||||
booking.currencyCode
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{dt(booking.createDateTime)
|
||||
.locale(lang)
|
||||
.format("ddd D MMM YYYY, hh:mm")}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{card} ending with {cardno}" },
|
||||
{ card: "N/A", cardno: "N/A" }
|
||||
)}
|
||||
</Body>
|
||||
</div>
|
||||
<Button
|
||||
className={styles.btn}
|
||||
intent="text"
|
||||
size="small"
|
||||
theme="base"
|
||||
variant="icon"
|
||||
wrapping
|
||||
>
|
||||
<CreditCardAddIcon />
|
||||
{intl.formatMessage({ id: "Save card to profile" })}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
.details,
|
||||
.payment {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.details {
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.payment {
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.details button.btn {
|
||||
align-self: flex-start;
|
||||
margin-top: var(--Spacing-x-half);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import styles from "./promo.module.css"
|
||||
|
||||
import type { PromoProps } from "@/types/components/hotelReservation/bookingConfirmation/promo"
|
||||
|
||||
export default function Promo({ buttonText, href, text, title }: PromoProps) {
|
||||
return (
|
||||
<Link className={styles.link} color="none" href={href}>
|
||||
<article className={styles.promo}>
|
||||
<Title color="white" level="h4">
|
||||
{title}
|
||||
</Title>
|
||||
<Body className={styles.text} color="white" textAlign="center">
|
||||
{text}
|
||||
</Body>
|
||||
<Button asChild intent="primary" size="small" theme="primaryStrong">
|
||||
<div>{buttonText}</div>
|
||||
</Button>
|
||||
</article>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
.promo {
|
||||
align-items: center;
|
||||
background-position: 50%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
border-radius: var(--Medium, 8px);
|
||||
display: flex;
|
||||
flex: 1 0 320px;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
height: 320px;
|
||||
justify-content: center;
|
||||
padding: var(--Spacing-x4) var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.link:nth-of-type(1) .promo {
|
||||
background-image: linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 0.36) 37.88%,
|
||||
rgba(0, 0, 0, 0.75) 100%
|
||||
),
|
||||
url("/_static/img/Scandic_Park_Party_Lipstick.jpg");
|
||||
}
|
||||
|
||||
.link:nth-of-type(2) .promo {
|
||||
background-image: linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 0.36) 37.88%,
|
||||
rgba(0, 0, 0, 0.75) 100%
|
||||
),
|
||||
url("/_static/img/Scandic_Family_Breakfast.jpg");
|
||||
}
|
||||
|
||||
.text {
|
||||
max-width: 400px;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { homeHrefs } from "@/constants/homeHrefs"
|
||||
import { myBooking } from "@/constants/myBooking"
|
||||
import { env } from "@/env/client"
|
||||
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import Promo from "./Promo"
|
||||
|
||||
import styles from "./promos.module.css"
|
||||
|
||||
import type { PromosProps } from "@/types/components/hotelReservation/bookingConfirmation/promos"
|
||||
|
||||
export default function Promos({
|
||||
confirmationNumber,
|
||||
hotelId,
|
||||
lastName,
|
||||
}: PromosProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const homeUrl = homeHrefs[env.NEXT_PUBLIC_NODE_ENV][lang]
|
||||
const myBookingUrl = myBooking[env.NEXT_PUBLIC_NODE_ENV][lang]
|
||||
return (
|
||||
<div className={styles.promos}>
|
||||
<Promo
|
||||
buttonText={intl.formatMessage({ id: "View and buy add-ons" })}
|
||||
href={`${myBookingUrl}?bookingId=${confirmationNumber}&lastName=${lastName}`}
|
||||
text={intl.formatMessage({
|
||||
id: "Discover the little extra touches to make your upcoming stay even more unforgettable.",
|
||||
})}
|
||||
title={intl.formatMessage({ id: "Spice things up" })}
|
||||
/>
|
||||
<Promo
|
||||
buttonText={intl.formatMessage({ id: "Book another stay" })}
|
||||
href={`${homeUrl}?hotel=${hotelId}`}
|
||||
text={intl.formatMessage({
|
||||
id: "Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
||||
})}
|
||||
title={intl.formatMessage({ id: "Book your next stay" })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
.promos {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
padding: var(--Spacing-x5) 0;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.promos {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
"use client"
|
||||
|
||||
import { notFound } from "next/navigation"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronRightSmallIcon,
|
||||
InfoCircleIcon,
|
||||
} from "@/components/Icons"
|
||||
import Modal from "@/components/Modal"
|
||||
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 { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import styles from "./receipt.module.css"
|
||||
|
||||
import type { BookingConfirmationReceiptProps } from "@/types/components/hotelReservation/bookingConfirmation/receipt"
|
||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
|
||||
export default function Receipt({
|
||||
booking,
|
||||
room,
|
||||
}: BookingConfirmationReceiptProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
if (!room) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const breakfastPkgSelected = booking.packages.find(
|
||||
(pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST
|
||||
)
|
||||
const breakfastPkgIncluded = booking.packages.find(
|
||||
(pkg) => pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
|
||||
)
|
||||
return (
|
||||
<section className={styles.receipt}>
|
||||
<Subtitle type="two">
|
||||
{intl.formatMessage({ id: "Booking summary" })}
|
||||
</Subtitle>
|
||||
<article className={styles.room}>
|
||||
<header className={styles.roomHeader}>
|
||||
<Body color="uiTextHighContrast">{room.name}</Body>
|
||||
{booking.rateDefinition.isMemberRate ? (
|
||||
<div className={styles.memberPrice}>
|
||||
<Body color="red">
|
||||
{formatPrice(intl, booking.roomPrice, booking.currencyCode)}
|
||||
</Body>
|
||||
</div>
|
||||
) : (
|
||||
<Body color="uiTextHighContrast">
|
||||
{formatPrice(intl, booking.roomPrice, booking.currencyCode)}
|
||||
</Body>
|
||||
)}
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
|
||||
{
|
||||
totalAdults: booking.adults,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{booking.rateDefinition.cancellationText}
|
||||
</Caption>
|
||||
<Modal
|
||||
trigger={
|
||||
<Button intent="text" className={styles.termsLink}>
|
||||
<Link
|
||||
color="peach80"
|
||||
href=""
|
||||
size="small"
|
||||
textDecoration="underline"
|
||||
variant="icon"
|
||||
>
|
||||
{intl.formatMessage({ id: "Reservation policy" })}
|
||||
<InfoCircleIcon color="peach80" />
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
title={booking.rateDefinition.cancellationText || ""}
|
||||
subtitle={
|
||||
booking.rateDefinition.cancellationRule == "CancellableBefore6PM"
|
||||
? intl.formatMessage({ id: "Pay later" })
|
||||
: intl.formatMessage({ id: "Pay now" })
|
||||
}
|
||||
>
|
||||
<div className={styles.terms}>
|
||||
{booking.rateDefinition.generalTerms?.map((info) => (
|
||||
<Body
|
||||
key={info}
|
||||
color="uiTextHighContrast"
|
||||
className={styles.termsText}
|
||||
>
|
||||
<CheckIcon
|
||||
color="uiSemanticSuccess"
|
||||
width={20}
|
||||
height={20}
|
||||
className={styles.termsIcon}
|
||||
></CheckIcon>
|
||||
{info}
|
||||
</Body>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
</header>
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">{room.bedType.description}</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{formatPrice(intl, 0, booking.currencyCode)}
|
||||
</Body>
|
||||
</div>
|
||||
<div className={styles.entry}>
|
||||
<Body>{intl.formatMessage({ id: "Breakfast buffet" })}</Body>
|
||||
{booking.rateDefinition.breakfastIncluded ?? breakfastPkgIncluded ? (
|
||||
<Body color="red">{intl.formatMessage({ id: "Included" })}</Body>
|
||||
) : null}
|
||||
|
||||
{breakfastPkgSelected ? (
|
||||
<Body color="uiTextHighContrast">
|
||||
{formatPrice(
|
||||
intl,
|
||||
breakfastPkgSelected.totalPrice,
|
||||
breakfastPkgSelected.currency
|
||||
)}
|
||||
</Body>
|
||||
) : null}
|
||||
</div>
|
||||
</article>
|
||||
<Divider color="primaryLightSubtle" />
|
||||
<div className={styles.price}>
|
||||
<div className={styles.entry}>
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: "Total price" })}
|
||||
</Body>
|
||||
<Body textTransform="bold">
|
||||
{formatPrice(intl, booking.totalPrice, booking.currencyCode)}
|
||||
</Body>
|
||||
</div>
|
||||
<div className={styles.entry}>
|
||||
<Button
|
||||
className={styles.btn}
|
||||
intent="text"
|
||||
size="small"
|
||||
theme="base"
|
||||
variant="icon"
|
||||
wrapping
|
||||
>
|
||||
{intl.formatMessage({ id: "Price details" })}
|
||||
<ChevronRightSmallIcon />
|
||||
</Button>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "Approx. {value}" },
|
||||
{
|
||||
value: "N/A",
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
.receipt {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.room {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.roomHeader {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.roomHeader :nth-child(n + 3) {
|
||||
grid-column: 1/-1;
|
||||
}
|
||||
|
||||
.memberPrice {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.entry {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.receipt .price button.btn {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.termsLink {
|
||||
justify-self: flex-start;
|
||||
}
|
||||
.terms {
|
||||
padding-top: var(--Spacing-x3);
|
||||
}
|
||||
.termsText:nth-child(n) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: var(--Spacing-x1);
|
||||
}
|
||||
.terms .termsIcon {
|
||||
padding-right: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.receipt {
|
||||
padding: var(--Spacing-x3);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
"use client "
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import styles from "./linkedReservation.module.css"
|
||||
|
||||
import type { LinkedReservationSchema } from "@/types/components/hotelReservation/bookingConfirmation/rooms"
|
||||
|
||||
interface LinkedReservationProps {
|
||||
linkedReservation: LinkedReservationSchema
|
||||
roomIndex: number
|
||||
}
|
||||
|
||||
export function LinkedReservation({
|
||||
linkedReservation,
|
||||
roomIndex,
|
||||
}: LinkedReservationProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const { checkinDate, checkoutDate, confirmationNumber, adults, children } =
|
||||
linkedReservation
|
||||
|
||||
const fromDate = dt(checkinDate).locale(lang)
|
||||
const toDate = dt(checkoutDate).locale(lang)
|
||||
return (
|
||||
<div className={styles.reservation}>
|
||||
<Subtitle color="uiTextHighContrast" type="two">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "Room {roomIndex}",
|
||||
},
|
||||
{
|
||||
roomIndex: roomIndex + 2,
|
||||
}
|
||||
)}
|
||||
</Subtitle>
|
||||
<ul className={styles.details}>
|
||||
<li className={styles.listItem}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Booking number" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{confirmationNumber}</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Check-in" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{checkInDate} from {checkInTime}" },
|
||||
{
|
||||
checkInDate: fromDate.format("ddd, D MMM"),
|
||||
checkInTime: fromDate.format("HH:mm"),
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Check-out" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{checkOutDate} from {checkOutTime}" },
|
||||
{
|
||||
checkOutDate: toDate.format("ddd, D MMM"),
|
||||
checkOutTime: toDate.format("HH:mm"),
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Adults" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{adults}</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Children" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{children}</Body>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
.reservation {
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
padding: var(--Spacing-x2) var(--Spacing-x2) var(--Spacing-x3)
|
||||
var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.details {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x-half) var(--Spacing-x3);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.listItem {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ChevronRightSmallIcon,
|
||||
CrossCircle,
|
||||
} from "@/components/Icons"
|
||||
import Image from "@/components/Image"
|
||||
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 useLang from "@/hooks/useLang"
|
||||
|
||||
import styles from "./room.module.css"
|
||||
|
||||
import type { RoomProps } from "@/types/components/hotelReservation/bookingConfirmation/rooms/room"
|
||||
|
||||
export default function Room({ booking, img, roomName }: RoomProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
const guestName = `${booking.guest.firstName} ${booking.guest.lastName}`
|
||||
const fromDate = dt(booking.checkInDate).locale(lang)
|
||||
const toDate = dt(booking.checkOutDate).locale(lang)
|
||||
return (
|
||||
<article className={styles.room}>
|
||||
<header className={styles.header}>
|
||||
{/* <Subtitle color="mainGrey60" type="two">
|
||||
{intl.formatMessage({ id: "Room" })} 1
|
||||
</Subtitle> */}
|
||||
<div className={styles.benefits}>
|
||||
{booking.rateDefinition.isMemberRate ? (
|
||||
<>
|
||||
<CheckCircleIcon color="green" height={20} width={20} />
|
||||
<Caption>
|
||||
{intl.formatMessage({ id: "Membership benefits applied" })}
|
||||
</Caption>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CrossCircle color="red" height={20} width={20} />
|
||||
<Caption>
|
||||
{intl.formatMessage({ id: "No membership benefits applied" })}
|
||||
</Caption>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
<div className={styles.booking}>
|
||||
<Image
|
||||
alt={img.metaData.altText}
|
||||
className={styles.img}
|
||||
focalPoint={{ x: 50, y: 50 }}
|
||||
height={204}
|
||||
src={img.imageSizes.medium}
|
||||
style={{ borderRadius: "var(--Corner-radius-Medium)" }}
|
||||
title={img.metaData.title}
|
||||
width={204}
|
||||
/>
|
||||
<div className={styles.roomDetails}>
|
||||
<div className={styles.roomName}>
|
||||
<Subtitle color="uiTextHighContrast" type="two">
|
||||
{roomName}
|
||||
</Subtitle>
|
||||
<Link color="burgundy" href="" variant="icon">
|
||||
{intl.formatMessage({ id: "View room details" })}
|
||||
<ChevronRightSmallIcon color="burgundy" />
|
||||
</Link>
|
||||
</div>
|
||||
<ul className={styles.details}>
|
||||
<li className={styles.listItem}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Check-in" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{checkInDate} from {checkInTime}" },
|
||||
{
|
||||
checkInDate: fromDate.format("ddd, D MMM"),
|
||||
checkInTime: fromDate.format("HH:mm"),
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Check-out" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{checkOutDate} from {checkOutTime}" },
|
||||
{
|
||||
checkOutDate: toDate.format("ddd, D MMM"),
|
||||
checkOutTime: toDate.format("HH:mm"),
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Breakfast" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "N/A" })}
|
||||
</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Cancellation policy" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{booking.rateDefinition.cancellationText}
|
||||
</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Rebooking" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "N/A" })}
|
||||
</Body>
|
||||
</li>
|
||||
</ul>
|
||||
<div className={styles.guest}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Main guest" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{guestName}</Body>
|
||||
{booking.guest.membershipNumber ? (
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "Friend no. {value}" },
|
||||
{
|
||||
value: booking.guest.membershipNumber,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
) : null}
|
||||
{booking.guest.phoneNumber ? (
|
||||
<Body color="uiTextHighContrast">
|
||||
{booking.guest.phoneNumber}
|
||||
</Body>
|
||||
) : null}
|
||||
{booking.guest.email ? (
|
||||
<Body color="uiTextHighContrast">{booking.guest.email}</Body>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
.room {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.header {
|
||||
align-items: flex-end;
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.benefits {
|
||||
align-items: center;
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half);
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.booking {
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
padding: var(--Spacing-x2) var(--Spacing-x2) var(--Spacing-x3)
|
||||
var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.roomDetails {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.roomName {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-half);
|
||||
grid-column: 1/-1;
|
||||
}
|
||||
|
||||
.details {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x-half) var(--Spacing-x3);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.listItem {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.guest {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1366px) {
|
||||
.details {
|
||||
padding-bottom: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.details p:nth-of-type(even) {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.header {
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.booking {
|
||||
gap: var(--Spacing-x3);
|
||||
grid-template-columns: auto 1fr;
|
||||
padding: var(--Spacing-x2) var(--Spacing-x3) var(--Spacing-x2)
|
||||
var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.roomDetails {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.guest {
|
||||
align-items: flex-end;
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import { LinkedReservation } from "./LinkedReservation"
|
||||
import Room from "./Room"
|
||||
|
||||
import styles from "./rooms.module.css"
|
||||
|
||||
import type { BookingConfirmationRoomsProps } from "@/types/components/hotelReservation/bookingConfirmation/rooms"
|
||||
|
||||
export default function Rooms({
|
||||
booking,
|
||||
mainRoom,
|
||||
linkedReservations,
|
||||
}: BookingConfirmationRoomsProps) {
|
||||
return (
|
||||
<section className={styles.rooms}>
|
||||
<Room
|
||||
booking={booking}
|
||||
img={mainRoom.images[0]}
|
||||
roomName={mainRoom.name}
|
||||
/>
|
||||
{linkedReservations?.map((reservation, idx) => (
|
||||
<LinkedReservation
|
||||
key={reservation.confirmationNumber}
|
||||
linkedReservation={reservation}
|
||||
roomIndex={idx}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.rooms {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x5);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
|
||||
import { notFound } from "next/navigation"
|
||||
import { Suspense } from "react"
|
||||
|
||||
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import TrackingSDK from "@/components/TrackingSDK"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import { invertedBedTypeMap } from "../utils"
|
||||
import Confirmation from "./Confirmation"
|
||||
|
||||
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||
import {
|
||||
TrackingChannelEnum,
|
||||
type TrackingSDKHotelInfo,
|
||||
type TrackingSDKPageData,
|
||||
type TrackingSDKPaymentInfo,
|
||||
} from "@/types/components/tracking"
|
||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
|
||||
export default async function BookingConfirmation({
|
||||
confirmationNumber,
|
||||
}: BookingConfirmationProps) {
|
||||
const lang = getLang()
|
||||
const bookingConfirmation = await getBookingConfirmation(confirmationNumber)
|
||||
|
||||
if (!bookingConfirmation) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const { booking, hotel, room } = bookingConfirmation
|
||||
|
||||
if (!room) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const arrivalDate = new Date(booking.checkInDate)
|
||||
const departureDate = new Date(booking.checkOutDate)
|
||||
|
||||
const breakfastPkgSelected = booking.packages.find(
|
||||
(pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST
|
||||
)
|
||||
|
||||
const breakfastAncillary = breakfastPkgSelected && {
|
||||
hotelid: hotel.operaId,
|
||||
productName: "BreakfastAdult",
|
||||
productCategory: "", // TODO: Add category
|
||||
productId: breakfastPkgSelected.code ?? "",
|
||||
productPrice: +breakfastPkgSelected.unitPrice,
|
||||
productUnits: booking.adults,
|
||||
productPoints: 0,
|
||||
productType: "food",
|
||||
}
|
||||
|
||||
const initialPageTrackingData: TrackingSDKPageData = {
|
||||
pageId: "booking-confirmation",
|
||||
domainLanguage: lang,
|
||||
channel: TrackingChannelEnum["hotelreservation"],
|
||||
pageName: `hotelreservation|confirmation`,
|
||||
siteSections: `hotelreservation|confirmation`,
|
||||
pageType: "confirmation",
|
||||
siteVersion: "new-web",
|
||||
}
|
||||
|
||||
const initialHotelsTrackingData: TrackingSDKHotelInfo = {
|
||||
arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
|
||||
departureDate: format(departureDate, "yyyy-MM-dd"),
|
||||
noOfAdults: booking.adults,
|
||||
noOfChildren: booking.childrenAges?.length,
|
||||
ageOfChildren: booking.childrenAges?.join(","),
|
||||
childBedPreference: booking?.childBedPreferences
|
||||
?.flatMap((c) => Array(c.quantity).fill(invertedBedTypeMap[c.bedType]))
|
||||
.join("|"),
|
||||
noOfRooms: 1, // // TODO: Handle multiple rooms
|
||||
duration: differenceInCalendarDays(departureDate, arrivalDate),
|
||||
leadTime: differenceInCalendarDays(arrivalDate, new Date()),
|
||||
searchType: "hotel",
|
||||
bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday",
|
||||
country: hotel?.address.country,
|
||||
hotelID: hotel.operaId,
|
||||
region: hotel?.address.city,
|
||||
rateCode: booking.rateDefinition.rateCode ?? undefined,
|
||||
//rateCodeType: , //TODO: Add when available in API. "regular, promotion, corporate etx",
|
||||
rateCodeName: booking.rateDefinition.title ?? undefined,
|
||||
rateCodeCancellationRule:
|
||||
booking.rateDefinition?.cancellationText ?? undefined,
|
||||
revenueCurrencyCode: booking.currencyCode,
|
||||
breakfastOption: booking.rateDefinition.breakfastIncluded
|
||||
? "breakfast buffet"
|
||||
: "no breakfast",
|
||||
totalPrice: booking.totalPrice,
|
||||
//specialRoomType: getSpecialRoomType(booking.packages), TODO: Add
|
||||
//roomTypeName: booking.roomTypeCode ?? undefined, TODO: Do we get the name?
|
||||
bedType: room?.bedType.name,
|
||||
roomTypeCode: booking.roomTypeCode ?? undefined,
|
||||
roomPrice: booking.roomPrice,
|
||||
bnr: booking.confirmationNumber ?? undefined,
|
||||
ancillaries: breakfastAncillary ? [breakfastAncillary] : [],
|
||||
}
|
||||
|
||||
const paymentInfo: TrackingSDKPaymentInfo = {
|
||||
paymentStatus: "confirmed",
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Confirmation booking={booking} hotel={hotel} room={room} />
|
||||
<Suspense fallback={null}>
|
||||
<TrackingSDK
|
||||
pageData={initialPageTrackingData}
|
||||
hotelInfo={initialHotelsTrackingData}
|
||||
paymentInfo={paymentInfo}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user