Merged in chore/SW-2878-extract-booking-confirmation-pag (pull request #2779)

Chore/SW-2878 extract booking confirmation pag

* chore(SW-2878): Moved booking confirmation page to booking-flow package

* chore(SW-2878): Fixed promo styles as per design

* chore(SW-2878): Kept tiny duplicate function to avoid export from booking-flow package


Approved-by: Anton Gunnarsson
This commit is contained in:
Hrishikesh Vaipurkar
2025-09-10 07:50:48 +00:00
parent c6da0fb8cb
commit a5790ee454
77 changed files with 410 additions and 371 deletions

View File

@@ -1,12 +1,8 @@
import { cookies } from "next/headers"
import { notFound, redirect } from "next/navigation"
import { BookingConfirmationPage as BookingConfirmationPagePrimitive } from "@scandic-hotels/booking-flow/pages/BookingConfirmationPage"
import { decrypt } from "@scandic-hotels/trpc/utils/encryption"
import { MEMBERSHIP_FAILED_ERROR } from "@/constants/booking"
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
import BookingConfirmation from "@/components/HotelReservation/BookingConfirmation"
import Tracking from "@/components/HotelReservation/BookingConfirmation/Tracking"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import type { LangParams, PageArgs } from "@/types/params"
@@ -14,35 +10,20 @@ export default async function BookingConfirmationPage(
props: PageArgs<LangParams, { RefId?: string }>
) {
const searchParams = await props.searchParams
const params = await props.params
const refId = searchParams.RefId
if (!refId) {
notFound()
}
const cookieStore = await cookies()
const sig = cookieStore.get("bcsig")?.value
if (!sig) {
redirect(`/${params.lang}`)
}
const expire = Number(decrypt(sig))
const now = Math.floor(Date.now() / 1000)
if (typeof expire === "number" && !isNaN(expire) && now > expire) {
redirect(`/${params.lang}`)
}
void getBookingConfirmation(refId)
const membershipFailedError =
searchParams.errorCode === MEMBERSHIP_FAILED_ERROR
const lang = await getLang()
const intl = await getIntl()
return (
<BookingConfirmation
refId={refId}
membershipFailedError={membershipFailedError}
<BookingConfirmationPagePrimitive
intl={intl}
lang={lang}
searchParams={searchParams}
renderTracking={(props) => (
<Tracking
bookingConfirmation={props.bookingConfirmation}
refId={props.refId}
/>
)}
/>
)
}

View File

@@ -1,11 +1,11 @@
import { notFound } from "next/navigation"
import { PaymentCallbackStatusEnum } from "@scandic-hotels/common/constants/booking"
import { myStay } from "@scandic-hotels/common/constants/routes/myStay"
import { logger } from "@scandic-hotels/common/logger"
import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
import { BookingErrorCodeEnum } from "@scandic-hotels/trpc/enums/bookingErrorCode"
import { PaymentCallbackStatusEnum } from "@/constants/booking"
import { serverClient } from "@/lib/trpc/server"
import GuaranteeCallback from "@/components/HotelReservation/MyStay/Ancillaries/GuaranteeCallback"

View File

@@ -1,5 +1,6 @@
import { notFound } from "next/navigation"
import { PaymentCallbackStatusEnum } from "@scandic-hotels/common/constants/booking"
import {
bookingConfirmation,
details,
@@ -11,7 +12,6 @@ import { getBooking } from "@scandic-hotels/trpc/routers/booking/utils"
import { encrypt } from "@scandic-hotels/trpc/utils/encryption"
import { isValidSession } from "@scandic-hotels/trpc/utils/session"
import { PaymentCallbackStatusEnum } from "@/constants/booking"
import { serverClient } from "@/lib/trpc/server"
import { auth } from "@/auth"

View File

@@ -1,28 +0,0 @@
.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);
}
.list {
padding-left: var(--Spacing-x2);
}
.link {
word-break: break-all;
}

View File

@@ -1,67 +0,0 @@
"use client"
import { useIntl } from "react-intl"
import Body from "@scandic-hotels/design-system/Body"
import Link from "@scandic-hotels/design-system/Link"
import Subtitle from "@scandic-hotels/design-system/Subtitle"
import styles from "./hotelDetails.module.css"
import type { BookingConfirmationHotelDetailsProps } from "@/types/components/hotelReservation/bookingConfirmation/hotelDetails"
export default function HotelDetails({
hotel,
}: BookingConfirmationHotelDetailsProps) {
const intl = useIntl()
return (
<div className={styles.container}>
<div className={styles.details}>
<Subtitle color="uiTextHighContrast" type="two">
{intl.formatMessage({
defaultMessage: "Hotel details",
})}
</Subtitle>
<div className={styles.hotel}>
<Body color="uiTextHighContrast">{hotel.name}</Body>
<Body color="uiTextHighContrast">
{intl.formatMessage(
{
defaultMessage: "{streetAddress}, {zipCode} {city}",
},
{
streetAddress: hotel.address.streetAddress,
zipCode: hotel.address.zipCode,
city: hotel.address.city,
}
)}
</Body>
<Body asChild color="uiTextHighContrast">
<Link
className={styles.link}
href={`tel:${hotel.contactInformation.phoneNumber}`}
>
{hotel.contactInformation.phoneNumber}
</Link>
</Body>
</div>
</div>
<div className={styles.contact}>
<Link
className={styles.link}
color="Text/Interactive/Secondary"
href={`mailto:${hotel.contactInformation.email}`}
>
{hotel.contactInformation.email}
</Link>
<Link
className={styles.link}
color="Text/Interactive/Secondary"
href={hotel.contactInformation.websiteUrl}
>
{hotel.contactInformation.websiteUrl}
</Link>
</div>
</div>
)
}

View File

@@ -1,48 +0,0 @@
"use client"
import { useIntl } from "react-intl"
import { useBookingConfirmationStore } from "@scandic-hotels/booking-flow/stores/booking-confirmation"
import Body from "@scandic-hotels/design-system/Body"
import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer"
import Subtitle from "@scandic-hotels/design-system/Subtitle"
import styles from "./paymentDetails.module.css"
export default function PaymentDetails() {
const intl = useIntl()
const { rooms, formattedTotalCost } = useBookingConfirmationStore(
(state) => ({
rooms: state.rooms,
formattedTotalCost: state.formattedTotalCost,
})
)
const hasAllRoomsLoaded = rooms.every((room) => room)
return (
<div className={styles.details}>
<Subtitle color="uiTextHighContrast" type="two">
{intl.formatMessage({
defaultMessage: "Payment details",
})}
</Subtitle>
<div className={styles.payment}>
{hasAllRoomsLoaded ? (
<Body color="uiTextHighContrast">
{intl.formatMessage(
{
defaultMessage: "Total cost: {amount}",
},
{
amount: formattedTotalCost,
}
)}
</Body>
) : (
<SkeletonShimmer width={"100%"} />
)}
</div>
</div>
)
}

View File

@@ -1,19 +0,0 @@
.details,
.payment {
display: flex;
flex-direction: column;
}
.details {
gap: var(--Spacing-x-one-and-half);
max-width: fit-content;
}
.payment {
gap: var(--Spacing-x-half);
}
.details button.btn {
align-self: flex-start;
margin-top: var(--Spacing-x-half);
}

View File

