448 lines
15 KiB
TypeScript
448 lines
15 KiB
TypeScript
"use client"
|
||
|
||
import { useIntl } from "react-intl"
|
||
|
||
import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon"
|
||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||
|
||
import { dt } from "@/lib/dt"
|
||
import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore"
|
||
|
||
import { IconForFeatureCode } from "@/components/HotelReservation/utils"
|
||
import Image from "@/components/Image"
|
||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||
import IconChip from "@/components/TempDesignSystem/IconChip"
|
||
import useLang from "@/hooks/useLang"
|
||
import { formatPrice } from "@/utils/numberFormatting"
|
||
|
||
import GuestDetails from "../GuestDetails"
|
||
import PriceDetails from "../PriceDetails"
|
||
import { hasModifiableRate } from "../utils"
|
||
import PriceType from "./PriceType"
|
||
import ToggleSidePeek from "./ToggleSidePeek"
|
||
|
||
import styles from "./room.module.css"
|
||
|
||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||
import type { Hotel, Room } from "@/types/hotel"
|
||
import type { User } from "@/types/user"
|
||
|
||
interface RoomProps {
|
||
bedType: Room["roomTypes"][number]
|
||
image: Room["images"][number]
|
||
hotel: Hotel
|
||
user: User | null
|
||
}
|
||
|
||
export function SingleRoom({ bedType, image, hotel, user }: RoomProps) {
|
||
const intl = useIntl()
|
||
const lang = useLang()
|
||
|
||
const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom)
|
||
const updateBookedRoom = useMyStayRoomDetailsStore(
|
||
(state) => state.actions.updateBookedRoom
|
||
)
|
||
|
||
if (!bookedRoom.roomNumber) {
|
||
return (
|
||
<div className={styles.room}>
|
||
<SkeletonShimmer width={"200px"} height="30px" />
|
||
<SkeletonShimmer width="100%" height="750px" />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const fromDate = dt(bookedRoom.checkInDate).locale(lang)
|
||
|
||
const {
|
||
adults,
|
||
bookingCode,
|
||
breakfast,
|
||
cheques,
|
||
childrenAges,
|
||
confirmationNumber,
|
||
isCancelled,
|
||
packages,
|
||
priceType,
|
||
rateDefinition,
|
||
roomPoints,
|
||
totalPrice,
|
||
vouchers,
|
||
} = bookedRoom
|
||
|
||
const mainBedWidthValueMsg = intl.formatMessage(
|
||
{
|
||
defaultMessage: "{value} cm",
|
||
},
|
||
{
|
||
value: bedType.mainBed.widthRange.min,
|
||
}
|
||
)
|
||
|
||
const mainBedWidthRangeMsg = intl.formatMessage(
|
||
{
|
||
defaultMessage: "{min}–{max} cm",
|
||
},
|
||
{
|
||
min: bedType.mainBed.widthRange.min,
|
||
max: bedType.mainBed.widthRange.max,
|
||
}
|
||
)
|
||
|
||
const adultsMsg = intl.formatMessage(
|
||
{
|
||
defaultMessage: "{adults, plural, one {# adult} other {# adults}}",
|
||
},
|
||
{
|
||
adults,
|
||
}
|
||
)
|
||
|
||
const childrenMsg = intl.formatMessage(
|
||
{
|
||
defaultMessage: "{children, plural, one {# child} other {# children}}",
|
||
},
|
||
{
|
||
children: childrenAges.length,
|
||
}
|
||
)
|
||
|
||
const adultsOnlyMsg = adultsMsg
|
||
const adultsAndChildrenMsg = [adultsMsg, childrenMsg].join(", ")
|
||
|
||
const hasPackages = packages?.some((item) =>
|
||
Object.values(RoomPackageCodeEnum).includes(
|
||
item.code as RoomPackageCodeEnum
|
||
)
|
||
)
|
||
|
||
const breakfastText = rateDefinition.breakfastIncluded
|
||
? intl.formatMessage({
|
||
defaultMessage: "Included",
|
||
})
|
||
: breakfast
|
||
? formatPrice(
|
||
intl,
|
||
breakfast.localPrice.totalPrice,
|
||
breakfast.localPrice.currency
|
||
)
|
||
: null
|
||
|
||
return (
|
||
<div>
|
||
<article className={styles.room}>
|
||
<Typography variant="Title/Subtitle/lg">
|
||
<p className={styles.roomName}>{bookedRoom.roomName}</p>
|
||
</Typography>
|
||
<div className={styles.roomHeader}>
|
||
<div className={styles.roomHeaderContent}>
|
||
<div className={styles.chip}>
|
||
<Typography variant="Tag/sm">
|
||
<span>
|
||
{intl.formatMessage(
|
||
{
|
||
defaultMessage: "Room {roomIndex}",
|
||
},
|
||
{
|
||
roomIndex: bookedRoom.roomNumber,
|
||
}
|
||
)}
|
||
</span>
|
||
</Typography>
|
||
</div>
|
||
<div className={styles.reference}>
|
||
<Typography variant="Body/Paragraph/mdBold">
|
||
<span>
|
||
{intl.formatMessage({
|
||
defaultMessage: "Reference",
|
||
})}
|
||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||
{":"}
|
||
</span>
|
||
</Typography>
|
||
<Typography variant="Body/Paragraph/mdRegular">
|
||
<span>{confirmationNumber}</span>
|
||
</Typography>
|
||
</div>
|
||
</div>
|
||
<div className={styles.sidePeek}>
|
||
<ToggleSidePeek
|
||
hotelId={hotel.operaId}
|
||
roomTypeCode={bookedRoom.roomTypeCode}
|
||
intent="text"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className={styles.booking}>
|
||
<div
|
||
className={`${styles.content} ${isCancelled ? styles.cancelled : ""}`}
|
||
>
|
||
{packages?.some((item) =>
|
||
Object.values(RoomPackageCodeEnum).includes(
|
||
item.code as RoomPackageCodeEnum
|
||
)
|
||
) && (
|
||
<div className={styles.packages}>
|
||
{packages
|
||
.filter((item) =>
|
||
Object.values(RoomPackageCodeEnum).includes(
|
||
item.code as RoomPackageCodeEnum
|
||
)
|
||
)
|
||
.map((item) => {
|
||
return (
|
||
<span className={styles.package} key={item.code}>
|
||
<IconForFeatureCode
|
||
featureCode={item.code}
|
||
size={16}
|
||
color="Icon/Interactive/Default"
|
||
/>
|
||
</span>
|
||
)
|
||
})}
|
||
</div>
|
||
)}
|
||
<div className={styles.imageContainer}>
|
||
<Image
|
||
key={image.imageSizes.small}
|
||
src={image.imageSizes.small}
|
||
className={styles.image}
|
||
alt={bookedRoom.roomName}
|
||
width={640}
|
||
height={960}
|
||
/>
|
||
</div>
|
||
<div className={styles.roomDetails}>
|
||
<div className={styles.bookingDetails}>
|
||
<div className={styles.row}>
|
||
<span className={styles.rowTitle}>
|
||
<MaterialIcon
|
||
icon="person"
|
||
color="Icon/Default"
|
||
size={20}
|
||
/>
|
||
<Typography variant="Body/Paragraph/mdBold">
|
||
<p>
|
||
{intl.formatMessage({
|
||
defaultMessage: "Guests",
|
||
})}
|
||
</p>
|
||
</Typography>
|
||
</span>
|
||
<div className={styles.rowContent}>
|
||
<Typography variant="Body/Paragraph/mdRegular">
|
||
<p color="uiTextHighContrast">
|
||
{childrenAges.length > 0
|
||
? adultsAndChildrenMsg
|
||
: adultsOnlyMsg}
|
||
</p>
|
||
</Typography>
|
||
</div>
|
||
</div>
|
||
<div className={styles.row}>
|
||
<span className={styles.rowTitle}>
|
||
<MaterialIcon
|
||
icon="contract"
|
||
color="Icon/Default"
|
||
size={20}
|
||
/>
|
||
<Typography variant="Body/Paragraph/mdBold">
|
||
<p>
|
||
{intl.formatMessage({
|
||
defaultMessage: "Terms",
|
||
})}
|
||
</p>
|
||
</Typography>
|
||
</span>
|
||
<div className={styles.rowContent}>
|
||
<Typography variant="Body/Paragraph/mdRegular">
|
||
<p color="uiTextHighContrast">
|
||
{rateDefinition.cancellationText}
|
||
</p>
|
||
</Typography>
|
||
</div>
|
||
</div>
|
||
{hasModifiableRate(rateDefinition.cancellationRule) && (
|
||
<div className={styles.row}>
|
||
<span className={styles.rowTitle}>
|
||
<MaterialIcon
|
||
icon="refresh"
|
||
color="Icon/Default"
|
||
size={20}
|
||
/>
|
||
<Typography variant="Body/Paragraph/mdBold">
|
||
<p>
|
||
{intl.formatMessage({
|
||
defaultMessage: "Modify By",
|
||
})}
|
||
</p>
|
||
</Typography>
|
||
</span>
|
||
<div className={styles.rowContent}>
|
||
<Typography variant="Body/Paragraph/mdRegular">
|
||
<p color="uiTextHighContrast">
|
||
{intl.formatMessage(
|
||
{
|
||
defaultMessage: "Until {time}, {date}",
|
||
},
|
||
{
|
||
time: "18:00",
|
||
date: fromDate.format("dddd D MMM"),
|
||
}
|
||
)}
|
||
</p>
|
||
</Typography>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{breakfastText !== null && (
|
||
<div className={styles.row}>
|
||
<span className={styles.rowTitle}>
|
||
<MaterialIcon
|
||
icon="coffee"
|
||
color="Icon/Default"
|
||
size={20}
|
||
/>
|
||
<Typography variant="Body/Paragraph/mdBold">
|
||
<p>
|
||
{intl.formatMessage({
|
||
defaultMessage: "Breakfast",
|
||
})}
|
||
</p>
|
||
</Typography>
|
||
</span>
|
||
<div className={styles.rowContent}>
|
||
<Typography variant="Body/Paragraph/mdRegular">
|
||
<p color="uiTextHighContrast">{breakfastText}</p>
|
||
</Typography>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{hasPackages && (
|
||
<div className={styles.row}>
|
||
<span className={styles.rowTitle}>
|
||
<MaterialIcon
|
||
icon="meeting_room"
|
||
color="Icon/Default"
|
||
size={20}
|
||
/>
|
||
<Typography variant="Body/Paragraph/mdBold">
|
||
<p>
|
||
{intl.formatMessage({
|
||
defaultMessage: "Room classification",
|
||
})}
|
||
</p>
|
||
</Typography>
|
||
</span>
|
||
<div className={styles.rowContent}>
|
||
<Typography variant="Body/Paragraph/mdRegular">
|
||
<p color="uiTextHighContrast">
|
||
{packages!
|
||
.filter((item) =>
|
||
Object.values(RoomPackageCodeEnum).includes(
|
||
item.code as RoomPackageCodeEnum
|
||
)
|
||
)
|
||
.map((item) => item.description)
|
||
.join(", ")}
|
||
</p>
|
||
</Typography>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className={styles.row}>
|
||
<span className={styles.rowTitle}>
|
||
<MaterialIcon icon="bed" color="Icon/Default" size={20} />
|
||
<Typography variant="Body/Paragraph/mdBold">
|
||
<p>
|
||
{intl.formatMessage({
|
||
defaultMessage: "Bed preference",
|
||
})}
|
||
</p>
|
||
</Typography>
|
||
</span>
|
||
<div className={styles.rowContent}>
|
||
<Typography variant="Body/Paragraph/mdRegular">
|
||
<p color="uiTextHighContrast">
|
||
{bedType.mainBed.description}
|
||
{bedType.mainBed.widthRange.min ===
|
||
bedType.mainBed.widthRange.max
|
||
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||
` (${mainBedWidthValueMsg})`
|
||
: // eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||
` (${mainBedWidthRangeMsg})`}
|
||
</p>
|
||
</Typography>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className={styles.guestDetailsDesktopWrapper}>
|
||
<GuestDetails
|
||
user={user}
|
||
booking={bookedRoom}
|
||
updateRoom={updateBookedRoom}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className={styles.bookingInformation}>
|
||
{bookingCode && (
|
||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||
<IconChip
|
||
color="blue"
|
||
icon={<DiscountIcon color="Icon/Feedback/Information" />}
|
||
>
|
||
{intl.formatMessage(
|
||
{
|
||
defaultMessage: "<strong>Booking code</strong>: {value}",
|
||
},
|
||
{
|
||
value: bookingCode,
|
||
strong: (text) => (
|
||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||
<strong>{text}</strong>
|
||
</Typography>
|
||
),
|
||
}
|
||
)}
|
||
</IconChip>
|
||
</Typography>
|
||
)}
|
||
<div className={styles.priceDetails}>
|
||
<div className={styles.price}>
|
||
<Typography variant="Body/Lead text">
|
||
<p color="uiTextHighContrast">
|
||
{intl.formatMessage({
|
||
defaultMessage: "Room total",
|
||
})}
|
||
</p>
|
||
</Typography>
|
||
<PriceType
|
||
cheques={cheques}
|
||
isCancelled={isCancelled}
|
||
priceType={priceType}
|
||
rateDefinition={rateDefinition}
|
||
roomPoints={roomPoints}
|
||
totalPrice={totalPrice}
|
||
vouchers={vouchers}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<PriceDetails />
|
||
<div className={styles.guestDetailsMobileWrapper}>
|
||
<GuestDetails
|
||
user={user}
|
||
booking={bookedRoom}
|
||
updateRoom={updateBookedRoom}
|
||
/>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
)
|
||
}
|