feat: new booking confirmation page
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
|
||||
export default function Loading() {
|
||||
return <LoadingSpinner />
|
||||
}
|
||||
@@ -1,7 +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);
|
||||
margin: 0 auto;
|
||||
width: min(calc(100dvw - (var(--Spacing-x3) * 2)), 948px);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { Suspense } from "react"
|
||||
|
||||
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import Details from "@/components/HotelReservation/BookingConfirmation/Details"
|
||||
import Header from "@/components/HotelReservation/BookingConfirmation/Header"
|
||||
import TotalPrice from "@/components/HotelReservation/BookingConfirmation/TotalPrice"
|
||||
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 LoadingSpinner from "@/components/LoadingSpinner"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
|
||||
import styles from "./page.module.css"
|
||||
@@ -18,10 +26,27 @@ export default async function BookingConfirmationPage({
|
||||
const { confirmationNumber } = searchParams
|
||||
|
||||
return (
|
||||
<div className={styles.main}>
|
||||
<Header confirmationNumber={confirmationNumber} />
|
||||
<Details confirmationNumber={confirmationNumber} />
|
||||
<TotalPrice confirmationNumber={confirmationNumber} />
|
||||
<main className={styles.main}>
|
||||
<Suspense fallback={<LoadingSpinner fullPage />}>
|
||||
<Header confirmationNumber={searchParams.confirmationNumber} />
|
||||
<div className={styles.booking}>
|
||||
<Rooms confirmationNumber={searchParams.confirmationNumber} />
|
||||
<PaymentDetails
|
||||
confirmationNumber={searchParams.confirmationNumber}
|
||||
/>
|
||||
<Divider color="primaryLightSubtle" />
|
||||
<HotelDetails confirmationNumber={searchParams.confirmationNumber} />
|
||||
<Promos />
|
||||
<div className={styles.mobileReceipt}>
|
||||
<Receipt confirmationNumber={searchParams.confirmationNumber} />
|
||||
</div>
|
||||
</div>
|
||||
<aside className={styles.aside}>
|
||||
<SidePanel variant="receipt">
|
||||
<Receipt confirmationNumber={searchParams.confirmationNumber} />
|
||||
</SidePanel>
|
||||
</aside>
|
||||
</Suspense>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
.layout {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
min-height: 100dvh;
|
||||
padding: 80px 0 160px;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import styles from "./layout.module.css"
|
||||
|
||||
// route groups needed as layouts have different bgc
|
||||
export default function ConfirmedBookingLayout({
|
||||
children,
|
||||
}: React.PropsWithChildren) {
|
||||
return <div className={styles.layout}>{children}</div>
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
|
||||
export default function Loading() {
|
||||
return <LoadingSpinner fullPage />
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import styles from "./layout.module.css"
|
||||
|
||||
import { LangParams, LayoutArgs } from "@/types/params"
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import styles from "./layout.module.css"
|
||||
|
||||
import { LangParams, LayoutArgs } from "@/types/params"
|
||||
@@ -12,9 +8,6 @@ export default function HotelReservationLayout({
|
||||
}: React.PropsWithChildren<LayoutArgs<LangParams>> & {
|
||||
sidePeek: React.ReactNode
|
||||
}) {
|
||||
if (!env.ENABLE_BOOKING_FLOW) {
|
||||
return notFound()
|
||||
}
|
||||
return (
|
||||
<div className={styles.layout}>
|
||||
{children}
|
||||
|
||||
@@ -17,7 +17,8 @@ import HistoryStateManager from "@/components/HotelReservation/EnterDetails/Hist
|
||||
import Payment from "@/components/HotelReservation/EnterDetails/Payment"
|
||||
import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion"
|
||||
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
|
||||
import Summary from "@/components/HotelReservation/EnterDetails/Summary"
|
||||
import DesktopSummary from "@/components/HotelReservation/EnterDetails/Summary/Desktop"
|
||||
import MobileSummary from "@/components/HotelReservation/EnterDetails/Summary/Mobile"
|
||||
import {
|
||||
generateChildrenString,
|
||||
getQueryParamsForEnterDetails,
|
||||
@@ -140,6 +141,13 @@ export default async function StepPage({
|
||||
}
|
||||
: undefined
|
||||
|
||||
const summary = {
|
||||
cancellationText: roomAvailability.cancellationText,
|
||||
isMember: !!user,
|
||||
rateDetails: roomAvailability.rateDetails,
|
||||
roomType: roomAvailability.selectedRoom.roomType,
|
||||
}
|
||||
|
||||
return (
|
||||
<EnterDetailsProvider
|
||||
bedTypes={roomAvailability.bedTypes}
|
||||
@@ -220,16 +228,8 @@ export default async function StepPage({
|
||||
</section>
|
||||
</div>
|
||||
<aside className={styles.summary}>
|
||||
<Summary
|
||||
adults={adults}
|
||||
fromDate={fromDate}
|
||||
hotelId={hotelId}
|
||||
kids={children}
|
||||
packageCodes={packageCodes}
|
||||
rateCode={rateCode}
|
||||
roomTypeCode={roomTypeCode}
|
||||
toDate={toDate}
|
||||
/>
|
||||
<MobileSummary {...summary} />
|
||||
<DesktopSummary {...summary} />
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
12
app/[lang]/(live)/(public)/hotelreservation/layout.tsx
Normal file
12
app/[lang]/(live)/(public)/hotelreservation/layout.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
|
||||
export default function HotelReservationLayout({
|
||||
children,
|
||||
}: React.PropsWithChildren) {
|
||||
if (!env.ENABLE_BOOKING_FLOW) {
|
||||
return notFound()
|
||||
}
|
||||
return <>{children}</>
|
||||
}
|
||||
@@ -104,6 +104,7 @@
|
||||
--max-width-text-block: 49.5rem;
|
||||
--current-mobile-site-header-height: 70.047px;
|
||||
--max-width-navigation: 89.5rem;
|
||||
|
||||
--max-width-spacing: calc(var(--Layout-Mobile-Margin-Margin-min) * 2);
|
||||
--max-width-page: min(
|
||||
calc(100dvw - var(--max-width-spacing)),
|
||||
@@ -156,6 +157,7 @@ ul {
|
||||
:root {
|
||||
--max-width-spacing: calc(var(--Layout-Tablet-Margin-Margin-min) * 2);
|
||||
}
|
||||
|
||||
body.overflow-hidden {
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
.details {
|
||||
background-color: var(--Base-Surface-Subtle-Normal);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x3);
|
||||
grid-area: details;
|
||||
padding: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.details {
|
||||
padding: var(--Spacing-x3) var(--Spacing-x3) var(--Spacing-x2);
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { dt } from "@/lib/dt"
|
||||
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import styles from "./details.module.css"
|
||||
|
||||
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||
|
||||
export default async function Details({
|
||||
confirmationNumber,
|
||||
}: BookingConfirmationProps) {
|
||||
const intl = await getIntl()
|
||||
const lang = getLang()
|
||||
const { booking } = await getBookingConfirmation(confirmationNumber)
|
||||
|
||||
const fromDate = dt(booking.checkInDate).locale(lang)
|
||||
const toDate = dt(booking.checkOutDate).locale(lang)
|
||||
|
||||
return (
|
||||
<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>N/A</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body>{intl.formatMessage({ id: "Cancellation policy" })}</Body>
|
||||
<Body>{booking.rateDefinition.cancellationText}</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body>{intl.formatMessage({ id: "Rebooking" })}</Body>
|
||||
<Body>N/A</Body>
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
display: grid;
|
||||
grid-area: actions;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
@@ -10,6 +11,5 @@
|
||||
grid-auto-columns: auto;
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns: auto;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,3 +16,9 @@
|
||||
.body {
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.header {
|
||||
padding-bottom: var(--Spacing-x4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,74 @@
|
||||
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
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 { getIntl } from "@/i18n"
|
||||
|
||||
import styles from "./hotelDetails.module.css"
|
||||
|
||||
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||
|
||||
export default async function HotelDetails({
|
||||
confirmationNumber,
|
||||
}: BookingConfirmationProps) {
|
||||
const intl = await getIntl()
|
||||
const { hotel } = await getBookingConfirmation(confirmationNumber)
|
||||
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">
|
||||
{hotel.address.streetAddress}, {hotel.address.zipCode}{" "}
|
||||
{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>N/A</li>
|
||||
</ul>
|
||||
</Toast>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
.imageContainer {
|
||||
align-items: center;
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
display: flex;
|
||||
grid-area: image;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import Image from "@/components/Image"
|
||||
|
||||
import styles from "./image.module.css"
|
||||
|
||||
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||
|
||||
export default async function HotelImage({
|
||||
confirmationNumber,
|
||||
}: BookingConfirmationProps) {
|
||||
const { hotel } = await getBookingConfirmation(confirmationNumber)
|
||||
return (
|
||||
<aside className={styles.imageContainer}>
|
||||
<Image
|
||||
alt={hotel.hotelContent.images.metaData.altText}
|
||||
height={256}
|
||||
src={hotel.hotelContent.images.imageSizes.medium}
|
||||
title={hotel.hotelContent.images.metaData.title}
|
||||
width={256}
|
||||
/>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { dt } from "@/lib/dt"
|
||||
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
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 { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import styles from "./paymentDetails.module.css"
|
||||
|
||||
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||
|
||||
export default async function PaymentDetails({
|
||||
confirmationNumber,
|
||||
}: BookingConfirmationProps) {
|
||||
const intl = await getIntl()
|
||||
const lang = getLang()
|
||||
const { booking } = await getBookingConfirmation(confirmationNumber)
|
||||
return (
|
||||
<div className={styles.details}>
|
||||
<Subtitle color="uiTextHighContrast" type="two">
|
||||
{intl.formatMessage({ id: "Payment details" })}
|
||||
</Subtitle>
|
||||
<div className={styles.payment}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatNumber(booking.totalPrice, {
|
||||
currency: booking.currencyCode,
|
||||
style: "currency",
|
||||
})}{" "}
|
||||
{intl.formatMessage({ id: "has been paid" })}
|
||||
</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,23 @@
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
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, text, title }: PromoProps) {
|
||||
return (
|
||||
<article className={styles.promo}>
|
||||
<Title color="white" level="h4">
|
||||
{title}
|
||||
</Title>
|
||||
<Body className={styles.text} color="white" textAlign="center">
|
||||
{text}
|
||||
</Body>
|
||||
<Button intent="primary" size="small" theme="primaryStrong">
|
||||
{buttonText}
|
||||
</Button>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
.promo:nth-of-type(1) {
|
||||
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(""); uncomment and add image once we have it */
|
||||
}
|
||||
|
||||
.promo:nth-of-type(2) {
|
||||
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(""); uncomment and add image once we have it */
|
||||
}
|
||||
|
||||
.text {
|
||||
max-width: 400px;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import Promo from "./Promo"
|
||||
|
||||
import styles from "./promos.module.css"
|
||||
|
||||
export default async function Promos() {
|
||||
const intl = await getIntl()
|
||||
return (
|
||||
<div className={styles.promos}>
|
||||
<Promo
|
||||
buttonText={intl.formatMessage({ id: "View and buy add-ons" })}
|
||||
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" })}
|
||||
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,143 @@
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import { ChevronRightSmallIcon, InfoCircleIcon } 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 { getIntl } from "@/i18n"
|
||||
import { getBookedHotelRoom } from "@/utils/getBookedHotelRoom"
|
||||
|
||||
import styles from "./receipt.module.css"
|
||||
|
||||
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
|
||||
export default async function Receipt({
|
||||
confirmationNumber,
|
||||
}: BookingConfirmationProps) {
|
||||
const intl = await getIntl()
|
||||
const { booking, hotel } = await getBookingConfirmation(confirmationNumber)
|
||||
const roomAndBed = getBookedHotelRoom(hotel, booking.roomTypeCode ?? "")
|
||||
if (!roomAndBed) {
|
||||
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: "Summary" })}</Subtitle>
|
||||
<article className={styles.room}>
|
||||
<header className={styles.roomHeader}>
|
||||
<Body color="uiTextHighContrast">{roomAndBed.name}</Body>
|
||||
{booking.rateDefinition.isMemberRate ? (
|
||||
<div className={styles.memberPrice}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
<s>N/A</s>
|
||||
</Body>
|
||||
<Body color="red">
|
||||
{intl.formatNumber(booking.roomPrice, {
|
||||
currency: booking.currencyCode,
|
||||
style: "currency",
|
||||
})}
|
||||
</Body>
|
||||
</div>
|
||||
) : (
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatNumber(booking.roomPrice, {
|
||||
currency: booking.currencyCode,
|
||||
style: "currency",
|
||||
})}
|
||||
</Body>
|
||||
)}
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "booking.adults" },
|
||||
{
|
||||
totalAdults: booking.adults,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{booking.rateDefinition.cancellationText}
|
||||
</Caption>
|
||||
<Link
|
||||
color="peach80"
|
||||
href=""
|
||||
size="small"
|
||||
textDecoration="underline"
|
||||
variant="icon"
|
||||
>
|
||||
{intl.formatMessage({ id: "Reservation policy" })}
|
||||
<InfoCircleIcon color="peach80" />
|
||||
</Link>
|
||||
</header>
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{roomAndBed.bedType.description}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatNumber(0, {
|
||||
currency: booking.currencyCode,
|
||||
style: "currency",
|
||||
})}
|
||||
</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">
|
||||
{intl.formatNumber(breakfastPkgSelected.totalPrice, {
|
||||
currency: breakfastPkgSelected.currency,
|
||||
style: "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">
|
||||
{intl.formatNumber(booking.totalPrice, {
|
||||
currency: booking.currencyCode,
|
||||
style: "currency",
|
||||
})}
|
||||
</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." })} N/A EUR
|
||||
</Caption>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.receipt {
|
||||
padding: var(--Spacing-x3);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,139 @@
|
||||
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 { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import styles from "./room.module.css"
|
||||
|
||||
export default function Room() {
|
||||
return <article className={styles.room}></article>
|
||||
import type { RoomProps } from "@/types/components/hotelReservation/bookingConfirmation/room"
|
||||
|
||||
export default async function Room({ booking, img, roomName }: RoomProps) {
|
||||
const intl = await getIntl()
|
||||
const lang = getLang()
|
||||
|
||||
const fromDate = dt(booking.checkInDate).locale(lang)
|
||||
const toDate = dt(booking.checkOutDate).locale(lang)
|
||||
return (
|
||||
<article className={styles.room}>
|
||||
<header className={styles.header}>
|
||||
<div>
|
||||
{/* <Subtitle color="mainGrey60" type="two">
|
||||
{intl.formatMessage({ id: "Room" })} 1
|
||||
</Subtitle> */}
|
||||
<Subtitle color="uiTextHighContrast" type="two">
|
||||
{`${intl.formatMessage({ id: "Reservation number" })} ${booking.confirmationNumber}`}
|
||||
</Subtitle>
|
||||
</div>
|
||||
<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">
|
||||
{`${fromDate.format("ddd, D MMM")} ${intl.formatMessage({ id: "from" })} ${fromDate.format("HH:mm")}`}
|
||||
</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Check-out" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{`${toDate.format("ddd, D MMM")} ${intl.formatMessage({ id: "from" })} ${toDate.format("HH:mm")}`}
|
||||
</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Breakfast" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">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">N/A</Body>
|
||||
</li>
|
||||
</ul>
|
||||
<div className={styles.guest}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Main guest" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{`${booking.guest.firstName} ${booking.guest.lastName}`}
|
||||
</Body>
|
||||
{booking.guest.membershipNumber ? (
|
||||
<Body color="uiTextHighContrast">
|
||||
{`${intl.formatMessage({ id: "Friend no." })} ${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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import { getBookedHotelRoom } from "@/utils/getBookedHotelRoom"
|
||||
|
||||
import Room from "./Room"
|
||||
|
||||
import styles from "./rooms.module.css"
|
||||
|
||||
export default function Rooms() {
|
||||
return <section className={styles.rooms}></section>
|
||||
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||
|
||||
export default async function Rooms({
|
||||
confirmationNumber,
|
||||
}: BookingConfirmationProps) {
|
||||
const { booking, hotel } = await getBookingConfirmation(confirmationNumber)
|
||||
const roomAndBed = getBookedHotelRoom(hotel, booking.roomTypeCode ?? "")
|
||||
if (!roomAndBed) {
|
||||
return notFound()
|
||||
}
|
||||
return (
|
||||
<section className={styles.rooms}>
|
||||
<Room
|
||||
booking={booking}
|
||||
img={roomAndBed.images[0]}
|
||||
roomName={roomAndBed.name}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
.rooms {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x9);
|
||||
grid-area: booking;
|
||||
gap: var(--Spacing-x5);
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import styles from "./summary.module.css"
|
||||
|
||||
export default function Summary() {
|
||||
return <aside className={styles.summary}>SUMMARY</aside>
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
.summary {
|
||||
background-color: hotpink;
|
||||
grid-area: summary;
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import {
|
||||
CoffeeIcon,
|
||||
DiscountIcon,
|
||||
DoorClosedIcon,
|
||||
PriceTagIcon,
|
||||
} from "@/components/Icons"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import styles from "./totalPrice.module.css"
|
||||
|
||||
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
|
||||
export default async function TotalPrice({
|
||||
confirmationNumber,
|
||||
}: BookingConfirmationProps) {
|
||||
const intl = await getIntl()
|
||||
const { booking } = await getBookingConfirmation(confirmationNumber)
|
||||
|
||||
const totalPrice = intl.formatNumber(booking.totalPrice, {
|
||||
currency: booking.currencyCode,
|
||||
style: "currency",
|
||||
})
|
||||
const breakfastPackage = booking.packages.find(
|
||||
(p) => p.code === BreakfastPackageEnum.REGULAR_BREAKFAST
|
||||
)
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<hgroup>
|
||||
<Subtitle color="uiTextPlaceholder" type="two">
|
||||
{intl.formatMessage({ id: "Total price" })}
|
||||
</Subtitle>
|
||||
<Subtitle color="uiTextHighContrast" type="two">
|
||||
{totalPrice} (~ EUR)
|
||||
</Subtitle>
|
||||
</hgroup>
|
||||
<div className={styles.items}>
|
||||
<div>
|
||||
<DoorClosedIcon />
|
||||
<Body color="uiTextPlaceholder">
|
||||
{`${intl.formatMessage({ id: "Room" })}, ${intl.formatMessage({ id: "booking.nights" }, { totalNights: 1 })}`}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{totalPrice}</Body>
|
||||
</div>
|
||||
<div>
|
||||
<CoffeeIcon />
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Breakfast" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{breakfastPackage
|
||||
? intl.formatNumber(breakfastPackage.totalPrice, {
|
||||
currency: breakfastPackage.currency,
|
||||
style: "currency",
|
||||
})
|
||||
: intl.formatMessage({ id: "No breakfast" })}
|
||||
</Body>
|
||||
</div>
|
||||
<div>
|
||||
<DiscountIcon />
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Member discount" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">N/A</Body>
|
||||
</div>
|
||||
<div>
|
||||
<PriceTagIcon height={20} width={20} />
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Points used" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">N/A</Body>
|
||||
</div>
|
||||
</div>
|
||||
<Divider color="primaryLightSubtle" />
|
||||
<div className={styles.items}>
|
||||
<div>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Price excl VAT" })}
|
||||
</Caption>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatNumber(booking.totalPriceExVat, {
|
||||
currency: booking.currencyCode,
|
||||
style: "currency",
|
||||
})}
|
||||
</Caption>
|
||||
</div>
|
||||
<div>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "VAT" })}
|
||||
</Caption>
|
||||
<Caption color="uiTextHighContrast">{booking.vatPercentage}%</Caption>
|
||||
</div>
|
||||
<div>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "VAT amount" })}
|
||||
</Caption>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatNumber(booking.vatAmount, {
|
||||
currency: booking.currencyCode,
|
||||
style: "currency",
|
||||
})}
|
||||
</Caption>
|
||||
</div>
|
||||
<div>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Price incl VAT" })}
|
||||
</Caption>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatNumber(booking.totalPrice, {
|
||||
currency: booking.currencyCode,
|
||||
style: "currency",
|
||||
})}
|
||||
</Caption>
|
||||
</div>
|
||||
<div>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Payment method" })}
|
||||
</Caption>
|
||||
<Caption color="uiTextHighContrast">N/A</Caption>
|
||||
</div>
|
||||
<div>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Payment status" })}
|
||||
</Caption>
|
||||
<Caption color="uiTextHighContrast">N/A</Caption>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
.container {
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x3);
|
||||
padding: var(--Spacing-x2) var(--Spacing-x3) var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.items {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3) var(--Spacing-x1);
|
||||
grid-template-columns: repeat(4, minmax(100px, 1fr));
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
import { profile } from "@/constants/routes/myPages"
|
||||
import { dt } from "@/lib/dt"
|
||||
import {
|
||||
getBookingConfirmation,
|
||||
getProfileSafely,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import { CreditCardAddIcon, EditIcon, PersonIcon } from "@/components/Icons"
|
||||
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 { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import styles from "./summary.module.css"
|
||||
|
||||
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
|
||||
export default async function Summary({
|
||||
confirmationNumber,
|
||||
}: BookingConfirmationProps) {
|
||||
const intl = await getIntl()
|
||||
const lang = getLang()
|
||||
const { booking, hotel } = await getBookingConfirmation(confirmationNumber)
|
||||
const user = await getProfileSafely()
|
||||
const { firstName, lastName } = booking.guest
|
||||
const membershipNumber = user?.membership?.membershipNumber
|
||||
const totalNights = dt(booking.checkOutDate.setHours(0, 0, 0)).diff(
|
||||
dt(booking.checkInDate.setHours(0, 0, 0)),
|
||||
"days"
|
||||
)
|
||||
|
||||
const breakfastPackage = booking.packages.find(
|
||||
(pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST
|
||||
)
|
||||
return (
|
||||
<div className={styles.summary}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.textContainer}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Guest" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{`${firstName} ${lastName}`}</Body>
|
||||
{membershipNumber ? (
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "membership.no" },
|
||||
{ membershipNumber }
|
||||
)}
|
||||
</Body>
|
||||
) : null}
|
||||
<Body color="uiTextHighContrast">{booking.guest.email}</Body>
|
||||
<Body color="uiTextHighContrast">{booking.guest.phoneNumber}</Body>
|
||||
</div>
|
||||
{user ? (
|
||||
<Link className={styles.link} href={profile[lang]} variant="icon">
|
||||
<PersonIcon color="baseButtonTextOnFillNormal" />
|
||||
<Caption color="burgundy" type="bold">
|
||||
{intl.formatMessage({ id: "Go to profile" })}
|
||||
</Caption>
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
<Divider color="primaryLightSubtle" />
|
||||
<div className={styles.container}>
|
||||
<div className={styles.textContainer}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Payment" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "guest.paid" },
|
||||
{
|
||||
amount: intl.formatNumber(booking.totalPrice),
|
||||
currency: booking.currencyCode,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">Date information N/A</Body>
|
||||
<Body color="uiTextHighContrast">Card information N/A</Body>
|
||||
</div>
|
||||
{/* # href until more info */}
|
||||
{user ? (
|
||||
<Link className={styles.link} href="#" variant="icon">
|
||||
<CreditCardAddIcon color="baseButtonTextOnFillNormal" />
|
||||
<Caption color="burgundy" type="bold">
|
||||
{intl.formatMessage({ id: "Save card to profile" })}
|
||||
</Caption>
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
<Divider color="primaryLightSubtle" />
|
||||
<div className={styles.container}>
|
||||
<div className={styles.textContainer}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Booking" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
N/A, {intl.formatMessage({ id: "booking.nights" }, { totalNights })}
|
||||
,{" "}
|
||||
{intl.formatMessage(
|
||||
{ id: "booking.adults" },
|
||||
{ totalAdults: booking.adults }
|
||||
)}
|
||||
</Body>
|
||||
{breakfastPackage ? (
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "Breakfast added" })}
|
||||
</Body>
|
||||
) : null}
|
||||
<Body color="uiTextHighContrast">Bedtype N/A</Body>
|
||||
</div>
|
||||
{/* # href until more info */}
|
||||
<Link className={styles.link} href="#" variant="icon">
|
||||
<EditIcon color="baseButtonTextOnFillNormal" />
|
||||
<Caption color="burgundy" type="bold">
|
||||
{intl.formatMessage({ id: "Manage booking" })}
|
||||
</Caption>
|
||||
</Link>
|
||||
</div>
|
||||
<Divider color="primaryLightSubtle" />
|
||||
<div className={styles.container}>
|
||||
<div className={styles.textContainer}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Hotel" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{hotel.name}</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{`${hotel.address.streetAddress}, ${hotel.address.zipCode} ${hotel.address.city}`}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{hotel.contactInformation.phoneNumber}
|
||||
</Body>
|
||||
<Caption color="uiTextMediumContrast" className={styles.latLong}>
|
||||
{`${intl.formatMessage({ id: "Longitude" }, { long: hotel.location.longitude })} ∙ ${intl.formatMessage({ id: "Latitude" }, { lat: hotel.location.latitude })}`}
|
||||
</Caption>
|
||||
</div>
|
||||
<div className={styles.hotelLinks}>
|
||||
<Link color="peach80" href={hotel.contactInformation.websiteUrl}>
|
||||
{hotel.contactInformation.websiteUrl}
|
||||
</Link>
|
||||
<Link
|
||||
color="peach80"
|
||||
href={`mailto:${hotel.contactInformation.email}`}
|
||||
>
|
||||
{hotel.contactInformation.email}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
.summary {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.container,
|
||||
.textContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.container {
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.textContainer {
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.container .textContainer .latLong {
|
||||
padding-top: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.hotelLinks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.summary .container .link {
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
|
||||
import Summary from "@/components/HotelReservation/Summary"
|
||||
import { SummaryBottomSheet } from "@/components/HotelReservation/Summary/BottomSheet"
|
||||
|
||||
import styles from "./summary.module.css"
|
||||
|
||||
import type { ClientSummaryProps } from "@/types/components/hotelReservation/enterDetails/summary"
|
||||
import type { DetailsState } from "@/types/stores/enter-details"
|
||||
|
||||
function storeSelector(state: DetailsState) {
|
||||
return {
|
||||
bedType: state.bedType,
|
||||
breakfast: state.breakfast,
|
||||
fromDate: state.booking.fromDate,
|
||||
join: state.guest.join,
|
||||
membershipNo: state.guest.membershipNo,
|
||||
packages: state.packages,
|
||||
roomRate: state.roomRate,
|
||||
roomPrice: state.roomPrice,
|
||||
toDate: state.booking.toDate,
|
||||
toggleSummaryOpen: state.actions.toggleSummaryOpen,
|
||||
totalPrice: state.totalPrice,
|
||||
}
|
||||
}
|
||||
|
||||
export default function ClientSummary({
|
||||
adults,
|
||||
cancellationText,
|
||||
isMember,
|
||||
kids,
|
||||
memberRate,
|
||||
rateDetails,
|
||||
roomType,
|
||||
}: ClientSummaryProps) {
|
||||
const {
|
||||
bedType,
|
||||
breakfast,
|
||||
fromDate,
|
||||
join,
|
||||
membershipNo,
|
||||
packages,
|
||||
roomPrice,
|
||||
toDate,
|
||||
toggleSummaryOpen,
|
||||
totalPrice,
|
||||
} = useEnterDetailsStore(storeSelector)
|
||||
|
||||
const showMemberPrice = !!(isMember && memberRate) || join || !!membershipNo
|
||||
const room = {
|
||||
adults,
|
||||
cancellationText,
|
||||
children: kids,
|
||||
packages,
|
||||
rateDetails,
|
||||
roomPrice,
|
||||
roomType,
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.mobileSummary}>
|
||||
<SummaryBottomSheet>
|
||||
<div className={styles.summary}>
|
||||
<Summary
|
||||
bedType={bedType}
|
||||
breakfast={breakfast}
|
||||
fromDate={fromDate}
|
||||
showMemberPrice={showMemberPrice}
|
||||
room={room}
|
||||
toDate={toDate}
|
||||
toggleSummaryOpen={toggleSummaryOpen}
|
||||
totalPrice={totalPrice}
|
||||
/>
|
||||
</div>
|
||||
</SummaryBottomSheet>
|
||||
</div>
|
||||
<div className={styles.desktopSummary}>
|
||||
<div className={styles.hider} />
|
||||
<div className={styles.summary}>
|
||||
<Summary
|
||||
bedType={bedType}
|
||||
breakfast={breakfast}
|
||||
fromDate={fromDate}
|
||||
showMemberPrice={showMemberPrice}
|
||||
room={room}
|
||||
toDate={toDate}
|
||||
totalPrice={totalPrice}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.shadow} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
13
components/HotelReservation/EnterDetails/Summary/Desktop.tsx
Normal file
13
components/HotelReservation/EnterDetails/Summary/Desktop.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import SidePanel from "@/components/HotelReservation/SidePanel"
|
||||
|
||||
import SummaryUI from "./UI"
|
||||
|
||||
import type { SummaryProps } from "@/types/components/hotelReservation/summary"
|
||||
|
||||
export default function DesktopSummary(props: SummaryProps) {
|
||||
return (
|
||||
<SidePanel variant="summary">
|
||||
<SummaryUI {...props} />
|
||||
</SidePanel>
|
||||
)
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
|
||||
import styles from "./bottomSheet.module.css"
|
||||
|
||||
export function SummaryBottomSheet({ children }: PropsWithChildren) {
|
||||
export default function SummaryBottomSheet({ children }: PropsWithChildren) {
|
||||
const intl = useIntl()
|
||||
|
||||
const { isSummaryOpen, toggleSummaryOpen, totalPrice, isSubmittingDisabled } =
|
||||
@@ -0,0 +1,18 @@
|
||||
import SummaryUI from "../UI"
|
||||
import SummaryBottomSheet from "./BottomSheet"
|
||||
|
||||
import styles from "./mobile.module.css"
|
||||
|
||||
import type { SummaryProps } from "@/types/components/hotelReservation/summary"
|
||||
|
||||
export default function MobileSummary(props: SummaryProps) {
|
||||
return (
|
||||
<div className={styles.mobileSummary}>
|
||||
<SummaryBottomSheet>
|
||||
<div className={styles.wrapper}>
|
||||
<SummaryUI {...props} />
|
||||
</div>
|
||||
</SummaryBottomSheet>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
.mobileSummary {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1366px) {
|
||||
.wrapper {
|
||||
background-color: var(--Main-Grey-White);
|
||||
border-color: var(--Primary-Light-On-Surface-Divider-subtle);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-bottom: none;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.mobileSummary {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
|
||||
import { ArrowRightIcon, ChevronDownSmallIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
@@ -13,25 +14,58 @@ import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import styles from "./summary.module.css"
|
||||
import styles from "./ui.module.css"
|
||||
|
||||
import type { SummaryProps } from "@/types/components/hotelReservation/summary"
|
||||
import { CurrencyEnum } from "@/types/enums/currency"
|
||||
import type { DetailsState } from "@/types/stores/enter-details"
|
||||
|
||||
export default function Summary({
|
||||
bedType,
|
||||
breakfast,
|
||||
fromDate,
|
||||
showMemberPrice,
|
||||
room,
|
||||
toDate,
|
||||
toggleSummaryOpen,
|
||||
totalPrice,
|
||||
export function storeSelector(state: DetailsState) {
|
||||
return {
|
||||
bedType: state.bedType,
|
||||
booking: state.booking,
|
||||
breakfast: state.breakfast,
|
||||
join: state.guest.join,
|
||||
membershipNo: state.guest.membershipNo,
|
||||
packages: state.packages,
|
||||
roomRate: state.roomRate,
|
||||
roomPrice: state.roomPrice,
|
||||
toggleSummaryOpen: state.actions.toggleSummaryOpen,
|
||||
totalPrice: state.totalPrice,
|
||||
}
|
||||
}
|
||||
|
||||
export default function SummaryUI({
|
||||
cancellationText,
|
||||
isMember,
|
||||
rateDetails,
|
||||
roomType,
|
||||
}: SummaryProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
const diff = dt(toDate).diff(fromDate, "days")
|
||||
const {
|
||||
bedType,
|
||||
booking,
|
||||
breakfast,
|
||||
join,
|
||||
membershipNo,
|
||||
packages,
|
||||
roomPrice,
|
||||
roomRate,
|
||||
toggleSummaryOpen,
|
||||
totalPrice,
|
||||
} = useEnterDetailsStore(storeSelector)
|
||||
|
||||
const adults = booking.rooms[0].adults
|
||||
const children = booking.rooms[0].children
|
||||
|
||||
const showMemberPrice = !!(
|
||||
(isMember || join || membershipNo) &&
|
||||
roomRate.memberRate
|
||||
)
|
||||
|
||||
const diff = dt(booking.toDate).diff(booking.fromDate, "days")
|
||||
|
||||
const nights = intl.formatMessage(
|
||||
{ id: "booking.nights" },
|
||||
@@ -51,9 +85,9 @@ export default function Summary({
|
||||
{intl.formatMessage({ id: "Summary" })}
|
||||
</Subtitle>
|
||||
<Body className={styles.date} color="baseTextMediumContrast">
|
||||
{dt(fromDate).locale(lang).format("ddd, D MMM")}
|
||||
{dt(booking.fromDate).locale(lang).format("ddd, D MMM")}
|
||||
<ArrowRightIcon color="peach80" height={15} width={15} />
|
||||
{dt(toDate).locale(lang).format("ddd, D MMM")} ({nights})
|
||||
{dt(booking.toDate).locale(lang).format("ddd, D MMM")} ({nights})
|
||||
</Body>
|
||||
<Button
|
||||
intent="text"
|
||||
@@ -68,13 +102,13 @@ export default function Summary({
|
||||
<div className={styles.addOns}>
|
||||
<div>
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">{room.roomType}</Body>
|
||||
<Body color="uiTextHighContrast">{roomType}</Body>
|
||||
<Caption color={showMemberPrice ? "red" : "uiTextHighContrast"}>
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{
|
||||
amount: intl.formatNumber(room.roomPrice.local.price),
|
||||
currency: room.roomPrice.local.currency,
|
||||
amount: intl.formatNumber(roomPrice.local.price),
|
||||
currency: roomPrice.local.currency,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
@@ -82,20 +116,18 @@ export default function Summary({
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "booking.adults" },
|
||||
{ totalAdults: room.adults }
|
||||
{ totalAdults: adults }
|
||||
)}
|
||||
</Caption>
|
||||
{room.children?.length ? (
|
||||
{children?.length ? (
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "booking.children" },
|
||||
{ totalChildren: room.children.length }
|
||||
{ totalChildren: children.length }
|
||||
)}
|
||||
</Caption>
|
||||
) : null}
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{room.cancellationText}
|
||||
</Caption>
|
||||
<Caption color="uiTextMediumContrast">{cancellationText}</Caption>
|
||||
<Popover
|
||||
placement="bottom left"
|
||||
triggerContent={
|
||||
@@ -106,16 +138,16 @@ export default function Summary({
|
||||
>
|
||||
<aside className={styles.rateDetailsPopover}>
|
||||
<header>
|
||||
<Caption type="bold">{room.cancellationText}</Caption>
|
||||
<Caption type="bold">{cancellationText}</Caption>
|
||||
</header>
|
||||
{room.rateDetails?.map((detail, idx) => (
|
||||
{rateDetails?.map((detail, idx) => (
|
||||
<Caption key={`rateDetails-${idx}`}>{detail}</Caption>
|
||||
))}
|
||||
</aside>
|
||||
</Popover>
|
||||
</div>
|
||||
{room.packages
|
||||
? room.packages.map((roomPackage) => (
|
||||
{packages
|
||||
? packages.map((roomPackage) => (
|
||||
<div className={styles.entry} key={roomPackage.code}>
|
||||
<div>
|
||||
<Body color="uiTextHighContrast">
|
||||
@@ -147,7 +179,7 @@ export default function Summary({
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{ amount: "0", currency: room.roomPrice.local.currency }
|
||||
{ amount: "0", currency: roomPrice.local.currency }
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
@@ -161,7 +193,7 @@ export default function Summary({
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{ amount: "0", currency: room.roomPrice.local.currency }
|
||||
{ amount: "0", currency: roomPrice.local.currency }
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
@@ -1,57 +0,0 @@
|
||||
import { redirect } from "next/navigation"
|
||||
|
||||
import { selectRate } from "@/constants/routes/hotelReservation"
|
||||
import {
|
||||
getProfileSafely,
|
||||
getSelectedRoomAvailability,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import { generateChildrenString } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import ClientSummary from "./Client"
|
||||
|
||||
import type { SummaryPageProps } from "@/types/components/hotelReservation/summary"
|
||||
|
||||
export default async function Summary({
|
||||
adults,
|
||||
fromDate,
|
||||
hotelId,
|
||||
kids,
|
||||
packageCodes,
|
||||
rateCode,
|
||||
roomTypeCode,
|
||||
toDate,
|
||||
}: SummaryPageProps) {
|
||||
const lang = getLang()
|
||||
|
||||
const availability = await getSelectedRoomAvailability({
|
||||
adults,
|
||||
children: kids ? generateChildrenString(kids) : undefined,
|
||||
hotelId,
|
||||
packageCodes,
|
||||
rateCode,
|
||||
roomStayStartDate: fromDate,
|
||||
roomStayEndDate: toDate,
|
||||
roomTypeCode,
|
||||
})
|
||||
const user = await getProfileSafely()
|
||||
|
||||
if (!availability || !availability.selectedRoom) {
|
||||
console.error("No hotel or availability data", availability)
|
||||
// TODO: handle this case
|
||||
redirect(selectRate(lang))
|
||||
}
|
||||
|
||||
return (
|
||||
<ClientSummary
|
||||
adults={adults}
|
||||
cancellationText={availability.cancellationText}
|
||||
isMember={!!user}
|
||||
kids={kids}
|
||||
memberRate={availability.memberRate}
|
||||
rateDetails={availability.rateDetails}
|
||||
roomType={availability.selectedRoom.roomType}
|
||||
/>
|
||||
)
|
||||
}
|
||||
19
components/HotelReservation/SidePanel/index.tsx
Normal file
19
components/HotelReservation/SidePanel/index.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { sidePanelVariants } from "./variants"
|
||||
|
||||
import styles from "./sidePanel.module.css"
|
||||
|
||||
import type { SidePanelProps } from "@/types/components/hotelReservation/sidePanel"
|
||||
|
||||
export default function SidePanel({
|
||||
children,
|
||||
variant,
|
||||
}: React.PropsWithChildren<SidePanelProps>) {
|
||||
const classNames = sidePanelVariants({ variant })
|
||||
return (
|
||||
<div className={classNames}>
|
||||
<div className={styles.hider} />
|
||||
<div className={styles.wrapper}>{children}</div>
|
||||
<div className={styles.shadow} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,68 +1,62 @@
|
||||
.mobileSummary {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.desktopSummary {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.summary {
|
||||
background-color: var(--Main-Grey-White);
|
||||
|
||||
border-color: var(--Primary-Light-On-Surface-Divider-subtle);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-bottom: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.hider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidePanel,
|
||||
.hider,
|
||||
.shadow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.mobileSummary {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.desktopSummary {
|
||||
.sidePanel {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
margin-top: calc(0px - var(--Spacing-x9));
|
||||
}
|
||||
|
||||
.summary {
|
||||
margin-top: calc(0px - var(--Spacing-x9));
|
||||
}
|
||||
|
||||
.hider {
|
||||
display: block;
|
||||
position: sticky;
|
||||
}
|
||||
|
||||
.receipt .hider {
|
||||
background-color: var(--Main-Grey-White);
|
||||
height: 150px;
|
||||
margin-top: -78px;
|
||||
top: -40px;
|
||||
}
|
||||
|
||||
.summary .hider {
|
||||
background-color: var(--Scandic-Brand-Warm-White);
|
||||
height: 40px;
|
||||
margin-top: var(--Spacing-x4);
|
||||
top: calc(var(--booking-widget-desktop-height) - 6px);
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
background-color: var(--Main-Grey-White);
|
||||
border-color: var(--Primary-Light-On-Surface-Divider-subtle);
|
||||
border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-bottom: none;
|
||||
margin-top: calc(0px - var(--Spacing-x9));
|
||||
position: sticky;
|
||||
top: calc(
|
||||
var(--booking-widget-desktop-height) + var(--Spacing-x2) +
|
||||
var(--Spacing-x-half)
|
||||
);
|
||||
z-index: 9;
|
||||
border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0;
|
||||
margin-top: calc(0px - var(--Spacing-x9));
|
||||
}
|
||||
|
||||
.shadow {
|
||||
display: block;
|
||||
background-color: var(--Main-Grey-White);
|
||||
border-color: var(--Primary-Light-On-Surface-Divider-subtle);
|
||||
border-style: solid;
|
||||
border-left-width: 1px;
|
||||
border-right-width: 1px;
|
||||
border-top: none;
|
||||
border-style: solid;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.hider {
|
||||
border-top: none;
|
||||
display: block;
|
||||
background-color: var(--Scandic-Brand-Warm-White);
|
||||
position: sticky;
|
||||
top: calc(var(--booking-widget-desktop-height) - 6px);
|
||||
margin-top: var(--Spacing-x4);
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
15
components/HotelReservation/SidePanel/variants.ts
Normal file
15
components/HotelReservation/SidePanel/variants.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
import styles from "./sidePanel.module.css"
|
||||
|
||||
export const sidePanelVariants = cva(styles.sidePanel, {
|
||||
variants: {
|
||||
variant: {
|
||||
receipt: styles.receipt,
|
||||
summary: styles.summary,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "summary",
|
||||
},
|
||||
})
|
||||
@@ -16,7 +16,8 @@
|
||||
.breadcrumb {
|
||||
font-family: var(--typography-Footnote-Bold-fontFamily);
|
||||
font-size: var(--typography-Footnote-Bold-fontSize);
|
||||
font-weight: 500; /* var(--typography-Footnote-Bold-fontWeight); */
|
||||
font-weight: 500;
|
||||
/* var(--typography-Footnote-Bold-fontWeight); */
|
||||
letter-spacing: var(--typography-Footnote-Bold-letterSpacing);
|
||||
line-height: var(--typography-Footnote-Bold-lineHeight);
|
||||
}
|
||||
@@ -24,7 +25,8 @@
|
||||
.link.breadcrumb {
|
||||
font-family: var(--typography-Footnote-Bold-fontFamily);
|
||||
font-size: var(--typography-Footnote-Bold-fontSize);
|
||||
font-weight: 500; /* var(--typography-Footnote-Bold-fontWeight); */
|
||||
font-weight: 500;
|
||||
/* var(--typography-Footnote-Bold-fontWeight); */
|
||||
letter-spacing: var(--typography-Footnote-Bold-letterSpacing);
|
||||
line-height: var(--typography-Footnote-Bold-lineHeight);
|
||||
}
|
||||
@@ -159,12 +161,15 @@
|
||||
color: var(--Scandic-Peach-50);
|
||||
}
|
||||
|
||||
.peach80 {
|
||||
.peach80,
|
||||
.baseTextMediumContrast {
|
||||
color: var(--Base-Text-Medium-contrast);
|
||||
}
|
||||
|
||||
.peach80:hover,
|
||||
.peach80:active {
|
||||
.peach80:active,
|
||||
.baseTextMediumContrast:hover,
|
||||
.baseTextMediumContrast:active {
|
||||
color: var(--Base-Text-High-contrast);
|
||||
}
|
||||
|
||||
@@ -235,6 +240,7 @@
|
||||
letter-spacing: var(--typography-Caption-Bold-letterSpacing);
|
||||
line-height: var(--typography-Caption-Bold-lineHeight);
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-family: var(--typography-Body-Bold-fontFamily);
|
||||
font-size: var(--typography-Body-Bold-fontSize);
|
||||
|
||||
@@ -9,6 +9,7 @@ export const linkVariants = cva(styles.link, {
|
||||
},
|
||||
color: {
|
||||
baseButtonTextOnFillNormal: styles.baseButtonTextOnFillNormal,
|
||||
baseTextMediumContrast: styles.baseTextMediumContrast,
|
||||
black: styles.black,
|
||||
burgundy: styles.burgundy,
|
||||
none: "",
|
||||
|
||||
@@ -86,3 +86,7 @@
|
||||
.baseTextDisabled {
|
||||
color: var(--Base-Text-Disabled);
|
||||
}
|
||||
|
||||
.mainGrey60 {
|
||||
color: var(--Main-Grey-60);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ const config = {
|
||||
uiTextMediumContrast: styles.uiTextMediumContrast,
|
||||
uiTextPlaceholder: styles.uiTextPlaceholder,
|
||||
red: styles.red,
|
||||
mainGrey60: styles.mainGrey60,
|
||||
},
|
||||
textAlign: {
|
||||
center: styles.center,
|
||||
|
||||
@@ -32,7 +32,7 @@ function getIcon(variant: ToastsProps["variant"]) {
|
||||
}
|
||||
}
|
||||
|
||||
export function Toast({ message, onClose, variant }: ToastsProps) {
|
||||
export function Toast({ children, message, onClose, variant }: ToastsProps) {
|
||||
const className = toastVariants({ variant })
|
||||
const Icon = getIcon(variant)
|
||||
return (
|
||||
@@ -40,10 +40,16 @@ export function Toast({ message, onClose, variant }: ToastsProps) {
|
||||
<div className={styles.iconContainer}>
|
||||
{Icon && <Icon color="white" height={24} width={24} />}
|
||||
</div>
|
||||
{message ? (
|
||||
<Body className={styles.message}>{message}</Body>
|
||||
) : (
|
||||
<div className={styles.content}>{children}</div>
|
||||
)}
|
||||
{onClose ? (
|
||||
<Button onClick={onClose} variant="icon" intent="text">
|
||||
<CloseLargeIcon />
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x3)
|
||||
var(--Spacing-x-one-and-half) var(--Spacing-x2);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.toast {
|
||||
width: var(--width);
|
||||
|
||||
@@ -2,9 +2,16 @@ import { toastVariants } from "./variants"
|
||||
|
||||
import type { VariantProps } from "class-variance-authority"
|
||||
|
||||
export interface ToastsProps
|
||||
extends Omit<React.AnchorHTMLAttributes<HTMLDivElement>, "color">,
|
||||
VariantProps<typeof toastVariants> {
|
||||
export type ToastsProps = Omit<React.HTMLAttributes<HTMLDivElement>, "color"> &
|
||||
VariantProps<typeof toastVariants> & {
|
||||
onClose?: () => void
|
||||
} & (
|
||||
| {
|
||||
children: React.ReactNode
|
||||
message?: never
|
||||
}
|
||||
| {
|
||||
children?: never
|
||||
message: React.ReactNode
|
||||
onClose: () => void
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -48,7 +48,9 @@
|
||||
"Bed type": "Bed type",
|
||||
"Birth date": "Birth date",
|
||||
"Book": "Book",
|
||||
"Book another stay": "Book another stay",
|
||||
"Book reward night": "Book reward night",
|
||||
"Book your next stay": "Book your next stay",
|
||||
"Booking": "Booking",
|
||||
"Booking number": "Booking number",
|
||||
"Breakfast": "Breakfast",
|
||||
@@ -114,6 +116,7 @@
|
||||
"Disabled booking options text": "Codes, cheques and reward nights aren't available on the new website yet.",
|
||||
"Discard changes": "Discard changes",
|
||||
"Discard unsaved changes?": "Discard unsaved changes?",
|
||||
"Discover the little extra touches to make your upcoming stay even more unforgettable.": "Discover the little extra touches to make your upcoming stay even more unforgettable.",
|
||||
"Distance in km to city centre": "{number} km to city centre",
|
||||
"Distance to city centre": "Distance to city centre",
|
||||
"Distance to hotel": "Distance to hotel: {distance} m",
|
||||
@@ -152,9 +155,11 @@
|
||||
"Free parking": "Free parking",
|
||||
"Free rebooking": "Free rebooking",
|
||||
"Free until": "Free until",
|
||||
"Friend no.": "Friend no.",
|
||||
"From": "From",
|
||||
"Garage": "Garage",
|
||||
"Get inspired": "Get inspired",
|
||||
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.": "Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
||||
"Get member benefits & offers": "Get member benefits & offers",
|
||||
"Gift(s) added to your benefits": "{amount, plural, one {Gift} other {Gifts}} added to your benefits",
|
||||
"Go back to edit": "Go back to edit",
|
||||
@@ -171,6 +176,7 @@
|
||||
"Home": "Home",
|
||||
"Hospital": "Hospital",
|
||||
"Hotel": "Hotel",
|
||||
"Hotel details": "Hotel details",
|
||||
"Hotel facilities": "Hotel facilities",
|
||||
"Hotel reservation": "Hotel reservation",
|
||||
"Hotel surroundings": "Hotel surroundings",
|
||||
@@ -213,8 +219,10 @@
|
||||
"Log in here": "Log in here",
|
||||
"Log in/Join": "Log in/Join",
|
||||
"Log out": "Log out",
|
||||
"Long {long} ∙ Lat {lat}": "Long {long} ∙ Lat {lat}",
|
||||
"Longitude": "Longitude {long}",
|
||||
"MY SAVED CARDS": "MY SAVED CARDS",
|
||||
"Main guest": "Main guest",
|
||||
"Main menu": "Main menu",
|
||||
"Manage booking": "Manage booking",
|
||||
"Manage preferences": "Manage preferences",
|
||||
@@ -228,6 +236,7 @@
|
||||
"Members": "Members",
|
||||
"Membership ID": "Membership ID",
|
||||
"Membership ID copied to clipboard": "Membership ID copied to clipboard",
|
||||
"Membership benefits applied": "Membership benefits applied",
|
||||
"Membership cards": "Membership cards",
|
||||
"Membership no": "Membership no",
|
||||
"Membership terms and conditions": "Membership terms and conditions",
|
||||
@@ -253,6 +262,7 @@
|
||||
"No breakfast": "No breakfast",
|
||||
"No content published": "No content published",
|
||||
"No matching location found": "No matching location found",
|
||||
"No membership benefits applied": "No membership benefits applied",
|
||||
"No prices available": "No prices available",
|
||||
"No results": "No results",
|
||||
"No transactions available": "No transactions available",
|
||||
@@ -285,6 +295,7 @@
|
||||
"Pay now": "Pay now",
|
||||
"Payment": "Payment",
|
||||
"Payment Guarantee": "Payment Guarantee",
|
||||
"Payment details": "Payment details",
|
||||
"Payment info": "Payment info",
|
||||
"Payment method": "Payment method",
|
||||
"Payment received": "Payment received",
|
||||
@@ -331,6 +342,8 @@
|
||||
"Relax": "Relax",
|
||||
"Remove card from member profile": "Remove card from member profile",
|
||||
"Request bedtype": "Request bedtype",
|
||||
"Reservation number": "Reservation number",
|
||||
"Reservation policy": "Reservation policy",
|
||||
"Restaurant": "{count, plural, one {#Restaurant} other {#Restaurants}}",
|
||||
"Restaurant & Bar": "Restaurant & Bar",
|
||||
"Restaurants & Bars": "Restaurants & Bars",
|
||||
@@ -386,6 +399,7 @@
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Something went wrong and we couldn't remove your card. Please try again later.",
|
||||
"Something went wrong!": "Something went wrong!",
|
||||
"Sort by": "Sort by",
|
||||
"Spice things up": "Spice things up",
|
||||
"Sports": "Sports",
|
||||
"Standard price": "Standard price",
|
||||
"Stay at HOTEL_NAME | Hotel in DESTINATION": "Stay at {hotelName} | Hotel in {destination}",
|
||||
@@ -424,8 +438,10 @@
|
||||
"User information": "User information",
|
||||
"VAT": "VAT",
|
||||
"VAT amount": "VAT amount",
|
||||
"View and buy add-ons": "View and buy add-ons",
|
||||
"View as list": "View as list",
|
||||
"View as map": "View as map",
|
||||
"View room details": "View room details",
|
||||
"View terms": "View terms",
|
||||
"View your booking": "View your booking",
|
||||
"Visiting address": "Visiting address",
|
||||
@@ -498,6 +514,7 @@
|
||||
"guest": "guest",
|
||||
"guest.paid": "{amount} {currency} has been paid",
|
||||
"guests": "guests",
|
||||
"has been paid": "has been paid",
|
||||
"hotelPages.rooms.roomCard.persons": "{size} ({totalOccupancy, plural, one {# person} other {# persons}})",
|
||||
"hotelPages.rooms.roomCard.seeRoomDetails": "See room details",
|
||||
"km to city center": "km to city center",
|
||||
|
||||
23
package-lock.json
generated
23
package-lock.json
generated
@@ -35,6 +35,7 @@
|
||||
"dayjs": "^1.11.10",
|
||||
"deepmerge": "^4.3.1",
|
||||
"downshift": "^9.0.8",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fetch-retry": "^6.0.0",
|
||||
"framer-motion": "^11.3.28",
|
||||
"graphql": "^16.8.1",
|
||||
@@ -43,7 +44,6 @@
|
||||
"immer": "10.1.1",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"libphonenumber-js": "^1.10.60",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"next": "^14.2.18",
|
||||
"next-auth": "^5.0.0-beta.19",
|
||||
"react": "^18",
|
||||
@@ -67,7 +67,6 @@
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/json-stable-stringify-without-jsonify": "^1.0.2",
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
@@ -6866,21 +6865,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz",
|
||||
"integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/lodash.isequal": {
|
||||
"version": "4.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.8.tgz",
|
||||
"integrity": "sha512-uput6pg4E/tj2LGxCZo9+y27JNyB2OZuuI/T5F+ylVDYuqICLG2/ktjxx0v6GvVntAf8TvEzeQLcV0ffRirXuA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz",
|
||||
@@ -15001,11 +14985,6 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz",
|
||||
"integrity": "sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg=="
|
||||
},
|
||||
"node_modules/lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"dayjs": "^1.11.10",
|
||||
"deepmerge": "^4.3.1",
|
||||
"downshift": "^9.0.8",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fetch-retry": "^6.0.0",
|
||||
"framer-motion": "^11.3.28",
|
||||
"graphql": "^16.8.1",
|
||||
@@ -58,7 +59,6 @@
|
||||
"immer": "10.1.1",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"libphonenumber-js": "^1.10.60",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"next": "^14.2.18",
|
||||
"next-auth": "^5.0.0-beta.19",
|
||||
"react": "^18",
|
||||
@@ -82,7 +82,6 @@
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/json-stable-stringify-without-jsonify": "^1.0.2",
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import isEqual from "lodash.isequal"
|
||||
import { z } from "zod"
|
||||
import isEqual from "fast-deep-equal"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { breakfastPackageSchema } from "@/server/routers/hotels/output"
|
||||
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import type { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData"
|
||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
import { CurrencyEnum } from "@/types/enums/currency"
|
||||
import { StepEnum } from "@/types/enums/step"
|
||||
import type { DetailsState, RoomRate } from "@/types/stores/enter-details"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface PromoProps {
|
||||
buttonText: string
|
||||
text: string
|
||||
title: string
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { RouterOutput } from "@/lib/trpc/client"
|
||||
|
||||
export interface RoomProps {
|
||||
booking: RouterOutput["booking"]["confirmation"]["booking"]
|
||||
img: NonNullable<
|
||||
RouterOutput["booking"]["confirmation"]["hotel"]["included"]
|
||||
>[number]["images"][number]
|
||||
roomName: NonNullable<
|
||||
RouterOutput["booking"]["confirmation"]["hotel"]["included"]
|
||||
>[number]["name"]
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
import type { Packages } from "@/types/requests/packages"
|
||||
import type { RoomAvailability } from "@/types/trpc/routers/hotel/availability"
|
||||
|
||||
export interface ClientSummaryProps
|
||||
|
||||
6
types/components/hotelReservation/sidePanel.ts
Normal file
6
types/components/hotelReservation/sidePanel.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { sidePanelVariants } from "@/components/HotelReservation/SidePanel/variants"
|
||||
|
||||
import type { VariantProps } from "class-variance-authority"
|
||||
|
||||
export interface SidePanelProps
|
||||
extends VariantProps<typeof sidePanelVariants> {}
|
||||
@@ -1,10 +1,6 @@
|
||||
import { RoomPackageCodeEnum } from "./selectRate/roomFilter"
|
||||
|
||||
import type { Packages } from "@/types/requests/packages"
|
||||
import type { DetailsState, Price } from "@/types/stores/enter-details"
|
||||
import type { RoomAvailability } from "@/types/trpc/routers/hotel/availability"
|
||||
import type { BedTypeSchema } from "./enterDetails/bedType"
|
||||
import type { BreakfastPackage } from "./enterDetails/breakfast"
|
||||
import type { Child } from "./selectRate/selectRate"
|
||||
|
||||
export type RoomsData = Pick<DetailsState, "roomPrice"> &
|
||||
@@ -15,25 +11,8 @@ export type RoomsData = Pick<DetailsState, "roomPrice"> &
|
||||
packages: Packages | null
|
||||
}
|
||||
|
||||
interface SharedSummaryProps {
|
||||
fromDate: string
|
||||
toDate: string
|
||||
}
|
||||
|
||||
export interface SummaryProps extends SharedSummaryProps {
|
||||
bedType: BedTypeSchema | undefined
|
||||
breakfast: BreakfastPackage | false | undefined
|
||||
showMemberPrice: boolean
|
||||
room: RoomsData
|
||||
toggleSummaryOpen?: () => void
|
||||
totalPrice: Price
|
||||
}
|
||||
|
||||
export interface SummaryPageProps extends SharedSummaryProps {
|
||||
adults: number
|
||||
hotelId: string
|
||||
kids: Child[] | undefined
|
||||
packageCodes: RoomPackageCodeEnum[] | undefined
|
||||
rateCode: string
|
||||
roomTypeCode: string
|
||||
export interface SummaryProps
|
||||
extends Pick<RoomAvailability, "cancellationText" | "rateDetails">,
|
||||
Pick<RoomAvailability["selectedRoom"], "roomType"> {
|
||||
isMember: boolean
|
||||
}
|
||||
|
||||
23
utils/getBookedHotelRoom.ts
Normal file
23
utils/getBookedHotelRoom.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { RouterOutput } from "@/lib/trpc/client"
|
||||
|
||||
export function getBookedHotelRoom(
|
||||
hotel: RouterOutput["booking"]["confirmation"]["hotel"],
|
||||
roomTypeCode: string
|
||||
) {
|
||||
const room = hotel.included?.find((include) => {
|
||||
return include.roomTypes.find((roomType) => roomType.code === roomTypeCode)
|
||||
})
|
||||
if (!room) {
|
||||
return null
|
||||
}
|
||||
const bedType = room.roomTypes.find(
|
||||
(roomType) => roomType.code === roomTypeCode
|
||||
)
|
||||
if (!bedType) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
...room,
|
||||
bedType,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user