feat: booking confirmation page with hardcoded data

This commit is contained in:
Simon Emanuelsson
2024-10-22 11:43:08 +02:00
parent 445bde8e2e
commit 2d23f9bbf3
42 changed files with 859 additions and 533 deletions

View File

@@ -0,0 +1,157 @@
.details,
.guest,
.header,
.hgroup,
.hotel,
.list,
.main,
.section,
.receipt,
.total {
display: flex;
flex-direction: column;
}
.main {
gap: var(--Spacing-x5);
margin: 0 auto;
width: min(calc(100dvw - (var(--Spacing-x3) * 2)), 708px);
}
.header,
.hgroup {
align-items: center;
}
.header {
gap: var(--Spacing-x3);
}
.hgroup {
gap: var(--Spacing-x-half);
}
.body {
max-width: 560px;
}
.section {
display: flex;
flex-direction: column;
gap: var(--Spacing-x9);
}
.booking {
display: grid;
gap: var(--Spacing-x-one-and-half);
grid-template-areas:
"image"
"details"
"actions";
}
.actions,
.details {
background-color: var(--Base-Surface-Subtle-Normal);
border-radius: var(--Corner-radius-Medium);
}
.details {
gap: var(--Spacing-x3);
grid-area: details;
padding: var(--Spacing-x2);
}
.tempImage {
align-items: center;
background-color: lightgrey;
border-radius: var(--Corner-radius-Medium);
display: flex;
grid-area: image;
justify-content: center;
}
.actions {
display: grid;
grid-area: actions;
padding: var(--Spacing-x1) var(--Spacing-x2);
}
.list {
gap: var(--Spacing-x-one-and-half);
list-style: none;
margin: 0;
padding: 0;
}
.listItem {
align-items: center;
display: flex;
gap: var(--Spacing-x1);
justify-content: space-between;
}
.summary {
display: grid;
gap: var(--Spacing-x3);
}
.guest,
.hotel {
gap: var(--Spacing-x-half);
}
.receipt,
.total {
gap: var(--Spacing-x2);
}
.divider {
grid-column: 1 / -1;
}
@media screen and (max-width: 767px) {
.actions {
& > button[class*="btn"][class*="icon"][class*="small"] {
border-bottom: 1px solid var(--Base-Border-Subtle);
border-radius: 0;
justify-content: space-between;
&:last-of-type {
border-bottom: none;
}
& > svg {
order: 2;
}
}
}
.tempImage {
min-height: 250px;
}
}
@media screen and (min-width: 768px) {
.booking {
grid-template-areas:
"details image"
"actions actions";
grid-template-columns: 1fr minmax(256px, min(256px, 100%));
}
.actions {
gap: var(--Spacing-x7);
grid-template-columns: repeat(auto-fit, minmax(50px, auto));
justify-content: center;
padding: var(--Spacing-x1) var(--Spacing-x3);
}
.details {
padding: var(--Spacing-x3) var(--Spacing-x3) var(--Spacing-x2);
}
.summary {
grid-template-columns: 1fr 1fr;
}
}

View File

