Migrate to a monorepo setup - step 1 * Move web to subfolder /apps/scandic-web * Yarn + transitive deps - Move to yarn - design-system package removed for now since yarn doesn't support the parameter for token (ie project currently broken) - Add missing transitive dependencies as Yarn otherwise prevents these imports - VS Code doesn't pick up TS path aliases unless you open /apps/scandic-web instead of root (will be fixed with monorepo) * Pin framer-motion to temporarily fix typing issue https://github.com/adobe/react-spectrum/issues/7494 * Pin zod to avoid typ error There seems to have been a breaking change in the types returned by zod where error is now returned as undefined instead of missing in the type. We should just handle this but to avoid merge conflicts just pin the dependency for now. * Pin react-intl version Pin version of react-intl to avoid tiny type issue where formatMessage does not accept a generic any more. This will be fixed in a future commit, but to avoid merge conflicts just pin for now. * Pin typescript version Temporarily pin version as newer versions as stricter and results in a type error. Will be fixed in future commit after merge. * Setup workspaces * Add design-system as a monorepo package * Remove unused env var DESIGN_SYSTEM_ACCESS_TOKEN * Fix husky for monorepo setup * Update netlify.toml * Add lint script to root package.json * Add stub readme * Fix react-intl formatMessage types * Test netlify.toml in root * Remove root toml * Update netlify.toml publish path * Remove package-lock.json * Update build for branch/preview builds Approved-by: Linus Flood
299 lines
11 KiB
TypeScript
299 lines
11 KiB
TypeScript
"use client"
|
|
|
|
import { useIntl } from "react-intl"
|
|
|
|
import { dt } from "@/lib/dt"
|
|
|
|
import { getIconForFeatureCode } from "@/components/HotelReservation/utils"
|
|
import {
|
|
BedDoubleIcon,
|
|
CoffeeIcon,
|
|
ContractIcon,
|
|
DoorOpenIcon,
|
|
PersonIcon,
|
|
} from "@/components/Icons"
|
|
import RocketLaunch from "@/components/Icons/Refresh"
|
|
import Image from "@/components/Image"
|
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
|
import useLang from "@/hooks/useLang"
|
|
import { formatPrice } from "@/utils/numberFormatting"
|
|
|
|
import ToggleSidePeek from "../../EnterDetails/SelectedRoom/ToggleSidePeek"
|
|
import PriceDetailsModal from "../../PriceDetailsModal"
|
|
import GuestDetails from "./GuestDetails"
|
|
|
|
import styles from "./room.module.css"
|
|
|
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
|
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
|
import type { Hotel, Room } from "@/types/hotel"
|
|
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
|
import type { User } from "@/types/user"
|
|
|
|
interface RoomProps {
|
|
booking: BookingConfirmation["booking"]
|
|
room:
|
|
| (Room & {
|
|
bedType: Room["roomTypes"][number]
|
|
})
|
|
| null
|
|
hotel: Hotel
|
|
user: User | null
|
|
}
|
|
|
|
function hasBreakfastPackage(
|
|
packages: BookingConfirmation["booking"]["packages"]
|
|
) {
|
|
return packages.some(
|
|
(p) =>
|
|
p.code === BreakfastPackageEnum.REGULAR_BREAKFAST ||
|
|
p.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST ||
|
|
p.code === BreakfastPackageEnum.SPECIAL_PACKAGE_BREAKFAST
|
|
)
|
|
}
|
|
|
|
function RoomHeader({
|
|
room,
|
|
hotel,
|
|
}: {
|
|
room: RoomProps["room"]
|
|
hotel: Hotel
|
|
}) {
|
|
if (!room) return null
|
|
|
|
return (
|
|
<div className={styles.roomHeader}>
|
|
<Subtitle
|
|
textTransform="uppercase"
|
|
color="burgundy"
|
|
className={styles.roomName}
|
|
>
|
|
{room.name}
|
|
</Subtitle>
|
|
<ToggleSidePeek
|
|
hotelId={hotel.operaId}
|
|
roomTypeCode={room.roomTypes[0].code}
|
|
intent="text"
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function Room({ booking, room, hotel, user }: RoomProps) {
|
|
const intl = useIntl()
|
|
const lang = useLang()
|
|
|
|
if (!room) return null
|
|
|
|
const fromDate = dt(booking.checkInDate).locale(lang)
|
|
|
|
return (
|
|
<div className={styles.roomContainer}>
|
|
<article className={styles.room}>
|
|
<RoomHeader room={room} hotel={hotel} />
|
|
<div className={styles.booking}>
|
|
<div className={styles.chipContainer}>
|
|
{booking.packages
|
|
.filter((item) =>
|
|
Object.values(RoomPackageCodeEnum).includes(
|
|
item.code as RoomPackageCodeEnum
|
|
)
|
|
)
|
|
.map((item) => {
|
|
const Icon = getIconForFeatureCode(
|
|
item.code as RoomPackageCodeEnum
|
|
)
|
|
return (
|
|
<span className={styles.chip} key={item.code}>
|
|
<Icon width={16} height={16} color="burgundy" />
|
|
</span>
|
|
)
|
|
})}
|
|
</div>
|
|
<div className={styles.images}>
|
|
{room.images.slice(0, 2).map((image) => (
|
|
<Image
|
|
key={image.imageSizes.large}
|
|
src={image.imageSizes.large}
|
|
className={styles.image}
|
|
alt={room?.name ?? hotel.name}
|
|
width={700}
|
|
height={450}
|
|
/>
|
|
))}
|
|
</div>
|
|
<div className={styles.roomDetails}>
|
|
<div className={styles.bookingDetails}>
|
|
<div className={styles.row}>
|
|
<span className={styles.rowTitle}>
|
|
<ContractIcon color="grey80" width={20} height={20} />
|
|
<Body textTransform="bold" color="uiTextHighContrast">
|
|
{intl.formatMessage({ id: "Booking policy" })}
|
|
</Body>
|
|
</span>
|
|
<div className={styles.rowContent}>
|
|
<Body color="uiTextHighContrast">
|
|
{booking.rateDefinition.cancellationText}
|
|
</Body>
|
|
</div>
|
|
</div>
|
|
<div className={styles.row}>
|
|
<span className={styles.rowTitle}>
|
|
<RocketLaunch color="grey80" width={20} height={20} />
|
|
<Body textTransform="bold" color="uiTextHighContrast">
|
|
{intl.formatMessage({ id: "Rebooking" })}
|
|
</Body>
|
|
</span>
|
|
<div className={styles.rowContent}>
|
|
<Body color="uiTextHighContrast">
|
|
{intl.formatMessage(
|
|
{ id: "Until {time}, {date}" },
|
|
{ time: "18:00", date: fromDate.format("dddd D MMM") }
|
|
)}
|
|
</Body>
|
|
</div>
|
|
</div>
|
|
{booking.packages.some((item) =>
|
|
Object.values(RoomPackageCodeEnum).includes(
|
|
item.code as RoomPackageCodeEnum
|
|
)
|
|
) && (
|
|
<div className={styles.row}>
|
|
<span className={styles.rowTitle}>
|
|
<DoorOpenIcon color="grey80" width={20} height={20} />
|
|
<Body textTransform="bold" color="uiTextHighContrast">
|
|
{intl.formatMessage({ id: "Room type" })}
|
|
</Body>
|
|
</span>
|
|
<div className={styles.rowContent}>
|
|
<Body color="uiTextHighContrast">
|
|
{booking.packages
|
|
.filter((item) =>
|
|
Object.values(RoomPackageCodeEnum).includes(
|
|
item.code as RoomPackageCodeEnum
|
|
)
|
|
)
|
|
.map((item) => item.description)
|
|
.join(", ")}
|
|
</Body>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div className={styles.row}>
|
|
<span className={styles.rowTitle}>
|
|
<PersonIcon color="grey80" width={20} height={20} />
|
|
<Body textTransform="bold" color="uiTextHighContrast">
|
|
{intl.formatMessage({ id: "Guests" })}
|
|
</Body>
|
|
</span>
|
|
<div className={styles.rowContent}>
|
|
<Body color="uiTextHighContrast">
|
|
{booking.childrenAges.length > 0
|
|
? intl.formatMessage(
|
|
{ id: "{adults} adults, {children} children" },
|
|
{
|
|
adults: booking.adults,
|
|
children: booking.childrenAges.length,
|
|
}
|
|
)
|
|
: intl.formatMessage(
|
|
{ id: "{adults} adults" },
|
|
{
|
|
adults: booking.adults,
|
|
}
|
|
)}
|
|
</Body>
|
|
</div>
|
|
</div>
|
|
<div className={styles.row}>
|
|
<span className={styles.rowTitle}>
|
|
<CoffeeIcon color="grey80" width={20} height={20} />
|
|
<Body textTransform="bold" color="uiTextHighContrast">
|
|
{intl.formatMessage({ id: "Breakfast" })}
|
|
</Body>
|
|
</span>
|
|
<div className={styles.rowContent}>
|
|
<Body color="uiTextHighContrast">
|
|
{hasBreakfastPackage(booking.packages)
|
|
? intl.formatMessage({ id: "Included" })
|
|
: intl.formatMessage({ id: "Not included" })}
|
|
</Body>
|
|
</div>
|
|
</div>
|
|
<div className={styles.row}>
|
|
<span className={styles.rowTitle}>
|
|
<BedDoubleIcon color="grey80" width={20} height={20} />
|
|
<Body textTransform="bold" color="uiTextHighContrast">
|
|
{intl.formatMessage({ id: "Bed preference" })}
|
|
</Body>
|
|
</span>
|
|
<div className={styles.rowContent}>
|
|
<Body color="uiTextHighContrast">
|
|
{room.bedType.mainBed.description}
|
|
{room.bedType.mainBed.widthRange.min ===
|
|
room.bedType.mainBed.widthRange.max
|
|
? ` (${room.bedType.mainBed.widthRange.min} ${intl.formatMessage({ id: "cm" })})`
|
|
: ` (${room.bedType.mainBed.widthRange.min} - ${room.bedType.mainBed.widthRange.max} ${intl.formatMessage({ id: "cm" })})`}
|
|
</Body>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<GuestDetails user={user} booking={booking} isMobile={false} />
|
|
</div>
|
|
<div className={styles.bookingInformation}>
|
|
<div className={styles.bookingCode}></div>
|
|
<div className={styles.priceDetails}>
|
|
<div className={styles.price}>
|
|
<Body color="uiTextHighContrast">
|
|
{intl.formatMessage({ id: "Room total" })}
|
|
</Body>
|
|
<Body color="uiTextHighContrast" textTransform="bold">
|
|
{formatPrice(intl, booking.totalPrice, booking.currencyCode)}
|
|
</Body>
|
|
</div>
|
|
|
|
<PriceDetailsModal
|
|
fromDate={dt(booking.checkInDate).format("YYYY-MM-DD")}
|
|
toDate={dt(booking.checkOutDate).format("YYYY-MM-DD")}
|
|
rooms={[
|
|
{
|
|
adults: booking.adults,
|
|
childrenInRoom: undefined,
|
|
roomPrice: {
|
|
perNight: {
|
|
requested: undefined,
|
|
local: {
|
|
currency: booking.currencyCode,
|
|
price: booking.totalPrice,
|
|
},
|
|
},
|
|
perStay: {
|
|
requested: undefined,
|
|
local: {
|
|
currency: booking.currencyCode,
|
|
price: booking.totalPrice,
|
|
},
|
|
},
|
|
},
|
|
roomType: room.name,
|
|
},
|
|
]}
|
|
totalPrice={{
|
|
requested: undefined,
|
|
local: {
|
|
currency: booking.currencyCode,
|
|
price: booking.totalPrice,
|
|
},
|
|
}}
|
|
vat={booking.vatPercentage}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
<GuestDetails user={user} booking={booking} isMobile={true} />
|
|
</div>
|
|
)
|
|
}
|