@@ -1,137 +0,0 @@
"use client"
import PriceDetailsModal from "@scandic-hotels/booking-flow/components/PriceDetailsModal"
import { useBookingConfirmationStore } from "@scandic-hotels/booking-flow/stores/booking-confirmation"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { dt } from "@scandic-hotels/common/dt"
import { mapToPrice } from "./mapToPrice"
import type { Price } from "@/types/components/hotelReservation/price"
export default function PriceDetails() {
const { bookingCode, currency, fromDate, rooms, vat, toDate } =
useBookingConfirmationStore((state) => ({
bookingCode: state.bookingCode ?? undefined,
currency: state.currencyCode,
fromDate: state.fromDate,
rooms: state.rooms,
toDate: state.toDate,
vat: state.vat,
}))
if (!rooms[0]) {
return null
}
const checkInDate = dt(fromDate).format("YYYY-MM-DD")
const checkOutDate = dt(toDate).format("YYYY-MM-DD")
const nights = dt(toDate)
.startOf("day")
.diff(dt(fromDate).startOf("day"), "days")
const totalPrice = rooms.reduce<Price>(
(total, room) => {
if (!room) {
return total
}
if (room.cheques) {
// CorporateCheque Booking
total.local.currency = CurrencyEnum.CC
total.local.price = total.local.price + room.cheques
} else if (room.roomPoints) {
// Redemption Booking
total.local.currency = CurrencyEnum.POINTS
total.local.price = total.local.price + room.roomPoints
} else if (room.vouchers) {
// Vouchers Booking
total.local.currency = CurrencyEnum.Voucher
total.local.price = total.local.price + room.vouchers
} else {
// Price Booking
total.local.price = total.local.price + room.totalPrice
}
// Corporate Cheque
if (room.cheques) {
if (room.roomPrice) {
total.local.additionalPrice =
(total.local.additionalPrice || 0) + room.totalPrice
total.local.additionalPriceCurrency = currency
} else {
const pkgsSum = room.packages.reduce(
(total, pkg) => total + pkg.totalPrice,
0
)
total.local.additionalPrice =
(total.local.additionalPrice || 0) + pkgsSum
const pkgsCurrency = room.packages.find(
(pkg) => pkg.currency
)?.currency
if (!total.local.additionalPriceCurrency) {
total.local.additionalPriceCurrency = pkgsCurrency ?? currency
}
}
}
// Redemption
if (room.roomPoints) {
if (room.roomPrice) {
total.local.additionalPrice =
(total.local.additionalPrice || 0) + room.totalPrice
total.local.additionalPriceCurrency = currency
} else {
const pkgsSum = room.packages.reduce(
(total, pkg) => total + pkg.totalPrice,
0
)
total.local.additionalPrice =
(total.local.additionalPrice || 0) + pkgsSum
const pkgsCurrency = room.packages.find(
(pkg) => pkg.currency
)?.currency
if (!total.local.additionalPriceCurrency) {
total.local.additionalPriceCurrency = pkgsCurrency ?? currency
}
}
}
// Voucher
if (room.vouchers && room.packages) {
const pkgsSum = room.packages.reduce(
(total, pkg) => total + pkg.totalPrice,
0
)
total.local.additionalPrice =
(total.local.additionalPrice || 0) + pkgsSum
const pkgsCurrency = room.packages.find((pkg) => pkg.currency)?.currency
if (!total.local.additionalPriceCurrency) {
total.local.additionalPriceCurrency = pkgsCurrency ?? currency
}
}
return total
},
{
local: {
currency,
price: 0,
},
requested: undefined,
}
)
const mappedRooms = mapToPrice(rooms, nights)
return (
<PriceDetailsModal
bookingCode={bookingCode}
fromDate={checkInDate}
rooms={mappedRooms}
toDate={checkOutDate}
totalPrice={totalPrice}
vat={vat}
defaultCurrency={currency}
/>
)
}

View File

@@ -1,126 +0,0 @@
import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast"
import { ChildBedMapEnum } from "@scandic-hotels/trpc/enums/childBedMapEnum"
import { PackageTypeEnum } from "@scandic-hotels/trpc/enums/packages"
import {
type BreakfastPackage,
breakfastPackageSchema,
packageSchema,
} from "@scandic-hotels/trpc/routers/hotels/schemas/packages"
import type { Room } from "@scandic-hotels/booking-flow/types/stores/booking-confirmation"
import type { Package } from "@scandic-hotels/trpc/types/packages"
export function mapToPrice(rooms: (Room | null)[], nights: number) {
return rooms
.filter((room): room is Room => !!room)
.map((room) => {
let price
if (room.cheques) {
price = {
corporateCheque: {
additionalPricePerStay: room.roomPrice ? room.roomPrice : undefined,
currency: room.roomPrice ? room.currencyCode : undefined,
numberOfCheques: room.cheques,
},
}
} else if (room.roomPoints) {
price = {
redemption: {
additionalPricePerStay: room.roomPrice ? room.roomPrice : undefined,
currency: room.roomPrice ? room.currencyCode : undefined,
pointsPerNight: room.roomPoints / nights,
pointsPerStay: room.roomPoints,
},
}
} else if (room.vouchers) {
price = {
voucher: {
numberOfVouchers: room.vouchers,
},
}
} else {
price = {
regular: {
currency: room.currencyCode,
pricePerNight: room.roomPrice / nights,
pricePerStay: room.roomPrice,
},
}
}
let breakfast:
| false
| undefined
| Omit<BreakfastPackage, "requestedPrice">
if (room.breakfast) {
const breakfastPackage = breakfastPackageSchema.safeParse({
code: room.breakfast?.code,
description: room.breakfast?.description,
localPrice: {
currency: room.breakfast?.currency,
price: room.breakfast?.unitPrice,
totalPrice: room.breakfast?.totalPrice,
},
packageType:
room.breakfast?.code === BreakfastPackageEnum.REGULAR_BREAKFAST
? PackageTypeEnum.BreakfastAdult
: "",
requestedPrice: {
currency: room.breakfast?.currency,
price: room.breakfast?.unitPrice,
totalPrice: room.breakfast?.totalPrice,
},
})
if (breakfastPackage.success) {
breakfast = breakfastPackage.data
}
}
if (!room.breakfastIncluded && !breakfast) {
breakfast = false
}
const packages = room.roomFeatures
?.map((featPkg) => {
const pkg = packageSchema.safeParse({
code: featPkg.code,
description: featPkg.description,
inventories: [],
localPrice: {
currency: featPkg.currency,
price: featPkg.unitPrice,
totalPrice: featPkg.totalPrice,
},
requestedPrice: {
currency: featPkg.currency,
price: featPkg.unitPrice,
totalPrice: featPkg.totalPrice,
},
})
if (pkg.success) {
return pkg.data
}
return null
})
.filter((pkg): pkg is Package => !!pkg)
return {
...room,
adults: room.adults,
bedType: {
description: room.bedDescription,
roomTypeCode: room.roomTypeCode || "",
type: room.bedType,
},
breakfast,
breakfastIncluded: room.rateDefinition.breakfastIncluded,
childrenInRoom: room.childrenAges?.map((age) => ({
age,
bed: ChildBedMapEnum.UNKNOWN,
})),
packages,
price,
roomType: room.name,
}
})
}

View File

@@ -1,26 +0,0 @@
import Body from "@scandic-hotels/design-system/Body"
import Link from "@scandic-hotels/design-system/Link"
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
import Title from "@scandic-hotels/design-system/Title"
import styles from "./promo.module.css"
import type { PromoProps } from "@/types/components/hotelReservation/bookingConfirmation/promo"
export default function Promo({ buttonText, href, text, title }: PromoProps) {
return (
<Link className={styles.link} color="none" href={href}>
<article className={styles.promo}>
<Title color="white" level="h4">
{title}
</Title>
<Body className={styles.text} color="white" textAlign="center">
{text}
</Body>
<Button asChild intent="primary" size="small" theme="primaryStrong">
<div>{buttonText}</div>
</Button>
</article>
</Link>
)
}

View File

@@ -1,40 +0,0 @@
.promo {
align-items: center;
background-position: 50%;
background-repeat: no-repeat;
background-size: cover;
border-radius: var(--Medium, 8px);
display: flex;
flex: 1 0 320px;
flex-direction: column;
gap: var(--Spacing-x2);
height: 320px;
justify-content: center;
padding: var(--Spacing-x4) var(--Spacing-x3);
}
.link:nth-of-type(1) .promo {
background-image:
linear-gradient(
180deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.36) 37.88%,
rgba(0, 0, 0, 0.75) 100%
),
url("/_static/img/Scandic_Park_Party_Lipstick.jpg");
}
.link:nth-of-type(2) .promo {
background-image:
linear-gradient(
180deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.36) 37.88%,
rgba(0, 0, 0, 0.75) 100%
),
url("/_static/img/Scandic_Family_Breakfast.jpg");
}
.text {
max-width: 400px;
}

View File