@@ -0,0 +1,279 @@
import { dt } from "@/lib/dt"
import { serverClient } from "@/lib/trpc/server"
import {
CalendarIcon,
DownloadIcon,
ImageIcon,
PrinterIcon,
} from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import Divider from "@/components/TempDesignSystem/Divider"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import Title from "@/components/TempDesignSystem/Text/Title"
import { getIntl } from "@/i18n"
import styles from "./page.module.css"
import type { LangParams, PageArgs } from "@/types/params"
export default async function BookingConfirmationPage({
params,
}: PageArgs<LangParams>) {
const booking = await serverClient().booking.confirmation({
confirmationNumber: "991697377",
})
if (!booking) {
return null
}
const intl = await getIntl()
const text = intl.formatMessage<React.ReactNode>(
{ id: "booking.confirmation.text" },
{
emailLink: (str) => (
<Link color="burgundy" href="#" textDecoration="underline">
{str}
</Link>
),
}
)
const fromDate = dt(booking.temp.fromDate).locale(params.lang)
const toDate = dt(booking.temp.toDate).locale(params.lang)
const nights = intl.formatMessage(
{ id: "booking.nights" },
{
totalNights: dt(toDate.format("YYYY-MM-DD")).diff(
dt(fromDate.format("YYYY-MM-DD")),
"days"
),
}
)
return (
<main className={styles.main}>
<header className={styles.header}>
<hgroup className={styles.hgroup}>
<Title
as="h4"
color="red"
textAlign="center"
textTransform="regular"
type="h2"
>
{intl.formatMessage({ id: "booking.confirmation.title" })}
</Title>
<Title
as="h4"
color="burgundy"
textAlign="center"
textTransform="regular"
type="h1"
>
{booking.hotel.name}
</Title>
</hgroup>
<Body className={styles.body} textAlign="center">
{text}
</Body>
</header>
<section className={styles.section}>
<div className={styles.booking}>
<article className={styles.details}>
<header>
<Subtitle color="burgundy" type="two">
{intl.formatMessage(
{ id: "Reference #{bookingNr}" },
{ bookingNr: "A92320VV" }
)}
</Subtitle>
</header>
<ul className={styles.list}>
<li className={styles.listItem}>
<Body>{intl.formatMessage({ id: "Check-in" })}</Body>
<Body>
{`${fromDate.format("ddd, D MMM")} ${intl.formatMessage({ id: "from" })} ${fromDate.format("HH:mm")}`}
</Body>
</li>
<li className={styles.listItem}>
<Body>{intl.formatMessage({ id: "Check-out" })}</Body>
<Body>
{`${toDate.format("ddd, D MMM")} ${intl.formatMessage({ id: "from" })} ${toDate.format("HH:mm")}`}
</Body>
</li>
<li className={styles.listItem}>
<Body>{intl.formatMessage({ id: "Breakfast" })}</Body>
<Body>
{booking.temp.breakfastFrom} - {booking.temp.breakfastTo}
</Body>
</li>
<li className={styles.listItem}>
<Body>{intl.formatMessage({ id: "Cancellation policy" })}</Body>
<Body>
{intl.formatMessage({ id: booking.temp.cancelPolicy })}
</Body>
</li>
<li className={styles.listItem}>
<Body>{intl.formatMessage({ id: "Rebooking" })}</Body>
<Body>{`${intl.formatMessage({ id: "Free until" })} ${fromDate.subtract(3, "day").format("ddd, D MMM")}`}</Body>
</li>
</ul>
</article>
<aside className={styles.tempImage}>
<ImageIcon height={80} width={80} />
</aside>
<div className={styles.actions}>
<Button
intent="text"
size="small"
theme="base"
variant="icon"
wrapping
>
<CalendarIcon />
{intl.formatMessage({ id: "Add to calendar" })}
</Button>
<Button
intent="text"
size="small"
theme="base"
variant="icon"
wrapping
>
<PrinterIcon />
{intl.formatMessage({ id: "Print confirmation" })}
</Button>
<Button
intent="text"
size="small"
theme="base"
variant="icon"
wrapping
>
<DownloadIcon />
{intl.formatMessage({ id: "Download invoice" })}
</Button>
</div>
</div>
<div className={styles.summary}>
<div className={styles.guest}>
<Caption color="uiTextPlaceholder">
{intl.formatMessage({ id: "Guest" })}
</Caption>
<div>
<Body color="burgundy" textTransform="bold">
{`${booking.guest.firstName} ${booking.guest.lastName}${booking.guest.memberbershipNumber ? ` (${intl.formatMessage({ id: "member no" })} ${booking.guest.memberbershipNumber})` : ""}`}
</Body>
<Body color="uiTextHighContrast">{booking.guest.email}</Body>
<Body color="uiTextHighContrast">
{booking.guest.phoneNumber}
</Body>
</div>
</div>
<div className={styles.hotel}>
<Caption color="uiTextPlaceholder">
{intl.formatMessage({ id: "Your hotel" })}
</Caption>
<div>
<Body color="burgundy" textTransform="bold">
{booking.hotel.name}
</Body>
<Body color="uiTextHighContrast">{booking.hotel.email}</Body>
<Body color="uiTextHighContrast">
{booking.hotel.phoneNumber}
</Body>
</div>
</div>
<Divider className={styles.divider} color="primaryLightSubtle" />
<div className={styles.receipt}>
<div>
<Body color="burgundy" textTransform="bold">
{`${booking.temp.room.type}, ${nights}`}
</Body>
<Body color="uiTextHighContrast">{booking.temp.room.price}</Body>
</div>
{booking.temp.packages.map((pkg) => (
<div key={pkg.name}>
<Body color="burgundy" textTransform="bold">
{pkg.name}
</Body>
<Body color="uiTextHighContrast">{pkg.price}</Body>
</div>
))}
</div>
<div className={styles.total}>
<div>
<Body color="burgundy" textTransform="bold">
{intl.formatMessage({ id: "VAT" })}
</Body>
<Body color="uiTextHighContrast">{booking.temp.room.vat}</Body>
</div>
<div>
<Body color="burgundy" textTransform="bold">
{intl.formatMessage({ id: "Total cost" })}
</Body>
<Body color="uiTextHighContrast">{booking.temp.total}</Body>
<Caption color="uiTextPlaceholder">
{`${intl.formatMessage({ id: "Approx." })} ${booking.temp.totalInEuro}`}
</Caption>
</div>
</div>
<Divider className={styles.divider} color="primaryLightSubtle" />
<div>
<Body color="burgundy" textTransform="bold">
{`${intl.formatMessage({ id: "Payment received" })} ${dt(booking.temp.payment).locale(params.lang).format("D MMM YYYY, h:mm z")}`}
</Body>
<Caption color="uiTextPlaceholder">
{intl.formatMessage(
{ id: "{card} ending with {cardno}" },
{
card: "Mastercard",
cardno: "2202",
}
)}
</Caption>
</div>
</div>
</section>
</main>
)
}
// const { email, hotel, stay, summary } = tempConfirmationData
// const confirmationNumber = useMemo(() => {
// if (typeof window === "undefined") return ""
// const storedConfirmationNumber = sessionStorage.getItem(
// BOOKING_CONFIRMATION_NUMBER
// )
// TODO: cleanup stored values
// sessionStorage.removeItem(BOOKING_CONFIRMATION_NUMBER)
// return storedConfirmationNumber
// }, [])
// const bookingStatus = useHandleBookingStatus(
// confirmationNumber,
// BookingStatusEnum.BookingCompleted,
// maxRetries,
// retryInterval
// )
// if (
// confirmationNumber === null ||
// bookingStatus.isError ||
// (bookingStatus.isFetched && !bookingStatus.data)
// ) {
// // TODO: handle error
// throw new Error("Error fetching booking status")
// }
// if (
// bookingStatus.data?.reservationStatus === BookingStatusEnum.BookingCompleted
// ) {
// return <LoadingSpinner />

