feat: new booking confirmation page

This commit is contained in:
Simon Emanuelsson
2024-11-28 14:22:31 +01:00
parent 329d762917
commit ed1574838a
66 changed files with 1127 additions and 825 deletions

View File

@@ -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);
}
}

View File

@@ -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>
)
}

View File

@@ -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;
}
}

View File

@@ -16,3 +16,9 @@
.body {
max-width: 720px;
}
@media screen and (min-width: 1367px) {
.header {
padding-bottom: var(--Spacing-x4);
}
}

View File

@@ -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;
}

View File

@@ -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>
)
}

View File

@@ -1,7 +0,0 @@
.imageContainer {
align-items: center;
border-radius: var(--Corner-radius-Medium);
display: flex;
grid-area: image;
justify-content: center;
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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);
}

View File

@@ -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>
)
}

View File

@@ -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;
}

View File

@@ -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>
)
}

View File

@@ -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;
}
}

View File

@@ -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>
)
}

View File

@@ -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);
}
}

View File

@@ -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>
)
}

View File

@@ -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;
}
}

View File

@@ -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>
)
}

View File

@@ -1,6 +1,5 @@
.rooms {
display: flex;
flex-direction: column;
gap: var(--Spacing-x9);
grid-area: booking;
gap: var(--Spacing-x5);
}

View File

@@ -1,5 +0,0 @@
import styles from "./summary.module.css"
export default function Summary() {
return <aside className={styles.summary}>SUMMARY</aside>
}

View File

@@ -1,4 +0,0 @@
.summary {
background-color: hotpink;
grid-area: summary;
}

View File

@@ -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>
)
}

View File

@@ -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));
}

View File

@@ -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>
)
}

View File

@@ -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);
}

View File

@@ -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>
</>
)
}

View 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>
)
}

View File

@@ -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 } =

View File

@@ -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>
)
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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}
/>
)
}

View 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>
)
}

View File

@@ -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;
}
}

View 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",
},
})