@@ -1,64 +0,0 @@
"use client"
import { useEffect } from "react"
import { useIntl } from "react-intl"
import { myStay } from "@scandic-hotels/common/constants/routes/myStay"
import useLang from "@/hooks/useLang"
import Promo from "./Promo"
import styles from "./promos.module.css"
import type { AdditionalInfoCookieValue } from "@scandic-hotels/booking-flow/types/components/findMyBooking/additionalInfoCookieValue"
import type { PromosProps } from "@/types/components/hotelReservation/bookingConfirmation/promos"
export default function Promos({ booking }: PromosProps) {
const intl = useIntl()
const lang = useLang()
const { refId, confirmationNumber, hotelId } = booking
const { email, firstName, lastName } = booking.guest
useEffect(() => {
// Setting the `bv` cookie allows direct access to My stay without prompting for more information.
const value: AdditionalInfoCookieValue = {
email,
firstName,
lastName,
confirmationNumber,
}
document.cookie = `bv=${JSON.stringify(value)}; Path=/; Max-Age=600; Secure; SameSite=Strict`
}, [confirmationNumber, email, firstName, lastName])
const myStayURL = `${myStay[lang]}?RefId=${encodeURIComponent(refId)}`
return (
<div className={styles.promos}>
<Promo
buttonText={intl.formatMessage({
defaultMessage: "View and buy add-ons",
})}
href={myStayURL}
text={intl.formatMessage({
defaultMessage:
"Discover the little extra touches to make your upcoming stay even more unforgettable.",
})}
title={intl.formatMessage({
defaultMessage: "Spice things up",
})}
/>
<Promo
buttonText={intl.formatMessage({
defaultMessage: "Book another stay",
})}
href={`/${lang}?hotel=${hotelId}`}
text={intl.formatMessage({
defaultMessage:
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
})}
title={intl.formatMessage({
defaultMessage: "Book your next stay",
})}
/>
</div>
)
}

View File

@@ -1,12 +0,0 @@
.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

@@ -1,12 +0,0 @@
.entry {
display: flex;
justify-content: space-between;
}
.textDefault {
color: var(--Text-Default);
}
.textSecondary {
color: var(--Text-Secondary);
}

View File

@@ -1,64 +0,0 @@
"use client"
import { useIntl } from "react-intl"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import { Typography } from "@scandic-hotels/design-system/Typography"
import styles from "./breakfast.module.css"
import type { PackageSchema } from "@scandic-hotels/trpc/types/bookingConfirmation"
interface BreakfastProps {
breakfast: PackageSchema | false | undefined
breakfastIncluded: boolean
guests: string
}
export default function Breakfast({
breakfast,
breakfastIncluded,
guests,
}: BreakfastProps) {
const intl = useIntl()
const breakfastBuffet = intl.formatMessage({
defaultMessage: "Breakfast buffet",
})
if (breakfastIncluded || breakfast) {
const price = breakfast
? formatPrice(intl, breakfast.totalPrice, breakfast.currency)
: intl.formatMessage({ defaultMessage: "Included" })
return (
<div className={styles.entry}>
<div>
<Typography variant="Body/Paragraph/mdRegular">
<p className={styles.textDefault}>{breakfastBuffet}</p>
</Typography>
<Typography variant="Body/Supporting text (caption)/smRegular">
<p className={styles.textSecondary}>{guests}</p>
</Typography>
</div>
<Typography variant="Body/Paragraph/mdRegular">
<p className={styles.textDefault}>{price}</p>
</Typography>
</div>
)
}
if (breakfast === false) {
const noBreakfast = intl.formatMessage({ defaultMessage: "No breakfast" })
return (
<div className={styles.entry}>
<Typography variant="Body/Paragraph/mdRegular">
<p className={styles.textDefault}>{breakfastBuffet}</p>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
<p className={styles.textDefault}>{noBreakfast}</p>
</Typography>
</div>
)
}
return null
}

View File

@@ -1,17 +0,0 @@
import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer"
import styles from "./roomSkeletonLoader.module.css"
export default function RoomSkeletonLoader() {
return (
<div className={styles.room}>
<SkeletonShimmer />
<SkeletonShimmer width={"15%"} />
<SkeletonShimmer width={"30%"} />
<SkeletonShimmer width={"40%"} />
<SkeletonShimmer />
<SkeletonShimmer />
<SkeletonShimmer />
</div>
)
}

View File

@@ -1,270 +0,0 @@
"use client"
import { cx } from "class-variance-authority"
import { useIntl } from "react-intl"
import { useBookingConfirmationStore } from "@scandic-hotels/booking-flow/stores/booking-confirmation"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import { Button } from "@scandic-hotels/design-system/Button"
import { Divider } from "@scandic-hotels/design-system/Divider"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Modal from "@scandic-hotels/design-system/Modal"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { ChildBedTypeEnum } from "@scandic-hotels/trpc/enums/childBedTypeEnum"
import { CancellationRuleEnum } from "@/constants/booking"
import { getFeatureDescription } from "@/components/HotelReservation/utils/getRoomFeatureDescription"
import Breakfast from "./Breakfast"
import RoomSkeletonLoader from "./RoomSkeletonLoader"
import styles from "./room.module.css"
import type { BookingConfirmationReceiptRoomProps } from "@/types/components/hotelReservation/bookingConfirmation/receipt"
export default function ReceiptRoom({
room,
roomNumber,
roomCount,
}: BookingConfirmationReceiptRoomProps) {
const intl = useIntl()
const { currencyCode, isVatCurrency } = useBookingConfirmationStore(
(state) => ({
currencyCode: state.currencyCode,
isVatCurrency: state.isVatCurrency,
})
)
if (!room) {
return <RoomSkeletonLoader />
}
const childBedCrib = room.childBedPreferences.find(
(c) => c.bedType === ChildBedTypeEnum.Crib
)
const childBedExtraBed = room.childBedPreferences.find(
(c) => c.bedType === ChildBedTypeEnum.ExtraBed
)
const adultsMsg = intl.formatMessage(
{
defaultMessage: "{totalAdults, plural, one {# adult} other {# adults}}",
},
{ totalAdults: room.adults }
)
const guestsParts = [adultsMsg]
if (room.childrenAges?.length) {
const childrenMsg = intl.formatMessage(
{
defaultMessage:
"{totalChildren, plural, one {# child} other {# children}}",
},
{ totalChildren: room.childrenAges.length }
)
guestsParts.push(childrenMsg)
}
const guests = guestsParts.join(", ")
const showDiscounted = room.rateDefinition.isMemberRate
return (
<>
<div className={styles.room}>
<div>
{roomCount > 1 ? (
<Typography variant="Body/Supporting text (caption)/smBold">
<p className={styles.roomTitle}>
{intl.formatMessage(
{
defaultMessage: "Room {roomIndex}",
},
{
roomIndex: roomNumber,
}
)}
</p>
</Typography>
) : null}
<div className={styles.entry}>
<div>
<Typography variant="Body/Paragraph/mdBold">
<p>{room.name}</p>
</Typography>
<Typography variant="Body/Supporting text (caption)/smRegular">
<div className={styles.additionalInformation}>
<p>{guestsParts.join(", ")}</p>
<p>{room.rateDefinition.cancellationText}</p>
</div>
</Typography>
</div>
<Typography variant="Body/Paragraph/mdRegular">
<div className={styles.prices}>
<p
className={cx(styles.price, {
[styles.discounted]: showDiscounted,
})}
>
{room.formattedRoomCost}
</p>
{/* TODO: add original price, we're currently not receiving this value from API */}
</div>
</Typography>
</div>
{room.rateDefinition.generalTerms ? (
<div className={styles.ctaWrapper}>
<Modal
trigger={
<Button
className={styles.termsButton}
variant="Text"
typography="Body/Supporting text (caption)/smBold"
wrapping={false}
>
{intl.formatMessage({
defaultMessage: "Reservation policy",
})}
<MaterialIcon
icon="chevron_right"
size={20}
color="CurrentColor"
/>
</Button>
}
title={
(isVatCurrency
? room.rateDefinition.cancellationText
: room.rateDefinition.title) || ""
}
subtitle={
room.rateDefinition.cancellationRule ===
CancellationRuleEnum.CancellableBefore6PM
? intl.formatMessage({
defaultMessage: "Pay later",
})
: intl.formatMessage({
defaultMessage: "Pay now",
})
}
>
<div className={styles.terms}>
{room.rateDefinition.generalTerms?.map((info) => (
<Typography
key={info}
className={styles.termsText}
variant="Body/Paragraph/mdRegular"
>
<span>
<MaterialIcon
icon="check"
color="Icon/Feedback/Success"
size={20}
className={styles.termsIcon}
/>
{info}
</span>
</Typography>
))}
</div>
</Modal>
</div>
) : null}
</div>
{room.roomFeatures
? room.roomFeatures.map((feature) => (
<div className={styles.entry} key={feature.code}>
<div>
<Typography variant="Body/Paragraph/mdRegular">
<p className={styles.uiTextHighContrast}>
{getFeatureDescription(
feature.code,
feature.description,
intl
)}
</p>
</Typography>
</div>
<Typography variant="Body/Paragraph/mdRegular">
<p className={styles.uiTextHighContrast}>
{formatPrice(intl, feature.totalPrice, feature.currency)}
</p>
</Typography>
</div>
))
: null}
<div className={styles.entry}>
<Typography variant="Body/Paragraph/mdRegular">
<p className={styles.uiTextHighContrast}>{room.bedDescription}</p>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
<p className={styles.uiTextHighContrast}>
{formatPrice(intl, 0, currencyCode)}
</p>
</Typography>
</div>
{childBedCrib ? (
<Typography variant="Body/Paragraph/mdRegular">
<div className={styles.entry}>
<div>
<p>
{intl.formatMessage(
{
defaultMessage: "Crib (child) × {count}",
},
{ count: childBedCrib.quantity }
)}
</p>
<Typography variant="Body/Supporting text (caption)/smRegular">
<p className={styles.uiTextHighContrast}>
{intl.formatMessage({
defaultMessage: "Subject to availability",
})}
</p>
</Typography>
</div>
<div className={styles.prices}>
<span className={styles.price}>
{formatPrice(intl, 0, currencyCode)}
</span>
</div>
</div>
</Typography>
) : null}
{childBedExtraBed ? (
<Typography variant="Body/Paragraph/mdRegular">
<div className={styles.entry}>
<div>
<p>
{intl.formatMessage(
{
defaultMessage: "Extra bed (child) × {count}",
},
{
count: childBedExtraBed.quantity,
}
)}
</p>
</div>
<div className={styles.prices}>
<span className={styles.price}>
{formatPrice(intl, 0, currencyCode)}
</span>
</div>
</div>
</Typography>
) : null}
<Breakfast
breakfast={room.breakfast}
breakfastIncluded={room.breakfastIncluded}
guests={guests}
/>
</div>
<Divider color="Border/Divider/Subtle" />
</>
)
}