View File

@@ -0,0 +1,5 @@
.layout {
background-color: var(--Base-Surface-Primary-light-Normal);
min-height: 100dvh;
padding: 80px 0 160px;
}

View File

@@ -0,0 +1,15 @@
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) {
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()
}
return <div className={styles.layout}>{children}</div>
}

View File

@@ -3,7 +3,7 @@ import { env } from "@/env/server"
import {
fetchAvailableHotels,
getFiltersFromHotels,
} from "@/app/[lang]/(live)/(public)/hotelreservation/select-hotel/utils"
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap"
import { setLang } from "@/i18n/serverContext"

View File

@@ -3,7 +3,7 @@ import { selectHotelMap } from "@/constants/routes/hotelReservation"
import {
fetchAvailableHotels,
getFiltersFromHotels,
} from "@/app/[lang]/(live)/(public)/hotelreservation/select-hotel/utils"
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
import { ChevronRightIcon } from "@/components/Icons"

View File

@@ -1,16 +0,0 @@
.main {
display: flex;
justify-content: center;
padding: var(--Spacing-x4);
background-color: var(--Scandic-Brand-Warm-White);
min-height: 100dvh;
max-width: var(--max-width);
margin: 0 auto;
}
.section {
display: flex;
flex-direction: column;
gap: var(--Spacing-x4);
width: 100%;
}

View File

@@ -1,67 +0,0 @@
"use client"
import { useMemo } from "react"
import {
BOOKING_CONFIRMATION_NUMBER,
BookingStatusEnum,
} from "@/constants/booking"
import IntroSection from "@/components/HotelReservation/BookingConfirmation/IntroSection"
import StaySection from "@/components/HotelReservation/BookingConfirmation/StaySection"
import SummarySection from "@/components/HotelReservation/BookingConfirmation/SummarySection"
import { tempConfirmationData } from "@/components/HotelReservation/BookingConfirmation/tempConfirmationData"
import LoadingSpinner from "@/components/LoadingSpinner"
import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus"
import styles from "./page.module.css"
const maxRetries = 10
const retryInterval = 2000
export default function BookingConfirmationPage() {
const { email, hotel, stay, summary } = tempConfirmationData
const confirmationNumber = useMemo(() => {
if (typeof window === "undefined") return ""
const storedConfirmationNumber = sessionStorage.getItem(
BOOKING_CONFIRMATION_NUMBER
)
// TODO: cleanup stored values
// sessionStorage.removeItem(BOOKING_CONFIRMATION_NUMBER)
return storedConfirmationNumber
}, [])
const bookingStatus = useHandleBookingStatus(
confirmationNumber,
BookingStatusEnum.BookingCompleted,
maxRetries,
retryInterval
)
if (
confirmationNumber === null ||
bookingStatus.isError ||
(bookingStatus.isFetched && !bookingStatus.data)
) {
// TODO: handle error
throw new Error("Error fetching booking status")
}
if (
bookingStatus.data?.reservationStatus === BookingStatusEnum.BookingCompleted
) {
return (
<main className={styles.main}>
<section className={styles.section}>
<IntroSection email={email} />
<StaySection hotel={hotel} stay={stay} />
<SummarySection summary={summary} />
</section>
</main>
)
}
return <LoadingSpinner />
}

View File

@@ -0,0 +1 @@
export { default } from "../../page"

View File

@@ -0,0 +1,3 @@
export default function ConfirmedBookingSlot() {
return null
}

View File

@@ -0,0 +1 @@
export { default } from "../page"