Files
web/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/Room/index.tsx
Anton Gunnarsson 002d093af4 Merged in feat/sw-2863-move-contentstack-router-to-trpc-package (pull request #2389)
feat(SW-2863): Move contentstack router to trpc package

* Add exports to packages and lint rule to prevent relative imports

* Add env to trpc package

* Add eslint to trpc package

* Apply lint rules

* Use direct imports from trpc package

* Add lint-staged config to trpc

* Move lang enum to common

* Restructure trpc package folder structure

* WIP first step

* update internal imports in trpc

* Fix most errors in scandic-web

Just 100 left...

* Move Props type out of trpc

* Fix CategorizedFilters types

* Move more schemas in hotel router

* Fix deps

* fix getNonContentstackUrls

* Fix import error

* Fix entry error handling

* Fix generateMetadata metrics

* Fix alertType enum

* Fix duplicated types

* lint:fix

* Merge branch 'master' into feat/sw-2863-move-contentstack-router-to-trpc-package

* Fix broken imports

* Merge branch 'master' into feat/sw-2863-move-contentstack-router-to-trpc-package


Approved-by: Linus Flood
2025-06-26 07:53:01 +00:00

307 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { cx } from "class-variance-authority"
import { useIntl } from "react-intl"
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 { Typography } from "@scandic-hotels/design-system/Typography"
import { ChildBedMapEnum } from "@scandic-hotels/trpc/enums/childBedMapEnum"
import Modal from "@/components/Modal"
import { formatPrice } from "@/utils/numberFormatting"
import { getMemberPrice, getPublicPrice } from "../utils"
import Breakfast from "./Breakfast"
import styles from "./room.module.css"
import type { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import type { Room as RoomType } from "@/types/stores/enter-details"
interface RoomProps {
room: RoomType
roomNumber: number
roomCount: number
isMember: boolean
isSpecialRate: boolean
nightsCount: number
defaultCurrency: CurrencyEnum
}
export default function Room({
room,
roomNumber,
roomCount,
isMember,
isSpecialRate,
nightsCount,
defaultCurrency,
}: RoomProps) {
const intl = useIntl()
const adults = room.adults
const childrenInRoom = room.childrenInRoom
const childrenBeds = childrenInRoom?.reduce(
(acc, value) => {
const bedType = Number(value.bed)
if (bedType === ChildBedMapEnum.IN_ADULTS_BED) {
return acc
}
const count = acc.get(bedType) ?? 0
acc.set(bedType, count + 1)
return acc
},
new Map<ChildBedMapEnum, number>([
[ChildBedMapEnum.IN_CRIB, 0],
[ChildBedMapEnum.IN_EXTRA_BED, 0],
])
)
const childBedCrib = childrenBeds?.get(ChildBedMapEnum.IN_CRIB)
const childBedExtraBed = childrenBeds?.get(ChildBedMapEnum.IN_EXTRA_BED)
const memberPrice = getMemberPrice(room.roomRate)
const publicPrice = getPublicPrice(room.roomRate)
const isFirstRoomMember = roomNumber === 1 && isMember
const isOrWillBecomeMember = !!(
room.guest.join ||
room.guest.membershipNo ||
isFirstRoomMember
)
const showMemberPrice = !!(isOrWillBecomeMember && memberPrice)
const showDiscounted = isSpecialRate || showMemberPrice
const adultsMsg = intl.formatMessage(
{
defaultMessage: "{totalAdults, plural, one {# adult} other {# adults}}",
},
{ totalAdults: adults }
)
const guestsParts = [adultsMsg]
if (childrenInRoom?.length) {
const childrenMsg = intl.formatMessage(
{
defaultMessage:
"{totalChildren, plural, one {# child} other {# children}}",
},
{ totalChildren: childrenInRoom.length }
)
guestsParts.push(childrenMsg)
}
let rateDetails = room.rateDetails
if (room.memberRateDetails) {
if (isMember || room.guest.join) {
rateDetails = room.memberRateDetails
}
}
const guests = guestsParts.join(", ")
const zeroPrice = formatPrice(intl, 0, defaultCurrency)
return (
<>
<div className={styles.room} data-testid={`summary-room-${roomNumber}`}>
<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.roomType}</p>
</Typography>
<Typography variant="Body/Supporting text (caption)/smRegular">
<div className={styles.additionalInformation}>
<p>{guests}</p>
<p>{room.cancellationText}</p>
</div>
</Typography>
</div>
<Typography variant="Body/Paragraph/mdRegular">
<div className={styles.prices}>
<p
className={cx(styles.price, {
[styles.discounted]: showDiscounted,
})}
>
{showMemberPrice
? formatPrice(
intl,
memberPrice.amount,
memberPrice.currency
)
: formatPrice(
intl,
room.roomPrice.perStay.local.price,
room.roomPrice.perStay.local.currency,
room.roomPrice.perStay.local.additionalPrice,
room.roomPrice.perStay.local.additionalPriceCurrency
)}
</p>
{showDiscounted && publicPrice ? (
<s className={styles.strikeThroughRate}>
{formatPrice(
intl,
publicPrice.amount,
publicPrice.currency
)}
</s>
) : null}
</div>
</Typography>
</div>
{rateDetails?.length ? (
<div className={styles.ctaWrapper}>
<Modal
trigger={
<Button
className={styles.termsButton}
variant="Text"
typography="Body/Supporting text (caption)/smBold"
wrapping={false}
>
{intl.formatMessage({
defaultMessage: "Rate details",
})}
<MaterialIcon
icon="chevron_right"
size={20}
color="CurrentColor"
/>
</Button>
}
title={room.cancellationText}
>
<div className={styles.terms}>
{rateDetails.map((info) => (
<Typography key={info} variant="Body/Paragraph/mdRegular">
<p className={styles.termsText}>
<MaterialIcon
icon="check"
color="Icon/Feedback/Success"
size={20}
className={styles.termsIcon}
/>
{info}
</p>
</Typography>
))}
</div>
</Modal>
</div>
) : null}
</div>
{room.roomFeatures
? room.roomFeatures.map((feature) => (
<Typography key={feature.code} variant="Body/Paragraph/mdRegular">
<div className={styles.entry}>
<p>{feature.description}</p>
<div className={styles.prices}>
<span className={styles.price}>
{formatPrice(
intl,
feature.localPrice.price,
feature.localPrice.currency
)}
</span>
</div>
</div>
</Typography>
))
: null}
{room.bedType ? (
<Typography variant="Body/Paragraph/mdRegular">
<div className={styles.entry}>
<p>{room.bedType.description}</p>
<div className={styles.prices}>
<span className={styles.price}>{zeroPrice}</span>
</div>
</div>
</Typography>
) : null}
{childBedCrib ? (
<Typography variant="Body/Paragraph/mdRegular">
<div className={styles.entry}>
<div>
<p>
{intl.formatMessage(
{
defaultMessage: "Crib (child) × {count}",
},
{ count: childBedCrib }
)}
</p>
<Typography variant="Body/Supporting text (caption)/smRegular">
<p>
{intl.formatMessage({
defaultMessage: "Subject to availability",
})}
</p>
</Typography>
</div>
<div className={styles.prices}>
<span className={styles.price}>
{formatPrice(intl, 0, room.roomPrice.perStay.local.currency)}
</span>
</div>
</div>
</Typography>
) : null}
{childBedExtraBed ? (
<Typography variant="Body/Paragraph/mdRegular">
<div className={styles.entry}>
<p>
{intl.formatMessage(
{
defaultMessage: "Extra bed (child) × {count}",
},
{
count: childBedExtraBed,
}
)}
</p>
<div className={styles.prices}>
<span className={styles.price}>
{formatPrice(intl, 0, room.roomPrice.perStay.local.currency)}
</span>
</div>
</div>
</Typography>
) : null}
<Breakfast
adults={room.adults}
breakfast={room.breakfast}
breakfastIncluded={room.breakfastIncluded}
guests={guests}
nights={nightsCount}
/>
</div>
<Divider color="Border/Divider/Subtle" />
</>
)
}