View File

@@ -1,55 +0,0 @@
.room {
display: flex;
flex-direction: column;
gap: var(--Space-x15);
overflow-y: auto;
color: var(--Text-Default);
}
.roomTitle,
.additionalInformation {
color: var(--Text-Secondary);
}
.terms {
margin-top: var(--Space-x3);
margin-bottom: var(--Space-x3);
}
.termsText:nth-child(n) {
display: flex;
margin-bottom: var(--Space-x1);
}
.terms .termsIcon {
margin-right: var(--Space-x1);
}
.entry {
display: flex;
gap: var(--Spacing-x-half);
justify-content: space-between;
}
.prices {
justify-items: flex-end;
flex-shrink: 0;
display: grid;
align-content: start;
}
.price {
color: var(--Text-Default);
&.discounted {
color: var(--Text-Accent-Primary);
}
}
.strikeThroughRate {
text-decoration: line-through;
color: var(--Text-Secondary);
}
.ctaWrapper {
margin-top: var(--Space-x15);
}

View File

@@ -1,5 +0,0 @@
.room {
display: flex;
gap: var(--Spacing-x1);
flex-direction: column;
}

View File

@@ -1,83 +0,0 @@
"use client"
import { cx } from "class-variance-authority"
import { useIntl } from "react-intl"
import { useBookingConfirmationStore } from "@scandic-hotels/booking-flow/stores/booking-confirmation"
import { BookingCodeChip } from "@scandic-hotels/design-system/BookingCodeChip"
import { Divider } from "@scandic-hotels/design-system/Divider"
import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer"
import { Typography } from "@scandic-hotels/design-system/Typography"
import PriceDetails from "../../PriceDetails"
import styles from "./totalPrice.module.css"
export default function TotalPrice() {
const intl = useIntl()
const { rooms, formattedTotalCost } = useBookingConfirmationStore(
(state) => ({
bookingCode: state.bookingCode,
rooms: state.rooms,
formattedTotalCost: state.formattedTotalCost,
})
)
const hasAllRoomsLoaded = rooms.every((room) => room)
const bookingCode = rooms.find((room) => room?.bookingCode)?.bookingCode
const isMemberRate = rooms.some((room) => room?.rateDefinition.isMemberRate)
const showDiscounted = bookingCode || isMemberRate
return (
<>
<Divider color="Border/Divider/Subtle" />
<div className={styles.price}>
<div className={styles.entry}>
<div>
<Typography variant="Body/Paragraph/mdRegular">
<p>
{intl.formatMessage(
{
defaultMessage: "<b>Total price</b> (incl VAT)",
},
{
b: (str) => (
<Typography variant="Body/Paragraph/mdBold">
<span>{str}</span>
</Typography>
),
}
)}
</p>
</Typography>
{/* TODO: Add approx price, we're currently not receiving this value from API */}
</div>
<div className={styles.prices}>
{hasAllRoomsLoaded ? (
<Typography variant="Body/Paragraph/mdBold">
<span
className={cx(styles.price, {
[styles.discounted]: showDiscounted,
})}
>
{formattedTotalCost}
</span>
</Typography>
) : (
<SkeletonShimmer width={"25%"} />
)}
</div>
</div>
</div>
<div className={styles.ctaWrapper}>
{hasAllRoomsLoaded ? (
<PriceDetails />
) : (
<SkeletonShimmer width={"100%"} />
)}
</div>
{bookingCode && <BookingCodeChip bookingCode={bookingCode} alignCenter />}
</>
)
}

View File

@@ -1,33 +0,0 @@
.entry {
display: flex;
gap: var(--Space-x05);
justify-content: space-between;
margin-bottom: var(--Space-x15);
}
.prices {
justify-items: flex-end;
flex-shrink: 0;
display: grid;
}
.price {
color: var(--Text-Default);
&.discounted {
color: var(--Text-Accent-Primary);
}
}
.strikeThroughRate {
text-decoration: line-through;
color: var(--Text-Secondary);
}
.approxPrice {
color: var(--Text-Secondary);
}
.ctaWrapper {
margin-top: var(--Space-x15);
}

View File

@@ -1,75 +0,0 @@
"use client"
import { useIntl } from "react-intl"
import { useBookingConfirmationStore } from "@scandic-hotels/booking-flow/stores/booking-confirmation"
import { longDateFormat } from "@scandic-hotels/common/constants/dateFormats"
import { dt } from "@scandic-hotels/common/dt"
import { Divider } from "@scandic-hotels/design-system/Divider"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import useLang from "@/hooks/useLang"
import Room from "./Room"
import TotalPrice from "./TotalPrice"
import styles from "./receipt.module.css"
export default function Receipt() {
const lang = useLang()
const intl = useIntl()
const { rooms, fromDate, toDate } = useBookingConfirmationStore((state) => ({
rooms: state.rooms,
fromDate: state.fromDate,
toDate: state.toDate,
}))
const totalNights = dt(toDate).diff(fromDate, "days")
const nights = intl.formatMessage(
{
defaultMessage: "{totalNights, plural, one {# night} other {# nights}}",
},
{ totalNights }
)
const filteredRooms = rooms.filter(
(room): room is NonNullable<typeof room> => !!room
)
return (
<section className={styles.receipt}>
<header>
<Typography variant="Title/Subtitle/md">
<h3 className={styles.heading}>
{intl.formatMessage({
defaultMessage: "Booking summary",
})}
</h3>
</Typography>
<Typography variant="Body/Paragraph/mdBold">
<p className={styles.dates}>
{dt(fromDate).locale(lang).format(longDateFormat[lang])}
<MaterialIcon icon="arrow_forward" size={15} color="CurrentColor" />
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
{dt(toDate).locale(lang).format(longDateFormat[lang])} ({nights})
</p>
</Typography>
</header>
<Divider color="Border/Divider/Subtle" />
{filteredRooms.map((room, idx) => (
<Room
key={room ? room.confirmationNumber : `loader-${idx}`}
room={room}
roomNumber={idx + 1}
roomCount={rooms.length}
/>
))}
<TotalPrice />
</section>
)
}

View File

@@ -1,22 +0,0 @@
.receipt {
display: grid;
gap: var(--Space-x2);
}
.heading {
color: var(--Text-Default);
}
.dates {
display: flex;
align-items: center;
gap: var(--Space-x1);
justify-content: flex-start;
color: var(--Text-Accent-Secondary);
}
@media screen and (min-width: 1367px) {
.receipt {
padding: var(--Space-x3);
}
}

View File

@@ -1,33 +0,0 @@
import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer"
import styles from "./linkedReservationCardSkeleton.module.css"
export function LinkedReservationCardSkeleton() {
return (
<div className={styles.card}>
<div className={styles.content}>
<div className={styles.img}>
<SkeletonShimmer height={"204px"} width={"100%"} />
</div>
<div className={styles.roomDetails}>
<div className={styles.roomName}>
<SkeletonShimmer height={"24px"} width={"130px"} />
<SkeletonShimmer height={"20px"} width={"140px"} />
</div>
<div className={styles.details}>
<SkeletonShimmer height={"20px"} width={"300px"} />
<SkeletonShimmer height={"20px"} width={"300px"} />
<SkeletonShimmer height={"20px"} width={"300px"} />
<SkeletonShimmer height={"20px"} width={"300px"} />
</div>
<div className={styles.guest}>
<SkeletonShimmer height={"20px"} width={"100px"} />
<SkeletonShimmer height={"20px"} width={"200px"} />
<SkeletonShimmer height={"20px"} width={"150px"} />
<SkeletonShimmer height={"20px"} width={"300px"} />
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,29 +0,0 @@
"use client"
import { useIntl } from "react-intl"
import Body from "@scandic-hotels/design-system/Body"
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
import styles from "./retry.module.css"
import type { RetryProps } from "@/types/components/hotelReservation/bookingConfirmation/rooms/linkedReservation"
export default function Retry({ handleRefetch }: RetryProps) {
const intl = useIntl()
return (
<div className={styles.retry}>
<Body>
{intl.formatMessage({
defaultMessage: "Something went wrong!",
})}
</Body>
<Button size={"small"} onPress={handleRefetch}>
{intl.formatMessage({
defaultMessage: "Try again",
})}
</Button>
</div>
)
}

View File

@@ -1,91 +0,0 @@
"use client"
import { useEffect } from "react"
import { useIntl } from "react-intl"
import { useBookingConfirmationStore } from "@scandic-hotels/booking-flow/stores/booking-confirmation"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import { trpc } from "@scandic-hotels/trpc/client"
import useLang from "@/hooks/useLang"
import { mapRoomState } from "../../utils"
import Room from "../Room"
import { LinkedReservationCardSkeleton } from "./LinkedReservationCardSkeleton"
import Retry from "./Retry"
import type { LinkedReservationProps } from "@/types/components/hotelReservation/bookingConfirmation/rooms/linkedReservation"
export function LinkedReservation({
checkInTime,
checkOutTime,
refId,
roomIndex,
}: LinkedReservationProps) {
const lang = useLang()
const { data, refetch, isLoading } = trpc.booking.get.useQuery({
refId,
lang,
})
const {
setRoom,
setFormattedTotalCost,
currencyCode,
totalBookingPrice,
totalBookingCheques,
} = useBookingConfirmationStore((state) => ({
setRoom: state.actions.setRoom,
setFormattedTotalCost: state.actions.setFormattedTotalCost,
currencyCode: state.currencyCode,
totalBookingPrice: state.totalBookingPrice,
totalBookingCheques: state.totalBookingCheques,
}))
const intl = useIntl()
useEffect(() => {
if (data?.room) {
const roomData = mapRoomState(data.booking, data.room, intl)
setRoom(roomData, roomIndex)
const formattedTotalCost = totalBookingCheques
? formatPrice(
intl,
totalBookingCheques,
CurrencyEnum.CC,
totalBookingPrice,
currencyCode
)
: formatPrice(intl, totalBookingPrice, currencyCode)
setFormattedTotalCost(formattedTotalCost)
}
}, [
data,
roomIndex,
intl,
setRoom,
totalBookingCheques,
totalBookingPrice,
currencyCode,
setFormattedTotalCost,
])
if (isLoading) {
return <LinkedReservationCardSkeleton />
}
if (!data?.room) {
return <Retry handleRefetch={refetch} />
}
return (
<Room
booking={data.booking}
checkInTime={checkInTime}
checkOutTime={checkOutTime}
img={data.room.images[0]}
roomName={data.room.name}
/>
)
}

View File

@@ -1,66 +0,0 @@
.card {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
}
.content {
background-color: var(--Background-Primary);
border-radius: var(--Corner-radius-lg);
display: grid;
gap: var(--Spacing-x2);
padding: var(--Spacing-x2) var(--Spacing-x2) var(--Spacing-x3)
var(--Spacing-x2);
}
.img {
border-radius: var(--Corner-radius-md);
overflow: hidden;
}
.roomDetails {
display: grid;
gap: var(--Spacing-x2);
}
.roomName {
display: flex;
flex-direction: column;
gap: var(--Spacing-x1);
grid-column: 1/-1;
justify-content: space-evenly;
}
.details {
display: flex;
flex-direction: column;
gap: var(--Spacing-x1);
justify-content: space-evenly;
}
.guest {
display: flex;
flex-direction: column;
gap: var(--Spacing-x1);
justify-content: space-evenly;
}
@media screen and (min-width: 1367px) {
.content {
gap: var(--Spacing-x3);
grid-template-columns: auto 1fr;
padding: var(--Spacing-x2) var(--Spacing-x3) var(--Spacing-x2)
var(--Spacing-x2);
}
.img {
min-width: 306px;
}
.roomDetails {
grid-template-columns: 1fr 1fr;
}
.guest {
align-items: flex-end;
}
}

View File

@@ -1,10 +0,0 @@
.retry {
background-color: var(--Background-Primary);
border-radius: var(--Corner-radius-lg);
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
padding: var(--Spacing-x2) var(--Spacing-x2) var(--Spacing-x3)
var(--Spacing-x2);
align-items: center;
}

View File

@@ -1,238 +0,0 @@
"use client"
import { useIntl } from "react-intl"
import { RoomDetailsSidePeek } from "@scandic-hotels/booking-flow/components/RoomDetailsSidePeek"
import { useBookingConfirmationStore } from "@scandic-hotels/booking-flow/stores/booking-confirmation"
import {
changeOrCancelDateFormat,
longDateFormat,
} from "@scandic-hotels/common/constants/dateFormats"
import { dt } from "@scandic-hotels/common/dt"
import Caption from "@scandic-hotels/design-system/Caption"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Image from "@scandic-hotels/design-system/Image"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { getHotelRoom } from "@scandic-hotels/trpc/routers/booking/helpers"
import { CancellationRuleEnum } from "@/constants/booking"
import useLang from "@/hooks/useLang"
import styles from "./room.module.css"
import type { RoomProps } from "@/types/components/hotelReservation/bookingConfirmation/rooms/room"
export default function Room({
booking,
checkInTime,
checkOutTime,
img,
roomName,
}: RoomProps) {
const intl = useIntl()
const lang = useLang()
const { roomCategories } = useBookingConfirmationStore((state) => ({
roomCategories: state.roomCategories,
}))
const room = getHotelRoom(roomCategories, booking.roomTypeCode)
const guestName = `${booking.guest.firstName} ${booking.guest.lastName}`
const fromDate = dt(booking.checkInDate).locale(lang)
const toDate = dt(booking.checkOutDate).locale(lang)
const isFlexBooking =
booking.rateDefinition.cancellationRule ===
CancellationRuleEnum.CancellableBefore6PM
const isChangeBooking =
booking.rateDefinition.cancellationRule === CancellationRuleEnum.Changeable
return (
<article className={styles.room}>
<header className={styles.header}>
<Typography variant="Title/Subtitle/md">
<h2>
{intl.formatMessage(
{
defaultMessage: "Booking number {value}",
},
{ value: booking.confirmationNumber }
)}
</h2>
</Typography>
{booking.rateDefinition.isMemberRate ? (
<div className={styles.benefits}>
<>
<MaterialIcon
color="Icon/Feedback/Success"
icon="check_circle"
size={20}
/>
<Caption>
{intl.formatMessage({
defaultMessage: "Membership benefits applied",
})}
</Caption>
</>
</div>
) : null}
{booking.guaranteeInfo && (
<div className={styles.benefits}>
<MaterialIcon
icon="check_circle"
color="Icon/Feedback/Success"
size={20}
/>
<Typography
variant="Body/Supporting text (caption)/smBold"
className={styles.guaranteeText}
>
<p>
{intl.formatMessage({
defaultMessage: "Booking guaranteed.",
})}
</p>
</Typography>
{/* eslint-disable formatjs/no-literal-string-in-jsx */}{" "}
{/* eslint-enable formatjs/no-literal-string-in-jsx */}
<Typography variant="Body/Supporting text (caption)/smRegular">
<p>
{intl.formatMessage({
defaultMessage:
"Your room will remain available for check-in even after 18:00.",
})}
</p>
</Typography>
</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-md)" }}
title={img?.metaData.title || img?.metaData.title_En || ""}
width={204}
/>
<div className={styles.roomDetails}>
<div className={styles.roomName}>
<Typography variant="Title/Subtitle/md">
<h2>{roomName}</h2>
</Typography>
{room && (
<RoomDetailsSidePeek
hotelId={booking.hotelId}
room={room}
roomTypeCode={booking.roomTypeCode}
buttonVariant="primary"
triggerLabel={intl.formatMessage({
defaultMessage: "View room details",
})}
wrapping={false}
/>
)}
</div>
<Typography variant="Body/Paragraph/mdRegular">
<ul className={styles.details}>
<li className={styles.listItem}>
<p className={styles.label}>
{intl.formatMessage({
defaultMessage: "Check-in",
})}
</p>
<p>
{intl.formatMessage(
{
defaultMessage: "{checkInDate} from {checkInTime}",
},
{
checkInDate: fromDate.format(longDateFormat[lang]),
checkInTime: checkInTime,
}
)}
</p>
</li>
<li className={styles.listItem}>
<p className={styles.label}>
{intl.formatMessage({
defaultMessage: "Check-out",
})}
</p>
<p>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
{`${toDate.format(longDateFormat[lang])}, ${checkOutTime}`}
</p>
</li>
<li className={styles.listItem}>
<p className={styles.label}>
{intl.formatMessage({
defaultMessage: "Cancellation policy",
})}
</p>
<p>{booking.rateDefinition.cancellationText}</p>
</li>
{isFlexBooking || isChangeBooking ? (
<li className={styles.listItem}>
<p className={styles.label}>
{intl.formatMessage({
defaultMessage: "Change or cancel",
})}
</p>
<p>
{intl.formatMessage(
{
defaultMessage: "Until {time}, {date}",
},
{
time: "18:00",
date: fromDate.format(changeOrCancelDateFormat[lang]),
}
)}
</p>
</li>
) : null}
</ul>
</Typography>
<div className={styles.guest}>
<Typography variant="Body/Paragraph/mdRegular">
<p className={styles.label}>
{intl.formatMessage({
defaultMessage: "Guest",
})}
</p>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
<p data-hj-suppress>{guestName}</p>
</Typography>
{booking.guest.membershipNumber ? (
<Typography variant="Body/Paragraph/mdRegular">
<p data-hj-suppress>
{intl.formatMessage(
{
defaultMessage: "Friend no. {value}",
},
{
value: booking.guest.membershipNumber,
}
)}
</p>
</Typography>
) : null}
{booking.guest.phoneNumber ? (
<Typography variant="Body/Paragraph/mdRegular">
<p data-hj-suppress>{booking.guest.phoneNumber}</p>
</Typography>
) : null}
{booking.guest.email ? (
<Typography variant="Body/Paragraph/mdRegular">
<p data-hj-suppress>{booking.guest.email}</p>
</Typography>
) : null}
</div>
</div>
</div>
</article>
)
}

View File

@@ -1,114 +0,0 @@
.room {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
}
.header {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
}
.benefits {
align-items: center;
border: 1px solid var(--Base-Border-Subtle);
border-radius: var(--Corner-radius-md);
display: flex;
gap: var(--Spacing-x1);
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half);
width: min(max-content, 100%);
}
.guaranteeText {
color: var(--Text-Feedback-Succes-Accent);
}
.booking {
background-color: var(--Background-Primary);
border-radius: var(--Corner-radius-lg);
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;
align-items: flex-start;
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: grid;
grid-template-columns: 1fr 1fr;
justify-content: space-between;
gap: var(--Space-x3);
}
.guest {
display: flex;
flex-direction: column;
gap: var(--Spacing-x-half);
}
.label {
color: var(--Text-Tertiary);
}
.details p:nth-of-type(even) {
text-align: right;
}
@media screen and (max-width: 1366px) {
.details {
padding-bottom: var(--Space-x1);
}
}
@media screen and (min-width: 1367px) {
.header {
grid-template-columns: 1fr auto;
}
.details p:nth-of-type(even) {
text-align: left;
}
.img {
width: 204px;
}
.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 auto;
}
.guest {
align-items: flex-end;
align-self: flex-end;
}
}

View File

@@ -1,66 +0,0 @@
import { Typography } from "@scandic-hotels/design-system/Typography"
import { getIntl } from "@/i18n"
import { LinkedReservation } from "./LinkedReservation"
import Room from "./Room"
import styles from "./rooms.module.css"
import type { BookingConfirmationRoomsProps } from "@/types/components/hotelReservation/bookingConfirmation/rooms"
export default async function Rooms({
booking,
checkInTime,
checkOutTime,
mainRoom,
}: BookingConfirmationRoomsProps) {
const intl = await getIntl()
return (
<section className={styles.rooms}>
<div className={styles.room}>
{booking.linkedReservations.length ? (
<Typography variant="Title/Subtitle/md">
<h2 className={styles.roomTitle}>
{intl.formatMessage(
{
defaultMessage: "Room {roomIndex}",
},
{ roomIndex: 1 }
)}
</h2>
</Typography>
) : null}
<Room
booking={booking}
checkInTime={checkInTime}
checkOutTime={checkOutTime}
img={mainRoom.images[0]}
roomName={mainRoom.name}
/>
</div>
{booking.linkedReservations.map((reservation, idx) => (
<div className={styles.room} key={reservation.confirmationNumber}>
<Typography variant="Title/Subtitle/md">
<h2 className={styles.roomTitle}>
{intl.formatMessage(
{
defaultMessage: "Room {roomIndex}",
},
{ roomIndex: idx + 2 }
)}
</h2>
</Typography>
<LinkedReservation
checkInTime={checkInTime}
checkOutTime={checkOutTime}
refId={reservation.refId}
roomIndex={idx + 1}
/>
</div>
))}
</section>
)
}

View File

@@ -1,15 +0,0 @@
.rooms {
display: flex;
flex-direction: column;
gap: var(--Spacing-x5);
}
.room {
display: flex;
flex-direction: column;
gap: var(--Space-x025);
}
.roomTitle {
color: var(--Text-Tertiary);
}

View File

@@ -2,6 +2,7 @@ import { createHash } from "crypto"
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
import { invertedBedTypeMap } from "@scandic-hotels/booking-flow/utils/SelectRate"
import { CancellationRuleEnum } from "@scandic-hotels/common/constants/booking"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { RateEnum } from "@scandic-hotels/common/constants/rate"
import {
@@ -14,8 +15,6 @@ import {
import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast"
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
import { CancellationRuleEnum } from "@/constants/booking"
import { readPaymentInfoFromSessionStorage } from "@/components/HotelReservation/EnterDetails/Payment/helpers"
import { getSpecialRoomType } from "@/utils/specialRoomType"

View File

@@ -1,22 +0,0 @@
.booking {
display: flex;
flex-direction: column;
gap: var(--Spacing-x5);
grid-area: booking;
padding-bottom: var(--Spacing-x9);
}
.aside {
display: none;
}
@media screen and (min-width: 1367px) {
.mobileReceipt {
display: none;
}
.aside {
display: grid;
grid-area: receipt;
}
}

View File

@@ -1,112 +0,0 @@
import { notFound } from "next/navigation"
import { Confirmation } from "@scandic-hotels/booking-flow/components/BookingConfirmation/Confirmation"
import BookingConfirmationProvider from "@scandic-hotels/booking-flow/providers/BookingConfirmationProvider"
import { filterOverlappingDates } from "@scandic-hotels/booking-flow/utils/SelectRate"
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
import { dt } from "@scandic-hotels/common/dt"
import { Alert } from "@scandic-hotels/design-system/Alert"
import { Divider } from "@scandic-hotels/design-system/Divider"
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
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 { getIntl } from "@/i18n"
import Tracking from "./Tracking"
import { mapRoomState } from "./utils"
import styles from "./bookingConfirmation.module.css"
type BookingConfirmationProps = {
refId: string
membershipFailedError: boolean
}
export default async function BookingConfirmation({
refId,
membershipFailedError,
}: BookingConfirmationProps) {
const bookingConfirmation = await getBookingConfirmation(refId)
if (!bookingConfirmation) {
return notFound()
}
const { booking, hotel, room, roomCategories } = bookingConfirmation
if (!room) {
return notFound()
}
const intl = await getIntl()
return (
<BookingConfirmationProvider
bookingCode={booking.bookingCode}
currencyCode={booking.currencyCode}
fromDate={booking.checkInDate}
toDate={booking.checkOutDate}
roomCategories={roomCategories}
rooms={[
mapRoomState(booking, room, intl),
// null represents "known but not yet fetched rooms" and is used to render placeholders correctly
...Array(booking.linkedReservations.length).fill(null),
]}
vat={booking.vatPercentage}
>
<Confirmation booking={booking} hotel={hotel} room={room}>
<div className={styles.booking}>
{membershipFailedError && (
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({
defaultMessage: "Failed to verify membership",
})}
text={intl.formatMessage({
defaultMessage:
"Your booking(s) is confirmed but we could not verify your membership. If you have booked with a member discount, you'll either need to present your existing membership number upon check-in, become a member or pay the price difference at the hotel. Signing up is preferably done online before the stay.",
})}
/>
)}
<Rooms
booking={booking}
checkInTime={hotel.hotelFacts.checkin.checkInTime}
checkOutTime={hotel.hotelFacts.checkin.checkOutTime}
mainRoom={room}
/>
<PaymentDetails />
<Divider color="Border/Divider/Subtle" />
<HotelDetails hotel={hotel} />
{filterOverlappingDates(
hotel.specialAlerts,
dt(booking.checkInDate),
dt(booking.checkOutDate)
).map((alert) => (
<div key={alert.id}>
<Alert
type={alert.type}
heading={alert.heading}
text={alert.text}
/>
</div>
))}
<Promos booking={booking} />
<div className={styles.mobileReceipt}>
<Receipt />
</div>
</div>
<aside className={styles.aside}>
<SidePanel variant="receipt">
<Receipt />
</SidePanel>
</aside>
</Confirmation>
<Tracking bookingConfirmation={bookingConfirmation} refId={refId} />
</BookingConfirmationProvider>
)
}

View File

@@ -1,91 +0,0 @@
import { type IntlShape } from "react-intl"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast"
import type { BookingConfirmationRoom } from "@scandic-hotels/booking-flow/types/components/bookingConfirmation/bookingConfirmation"
import type {
BookingConfirmationSchema,
PackageSchema,
} from "@scandic-hotels/trpc/types/bookingConfirmation"
export function mapRoomState(
booking: BookingConfirmationSchema,
room: BookingConfirmationRoom,
intl: IntlShape
) {
const breakfastPackage = booking.packages.find(
(pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST
)
const breakfastIncluded =
booking.packages.some(
(pkg) => pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
) || booking.rateDefinition.breakfastIncluded
let breakfast: false | undefined | PackageSchema
if (breakfastPackage) {
breakfast = breakfastPackage
} else if (!breakfastIncluded) {
breakfast = false
}
let formattedRoomCost = formatPrice(
intl,
booking.roomPrice,
booking.currencyCode
)
if (booking.roomPoints) {
formattedRoomCost = formatPrice(
intl,
booking.roomPoints,
CurrencyEnum.POINTS,
booking.roomPrice,
booking.currencyCode
)
} else if (booking.cheques) {
formattedRoomCost = formatPrice(
intl,
booking.cheques,
CurrencyEnum.CC,
booking.roomPrice,
booking.currencyCode
)
} else if (booking.vouchers) {
formattedRoomCost = formatPrice(
intl,
booking.vouchers,
CurrencyEnum.Voucher
)
}
return {
adults: booking.adults,
bedDescription: room.bedType.description,
bedType: room.bedType.mainBed.type,
bookingCode: booking.bookingCode,
breakfast,
breakfastIncluded,
cheques: booking.cheques,
childrenAges: booking.childrenAges,
childBedPreferences: booking.childBedPreferences,
confirmationNumber: booking.confirmationNumber,
currencyCode: booking.currencyCode,
formattedRoomCost,
fromDate: booking.checkInDate,
name: room.name,
packages: booking.packages,
rateDefinition: booking.rateDefinition,
rateCodeType: booking.bookingType,
refId: booking.refId,
roomFeatures: booking.packages.filter((p) => p.type === "RoomFeature"),
roomPoints: booking.roomPoints,
roomPrice: booking.roomPrice,
roomTypeCode: booking.roomTypeCode,
toDate: booking.checkOutDate,
totalPrice: booking.totalPrice,
totalPriceExVat: booking.totalPriceExVat,
vatAmount: booking.vatAmount,
vouchers: booking.vouchers,
}
}

View File

@@ -4,10 +4,10 @@ import { useRouter } from "next/navigation"
import { useEffect } from "react"
import { serializeBookingSearchParams } from "@scandic-hotels/booking-flow/utils/url"
import { PaymentCallbackStatusEnum } from "@scandic-hotels/common/constants/booking"
import { trackEvent } from "@scandic-hotels/common/tracking/base"
import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
import { PaymentCallbackStatusEnum } from "@/constants/booking"
import { detailsStorageName } from "@/stores/enter-details"
import { trackPaymentEvent } from "@/utils/tracking"

View File

@@ -3,11 +3,10 @@
import { useRouter } from "next/navigation"
import { useEffect } from "react"
import { MEMBERSHIP_FAILED_ERROR } from "@scandic-hotels/common/constants/booking"
import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
import { BookingStatusEnum } from "@scandic-hotels/trpc/enums/bookingStatus"
import { MEMBERSHIP_FAILED_ERROR } from "@/constants/booking"
import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus"
import TimeoutSpinner from "./TimeoutSpinner"

View File

@@ -9,6 +9,7 @@ import {
} from "react-aria-components"
import { useIntl } from "react-intl"
import { getFeatureDescription } from "@scandic-hotels/booking-flow/utils/getRoomFeatureDescription"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import Body from "@scandic-hotels/design-system/Body"
import Caption from "@scandic-hotels/design-system/Caption"
@@ -17,8 +18,6 @@ import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
import Subtitle from "@scandic-hotels/design-system/Subtitle"
import { getFeatureDescription } from "@/components/HotelReservation/utils/getRoomFeatureDescription"
import styles from "./priceChangeSummary.module.css"
import type { RoomState } from "@/types/stores/enter-details"

View File

@@ -1,8 +1,8 @@
"use client"
import { useEnterDetailsStore } from "@/stores/enter-details"
import { SidePanel } from "@scandic-hotels/booking-flow/components/SidePanel"
import SidePanel from "@/components/HotelReservation/SidePanel"
import { useEnterDetailsStore } from "@/stores/enter-details"
import SummaryUI from "./UI"

View File

@@ -1,6 +1,7 @@
import { cx } from "class-variance-authority"
import { useIntl } from "react-intl"
import { getFeatureDescription } from "@scandic-hotels/booking-flow/utils/getRoomFeatureDescription"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import { Button } from "@scandic-hotels/design-system/Button"
@@ -10,8 +11,6 @@ import Modal from "@scandic-hotels/design-system/Modal"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { ChildBedMapEnum } from "@scandic-hotels/trpc/enums/childBedMapEnum"
import { getFeatureDescription } from "@/components/HotelReservation/utils/getRoomFeatureDescription"
import Breakfast from "./Breakfast"
import styles from "./room.module.css"

View File

@@ -5,7 +5,7 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import styles from "./promo.module.css"
import type { PromoProps } from "@/types/components/hotelReservation/bookingConfirmation/promo"
import type { PromoProps } from "@scandic-hotels/booking-flow/types/components/promo/promoProps"
export default function Promo({
buttonText,

View File

@@ -1,12 +1,12 @@
"use client"
import { useIntl } from "react-intl"
import { CancellationRuleEnum } from "@scandic-hotels/common/constants/booking"
import { preliminaryReceipt } from "@scandic-hotels/common/constants/routes/myStay"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Link from "@scandic-hotels/design-system/Link"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { CancellationRuleEnum } from "@/constants/booking"
import { useMyStayStore } from "@/stores/my-stay"
import useLang from "@/hooks/useLang"

View File

@@ -2,6 +2,7 @@
import { useIntl } from "react-intl"
import { IconForFeatureCode } from "@scandic-hotels/booking-flow/utils/SelectRate"
import { CancellationRuleEnum } from "@scandic-hotels/common/constants/booking"
import { changeOrCancelDateFormat } from "@scandic-hotels/common/constants/dateFormats"
import { RateEnum } from "@scandic-hotels/common/constants/rate"
import { dt } from "@scandic-hotels/common/dt"
@@ -15,8 +16,6 @@ import Modal from "@scandic-hotels/design-system/Modal"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
import { CancellationRuleEnum } from "@/constants/booking"
import useRateTitles from "@/hooks/booking/useRateTitles"
import useLang from "@/hooks/useLang"

View File

@@ -1,11 +1,10 @@
import { useIntl } from "react-intl"
import { getFeatureDescription } from "@scandic-hotels/booking-flow/utils/getRoomFeatureDescription"
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
import { useMyStayStore } from "@/stores/my-stay"
import { getFeatureDescription } from "@/components/HotelReservation/utils/getRoomFeatureDescription"
import Row from "./Row"
export default function Packages() {

View File

@@ -1,12 +1,12 @@
import { useIntl } from "react-intl"
import { CancellationRuleEnum } from "@scandic-hotels/common/constants/booking"
import { RateEnum } from "@scandic-hotels/common/constants/rate"
import { IconButton } from "@scandic-hotels/design-system/IconButton"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Modal from "@scandic-hotels/design-system/Modal"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { CancellationRuleEnum } from "@/constants/booking"
import { useMyStayStore } from "@/stores/my-stay"
import useRateTitles from "@/hooks/booking/useRateTitles"

View File

@@ -3,11 +3,10 @@
import { useRouter } from "next/navigation"
import { useEffect } from "react"
import { PaymentCallbackStatusEnum } from "@scandic-hotels/common/constants/booking"
import { trackEvent } from "@scandic-hotels/common/tracking/base"
import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
import { PaymentCallbackStatusEnum } from "@/constants/booking"
import {
clearGlaSessionStorage,
readGlaFromSessionStorage,

View File

@@ -1,7 +1,6 @@
import { CancellationRuleEnum } from "@scandic-hotels/common/constants/booking"
import { ChildBedTypeEnum } from "@scandic-hotels/trpc/enums/childBedTypeEnum"
import { CancellationRuleEnum } from "@/constants/booking"
export function formatChildBedPreferences({
childrenAges,
childBedPreferences,

View File

@@ -1,11 +1,10 @@
import { CancellationRuleEnum } from "@scandic-hotels/common/constants/booking"
import { dt } from "@scandic-hotels/common/dt"
import { BookingStatusEnum } from "@scandic-hotels/trpc/enums/bookingStatus"
import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast"
import { PackageTypeEnum } from "@scandic-hotels/trpc/enums/packages"
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
import { CancellationRuleEnum } from "@/constants/booking"
import { convertToChildType } from "../../utils/convertToChildType"
import { getPriceType } from "../../utils/getPriceType"
import { formatChildBedPreferences } from "../utils"

View File

@@ -1,19 +0,0 @@
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,62 +0,0 @@
.sidePanel,
.hider,
.shadow {
display: none;
}
@media screen and (min-width: 1367px) {
.sidePanel {
display: grid;
grid-template-rows: auto auto 1fr;
}
.summary {
margin-top: calc(0px - var(--Spacing-x9));
}
.hider {
display: block;
position: sticky;
}
.receipt .hider {
background-color: transparent;
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-lg) var(--Corner-radius-lg) 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;
}
.shadow {
background-color: var(--Main-Grey-White);
border-color: var(--Primary-Light-On-Surface-Divider-subtle);
border-left-width: 1px;
border-right-width: 1px;
border-style: solid;
border-bottom: none;
border-top: none;
display: block;
}
}

View File

@@ -1,15 +0,0 @@
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",
},
})

View File

@@ -1,23 +0,0 @@
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
import type { IntlShape } from "react-intl"
export function getFeatureDescription(
code: string,
description: string,
intl: IntlShape
): string {
const roomFeatureDescriptions: Record<string, string> = {
[RoomPackageCodeEnum.ACCESSIBILITY_ROOM]: intl.formatMessage({
defaultMessage: "Accessible room",
}),
[RoomPackageCodeEnum.ALLERGY_ROOM]: intl.formatMessage({
defaultMessage: "Allergy-friendly room",
}),
[RoomPackageCodeEnum.PET_ROOM]: intl.formatMessage({
defaultMessage: "Pet-friendly room",
}),
}
return roomFeatureDescriptions[code] ?? description
}

View File

@@ -1,5 +1,6 @@
import { useIntl } from "react-intl"
import { getFeatureDescription } from "@scandic-hotels/booking-flow/utils/getRoomFeatureDescription"
import { sumPackages } from "@scandic-hotels/booking-flow/utils/SelectRate"
import { changeOrCancelDateFormat } from "@scandic-hotels/common/constants/dateFormats"
import { dt } from "@scandic-hotels/common/dt"
@@ -17,7 +18,6 @@ import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
import GuestDetails from "@/components/HotelReservation/MyStay/GuestDetails"
import PriceType from "@/components/HotelReservation/MyStay/PriceType"
import { hasModifiableRate } from "@/components/HotelReservation/MyStay/utils"
import { getFeatureDescription } from "@/components/HotelReservation/utils/getRoomFeatureDescription"
import useLang from "@/hooks/useLang"
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"

View File

@@ -1,15 +0,0 @@
export const SEARCHTYPE = "searchtype"
export const MEMBERSHIP_FAILED_ERROR = "MembershipFailedError"
export enum CancellationRuleEnum {
CancellableBefore6PM = "CancellableBefore6PM",
NonCancellable = "NonCancellable",
Changeable = "Changeable",
}
export enum PaymentCallbackStatusEnum {
Success = "success",
Error = "error",
Cancel = "cancel",
}

View File

@@ -1,11 +1,11 @@
import { type NextMiddleware, NextResponse } from "next/server"
import { SEARCHTYPE } from "@scandic-hotels/common/constants/booking"
import { login } from "@scandic-hotels/common/constants/routes/handleAuth"
import { findLang } from "@scandic-hotels/common/utils/languages"
import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
import { isValidSession } from "@scandic-hotels/trpc/utils/session"
import { SEARCHTYPE } from "@/constants/booking"
import { getPublicNextURL } from "@/server/utils"
import { auth } from "@/auth"

View File

@@ -1,5 +0,0 @@
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
export interface BookingConfirmationHotelDetailsProps {
hotel: BookingConfirmation["hotel"]
}

View File

@@ -1,20 +0,0 @@
export interface PromoProps {
buttonText: string
href: string
text: string
title: string
image?: {
imageSizes: {
large: string
medium: string
small: string
tiny: string
}
metaData: {
altText: string
altText_En: string
copyRight: string
title: string
}
}
}

View File

@@ -1,3 +0,0 @@
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
export interface PromosProps extends Pick<BookingConfirmation, "booking"> {}

View File

@@ -1,7 +0,0 @@
import type { Room } from "@scandic-hotels/booking-flow/types/stores/booking-confirmation"
export interface BookingConfirmationReceiptRoomProps {
room: Room
roomNumber: number
roomCount: number
}

View File

@@ -1,11 +0,0 @@
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { Room } from "@scandic-hotels/trpc/types/hotel"
export interface BookingConfirmationRoomsProps
extends Pick<BookingConfirmation, "booking"> {
mainRoom: Room & {
bedType: Room["roomTypes"][number]
}
checkInTime: string
checkOutTime: string
}

View File

@@ -1,10 +0,0 @@
export interface LinkedReservationProps {
checkInTime: string
checkOutTime: string
refId: string
roomIndex: number
}
export interface RetryProps {
handleRefetch: () => void
}

View File

@@ -1,9 +0,0 @@
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
export interface RoomProps {
booking: BookingConfirmation["booking"]
checkInTime: string
checkOutTime: string
img?: NonNullable<BookingConfirmation["room"]>["images"][number]
roomName: NonNullable<BookingConfirmation["room"]>["name"]
}

View File

@@ -1,6 +0,0 @@
import type { VariantProps } from "class-variance-authority"
import type { sidePanelVariants } from "@/components/HotelReservation/SidePanel/variants"
export interface SidePanelProps
extends VariantProps<typeof sidePanelVariants> {}