feat: refactor of my stay

This commit is contained in:
Simon Emanuelsson
2025-04-25 14:08:14 +02:00
committed by Simon.Emanuelsson
parent b5deb84b33
commit ec087a3d15
208 changed files with 5458 additions and 4569 deletions

View File

@@ -0,0 +1,45 @@
import SkeletonShimmer from "@/components/SkeletonShimmer"
import Divider from "@/components/TempDesignSystem/Divider"
import styles from "./multiRoom.module.css"
export default function MultiRoomSkeleton() {
return (
<div className={styles.multiRoom}>
<div className={styles.roomName}>
<SkeletonShimmer width="50%" height="30px" />
</div>
<div className={styles.roomHeader}>
<SkeletonShimmer width="100%" height="23px" />
</div>
<div className={styles.multiRoomCard}>
<div className={styles.imageContainer}>
<SkeletonShimmer width="100%" height="100%" />
</div>
<div className={styles.details}>
<div className={styles.row}>
<SkeletonShimmer width="60px" height="23px" />
<SkeletonShimmer width="110px" height="23px" />
</div>
<div className={styles.row}>
<SkeletonShimmer width="55px" height="23px" />
<SkeletonShimmer width="130px" height="23px" />
</div>
<div className={styles.row}>
<SkeletonShimmer width="75px" height="23px" />
<SkeletonShimmer width="155px" height="23px" />
</div>
<div className={styles.row}>
<SkeletonShimmer width="75px" height="23px" />
<SkeletonShimmer width="65px" height="23px" />
</div>
<Divider color="subtle" />
<div className={styles.row}>
<SkeletonShimmer width="150px" height="30px" />
<SkeletonShimmer width="150px" height="30px" />
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,43 @@
"use client"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import useSidePeekStore from "@/stores/sidepeek"
import Button from "@/components/TempDesignSystem/Button"
import styles from "./toggleSidePeek.module.css"
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
import type { ToggleSidePeekProps } from "@/types/components/hotelReservation/toggleSidePeekProps"
export default function ToggleSidePeek({
hotelId,
roomTypeCode,
user,
confirmationNumber,
}: ToggleSidePeekProps) {
const openSidePeek = useSidePeekStore((state) => state.openSidePeek)
return (
<Button
onClick={() =>
openSidePeek({
key: SidePeekEnum.bookedRoomDetails,
hotelId,
roomTypeCode,
user,
confirmationNumber,
})
}
size="small"
variant="icon"
intent="text"
wrapping
>
<div className={styles.iconContainer}>
<MaterialIcon icon="pan_zoom" color="CurrentColor" />
</div>
</Button>
)
}

View File

@@ -0,0 +1,299 @@
"use client"
import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { dt } from "@/lib/dt"
import { IconForFeatureCode } from "@/components/HotelReservation/utils"
import Image from "@/components/Image"
import Divider from "@/components/TempDesignSystem/Divider"
import IconChip from "@/components/TempDesignSystem/IconChip"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import PriceType from "../../PriceType"
import { hasModifiableRate } from "../../utils"
import ToggleSidePeek from "./ToggleSidePeek"
import styles from "./multiRoom.module.css"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { Room } from "@/types/stores/my-stay"
import type { SafeUser } from "@/types/user"
interface MultiRoomProps {
booking: Room
roomNr: number
user: SafeUser
}
export default function MultiRoom({ booking, roomNr, user }: MultiRoomProps) {
const intl = useIntl()
const lang = useLang()
const {
adults,
breakfast,
cancellationNumber,
checkInDate,
cheques,
childrenAges,
confirmationNumber,
currencyCode,
hotelId,
packages,
rateDefinition,
room,
roomName,
roomPoints,
isCancelled,
priceType,
roomTypeCode,
vouchers,
totalPrice,
} = booking
const fromDate = dt(checkInDate).locale(lang)
const adultsMsg = intl.formatMessage(
{
defaultMessage: "{adults, plural, one {# adult} other {# adults}}",
},
{
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 formattedTotalPrice = formatPrice(intl, totalPrice, currencyCode)
let breakfastPrice = intl.formatMessage({
defaultMessage: "No breakfast",
})
if (rateDefinition.breakfastIncluded) {
breakfastPrice = intl.formatMessage({
defaultMessage: "Included",
})
} else if (breakfast) {
breakfastPrice = formatPrice(
intl,
breakfast.localPrice.totalPrice,
breakfast.localPrice.currency
)
}
return (
<article className={styles.multiRoom}>
<Typography variant="Title/smRegular">
<h3 className={styles.roomName}>{roomName}</h3>
</Typography>
<div className={styles.roomHeader}>
{isCancelled ? (
<IconChip
color={"red"}
icon={
<MaterialIcon
icon="cancel"
size={20}
color="Icon/Feedback/Error"
/>
}
>
<Typography variant="Body/Supporting text (caption)/smBold">
<span>
{intl.formatMessage({
defaultMessage: "Cancelled",
})}
</span>
</Typography>
</IconChip>
) : (
<div className={styles.chip}>
<Typography variant="Tag/sm">
<span>
{intl.formatMessage(
{
defaultMessage: "Room {roomIndex}",
},
{
roomIndex: roomNr,
}
)}
</span>
</Typography>
</div>
)}
<div className={styles.reference}>
<Typography variant="Body/Supporting text (caption)/smBold">
{isCancelled ? (
<span>
{intl.formatMessage({
defaultMessage: "Cancellation no",
})}
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
{":"}
</span>
) : (
<span>
{intl.formatMessage({
defaultMessage: "Reference",
})}
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
{":"}
</span>
)}
</Typography>
<Typography variant="Body/Supporting text (caption)/smRegular">
{isCancelled ? (
<span className={styles.cancellationNumber}>
{cancellationNumber}
</span>
) : (
<span>{confirmationNumber}</span>
)}
</Typography>
</div>
<div className={styles.toggleSidePeek}>
<ToggleSidePeek
hotelId={hotelId}
roomTypeCode={roomTypeCode}
user={user}
confirmationNumber={confirmationNumber}
/>
</div>
</div>
<div
className={`${styles.multiRoomCard} ${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
src={room?.images[0]?.imageSizes.small ?? ""}
alt={roomName}
fill
/>
</div>
<div className={styles.details}>
<div className={styles.row}>
<Typography variant="Body/Paragraph/mdBold">
<p>
{intl.formatMessage({
defaultMessage: "Guests",
})}
</p>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
<p>
{childrenAges.length > 0 ? adultsAndChildrenMsg : adultsOnlyMsg}
</p>
</Typography>
</div>
{rateDefinition.cancellationText ? (
<div className={styles.row}>
<Typography variant="Body/Paragraph/mdBold">
<p>
{intl.formatMessage({
defaultMessage: "Terms",
})}
</p>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
<p>{rateDefinition.cancellationText}</p>
</Typography>
</div>
) : null}
{hasModifiableRate(rateDefinition.cancellationRule) && (
<div className={styles.row}>
<Typography variant="Body/Paragraph/mdBold">
<p>
{intl.formatMessage({
defaultMessage: "Modify By",
})}
</p>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<p color="uiTextHighContrast">
18:00, {fromDate.format("dddd D MMM")}
</p>
</Typography>
</div>
)}
{breakfastPrice !== null && (
<div className={styles.row}>
<Typography variant="Body/Paragraph/mdBold">
<p>
{intl.formatMessage({
defaultMessage: "Breakfast",
})}
</p>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
<p color="uiTextHighContrast">{breakfastPrice}</p>
</Typography>
</div>
)}
<Divider color="subtle" />
<div className={styles.row}>
<Typography variant="Body/Lead text">
<p>
{intl.formatMessage({
defaultMessage: "Room total",
})}
</p>
</Typography>
<PriceType
cheques={cheques}
formattedTotalPrice={formattedTotalPrice}
isCancelled={isCancelled}
priceType={priceType}
rateDefinition={rateDefinition}
roomPoints={roomPoints}
totalPrice={totalPrice}
vouchers={vouchers}
/>
</div>
</div>
</div>
</article>
)
}

View File

@@ -0,0 +1,94 @@
.multiRoom {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
padding: 0 var(--Spacing-x2);
}
.cancelled {
opacity: 0.5;
}
.cancellationNumber {
text-decoration: line-through;
}
.multiRoomCard {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
background-color: var(--Base-Background-Primary-Normal);
border-radius: var(--Corner-radius-Large);
border: 1px solid var(--Base-Border-Subtle);
overflow: hidden;
padding-bottom: var(--Spacing-x3);
position: relative;
}
.imageContainer {
width: 100%;
height: 342px;
position: relative;
}
.roomName {
color: var(--Scandic-Brand-Burgundy);
}
.roomHeader {
display: flex;
align-items: center;
gap: var(--Spacing-x-one-and-half);
}
.chip {
background-color: var(--Scandic-Peach-30);
color: var(--Scandic-Red-100);
border-radius: var(--Corner-radius-Small);
padding: var(--Spacing-x-half) var(--Spacing-x1);
height: fit-content;
}
.toggleSidePeek {
margin-left: auto;
}
.reference {
display: flex;
gap: var(--Spacing-x-half);
}
.details {
display: flex;
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2) 0;
gap: var(--Spacing-x2);
flex-direction: column;
}
.row {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.packages {
position: absolute;
top: 304px;
left: 10px;
display: flex;
flex-direction: row;
gap: var(--Spacing-x1);
z-index: 100;
}
.package {
background-color: var(--Main-Grey-White);
padding: var(--Spacing-x-half) var(--Spacing-x1);
border-radius: var(--Corner-radius-Small);
}
@media (min-width: 768px) {
.multiRoom {
padding: 0;
}
}

View File

@@ -0,0 +1,6 @@
.iconContainer {
display: flex;
border: 1px solid var(--Base-Border-Subtle);
border-radius: var(--Corner-radius-Small);
padding: var(--Spacing-x-half);
}

View File

@@ -0,0 +1,46 @@
"use client"
import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import useSidePeekStore from "@/stores/sidepeek"
import Button from "@/components/TempDesignSystem/Button"
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
import type { ToggleSidePeekProps } from "@/types/components/hotelReservation/toggleSidePeekProps"
export default function ToggleSidePeek({
hotelId,
roomTypeCode,
intent = "textInverted",
title,
}: ToggleSidePeekProps) {
const intl = useIntl()
const openSidePeek = useSidePeekStore((state) => state.openSidePeek)
return (
<Button
onClick={() =>
openSidePeek({
key: SidePeekEnum.roomDetails,
hotelId,
roomTypeCode,
})
}
theme="base"
size="small"
variant="icon"
intent={intent}
wrapping
>
{title
? title
: intl.formatMessage({
defaultMessage: "See room details",
})}
<MaterialIcon icon="chevron_right" size={14} color="CurrentColor" />
</Button>
)
}

View File

@@ -0,0 +1,462 @@
"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 { useMyStayStore } from "@/stores/my-stay"
import GuestDetails from "@/components/HotelReservation/MyStay/GuestDetails"
import PriceDetails from "@/components/HotelReservation/MyStay/PriceDetails"
import PriceType from "@/components/HotelReservation/MyStay/PriceType"
import { hasModifiableRate } from "@/components/HotelReservation/MyStay/utils"
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 ToggleSidePeek from "./ToggleSidePeek"
import styles from "./room.module.css"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { Room } from "@/types/hotel"
import type { SafeUser } from "@/types/user"
interface RoomProps {
bedType: Room["roomTypes"][number]
image: Room["images"][number]
user: SafeUser
}
export default function SingleRoom({ bedType, image, user }: RoomProps) {
const intl = useIntl()
const lang = useLang()
const {
adults,
bookingCode,
breakfast,
checkInDate,
cheques,
childrenAges,
confirmationNumber,
formattedTotalPrice,
hotel,
isCancelled,
packages,
priceType,
rateDefinition,
roomName,
roomNumber,
roomPoints,
roomTypeCode,
totalPrice,
vouchers,
} = useMyStayStore((state) => ({
adults: state.bookedRoom.adults,
bookingCode: state.bookedRoom.bookingCode,
breakfast: state.bookedRoom.breakfast,
checkInDate: state.bookedRoom.checkInDate,
cheques: state.bookedRoom.cheques,
childrenAges: state.bookedRoom.childrenAges,
confirmationNumber: state.bookedRoom.confirmationNumber,
formattedTotalPrice: state.totalPrice,
hotel: state.hotel,
isCancelled: state.bookedRoom.isCancelled,
packages: state.bookedRoom.packages,
priceType: state.bookedRoom.priceType,
rateDefinition: state.bookedRoom.rateDefinition,
roomName: state.bookedRoom.roomName,
roomNumber: state.bookedRoom.roomNumber,
roomPoints: state.bookedRoom.roomPoints,
roomTypeCode: state.bookedRoom.roomTypeCode,
totalPrice: state.bookedRoom.totalPrice,
vouchers: state.bookedRoom.vouchers,
}))
if (!roomNumber) {
return (
<div className={styles.room}>
<SkeletonShimmer width={"200px"} height="30px" />
<SkeletonShimmer width="100%" height="750px" />
</div>
)
}
const fromDate = dt(checkInDate).locale(lang)
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
)
)
let breakfastPrice = null
if (rateDefinition.breakfastIncluded) {
breakfastPrice = intl.formatMessage({
defaultMessage: "Included",
})
} else if (breakfast) {
breakfastPrice = formatPrice(
intl,
breakfast.localPrice.totalPrice,
breakfast.localPrice.currency
)
}
return (
<div>
<article className={styles.room}>
<Typography variant="Title/Subtitle/lg">
<p className={styles.roomName}>{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: 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={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={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>
{rateDefinition.cancellationText ? (
<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>
) : null}
{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>
)}
{breakfastPrice !== 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">{breakfastPrice}</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} />
</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}
formattedTotalPrice={formattedTotalPrice}
isCancelled={isCancelled}
rateDefinition={rateDefinition}
priceType={priceType}
roomPoints={roomPoints}
totalPrice={totalPrice}
vouchers={vouchers}
/>
</div>
</div>
</div>
</div>
<PriceDetails />
<div className={styles.guestDetailsMobileWrapper}>
<GuestDetails user={user} />
</div>
</article>
</div>
)
}

View File

@@ -0,0 +1,277 @@
.room {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
background-color: var(--Base-Background-Primary-Normal);
padding: var(--Spacing-x3) 0;
}
.roomName {
color: var(--Scandic-Brand-Burgundy);
padding: 0 var(--Spacing-x2);
}
.roomHeader {
display: flex;
gap: var(--Spacing-x-one-and-half);
padding: 0 var(--Spacing-x2);
}
.roomHeaderContent {
display: flex;
gap: var(--Spacing-x-one-and-half);
}
.sidePeek {
display: none;
}
.booking {
display: flex;
flex-direction: column;
overflow: hidden;
}
.content {
display: grid;
gap: var(--Spacing-x2);
position: relative;
width: 100%;
margin: 0 auto;
grid-template-columns: 1fr;
}
.cancelled {
opacity: 0.5;
}
.chip {
background-color: var(--Scandic-Peach-30);
color: var(--Scandic-Red-100);
border-radius: var(--Corner-radius-Small);
padding: var(--Spacing-x-half) var(--Spacing-x1);
}
.reference {
display: flex;
gap: var(--Spacing-x-half);
}
.packages {
position: absolute;
top: 180px;
left: 15px;
display: flex;
flex-direction: row;
gap: var(--Spacing-x1);
z-index: 1;
}
.package {
background-color: var(--Main-Grey-White);
padding: var(--Spacing-x-half) var(--Spacing-x1);
border-radius: var(--Corner-radius-Small);
}
.imageContainer {
height: 220px;
overflow: hidden;
}
.image {
width: 100%;
height: 220px;
aspect-ratio: 16/9;
object-fit: cover;
}
.imagePlaceholder {
height: 100%;
width: 100%;
background-color: #fff;
background-image:
linear-gradient(45deg, #000000 25%, transparent 25%),
linear-gradient(-45deg, #000000 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #000000 75%),
linear-gradient(-45deg, transparent 75%, #000000 75%);
background-size: 120px 120px;
background-position:
0 0,
0 60px,
60px -60px,
-60px 0;
}
.roomDetails {
display: flex;
flex-direction: column;
gap: var(--Spacing-x5);
}
.bookingDetails {
max-width: 100%;
padding: 0 var(--Spacing-x2);
}
.row {
display: flex;
flex-direction: column;
padding: var(--Spacing-x-one-and-half) 0;
}
.row:last-child {
border-bottom: none;
}
.rowTitle {
display: flex;
flex-direction: row;
gap: var(--Spacing-x1);
}
.rowTitle svg {
width: 24px;
height: 24px;
}
.rowContent {
padding-left: var(--Spacing-x4);
}
.bookingInformation {
display: flex;
flex-direction: column-reverse;
align-items: center;
gap: var(--Spacing-x2);
background-color: var(--Scandic-Beige-10);
margin: 0 var(--Spacing-x2);
border: 1px solid var(--Base-Border-Subtle);
border-radius: var(--Corner-radius-Medium);
}
.priceDetails {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--Spacing-x1);
padding: var(--Spacing-x-one-and-half) 0;
width: calc(100% - var(--Spacing-x4));
justify-content: center;
margin: 0 auto;
}
.price {
align-items: center;
display: flex;
gap: var(--Spacing-x1);
justify-content: space-between;
width: 100%;
}
.userDetails {
width: 100%;
border-bottom: 1px solid var(--Base-Border-Subtle);
margin-bottom: var(--Spacing-x1);
color: var(--Scandic-Brand-Burgundy);
}
.guestDetailsMobileWrapper {
display: block;
padding: 0 var(--Spacing-x2);
}
.guestDetailsDesktopWrapper {
display: none;
}
@media (min-width: 768px) {
.room {
background-color: transparent;
padding: 0;
}
.roomName {
padding: 0;
}
.roomHeader {
justify-content: space-between;
align-items: center;
flex-direction: row;
padding: 0;
}
.sidePeek {
display: block;
}
.booking {
border-radius: var(--Corner-radius-Large);
background-color: var(--Base-Background-Primary-Normal);
}
.content {
padding: var(--Spacing-x2);
grid-template-columns: 3fr 2fr;
width: var(--max-width-content);
}
.packages {
top: 620px;
left: 25px;
}
.imageContainer {
height: 640px;
}
.image {
height: 100%;
border-radius: var(--Corner-radius-Medium);
}
.bookingDetails {
padding: 0;
}
.row {
border-bottom: 1px solid var(--Base-Border-Subtle);
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.rowTitle svg {
width: 20px;
height: 20px;
}
.bookingInformation {
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
padding: var(--Spacing-x-one-and-half);
margin: 0;
border-radius: 0;
border: none;
}
.priceDetails {
margin: 0 0 0 auto;
width: auto;
align-items: flex-end;
}
.price {
justify-content: flex-end;
}
.guestDetailsMobileWrapper {
display: none;
}
.guestDetailsDesktopWrapper {
display: block;
margin-top: auto;
}
}

View File

@@ -0,0 +1,26 @@
"use client"
import { useMyStayStore } from "@/stores/my-stay"
import PriceType from "../PriceType"
import type { PriceType as _PriceType } from "@/types/components/hotelReservation/myStay/myStay"
export default function TotalPrice() {
const { bookedRoom, formattedTotalPrice } = useMyStayStore((state) => ({
bookedRoom: state.bookedRoom,
formattedTotalPrice: state.totalPrice,
}))
return (
<PriceType
cheques={bookedRoom.cheques}
formattedTotalPrice={formattedTotalPrice}
isCancelled={bookedRoom.isCancelled}
priceType={bookedRoom.priceType}
rateDefinition={bookedRoom.rateDefinition}
roomPoints={bookedRoom.roomPoints}
totalPrice={bookedRoom.totalPrice}
vouchers={bookedRoom.vouchers}
/>
)
}

View File

@@ -1,80 +0,0 @@
"use client"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore"
import { useMyStayTotalPriceStore } from "@/stores/my-stay/myStayTotalPrice"
import Points from "../../Points"
import Price from "../../Price"
import styles from "./totalPrice.module.css"
import type { PriceType } from "@/types/components/hotelReservation/myStay/myStay"
export type Variant =
| "Title/Subtitle/lg"
| "Title/Subtitle/md"
| "Body/Paragraph/mdBold"
interface TotalPriceProps {
variant: Variant
type?: PriceType
}
export default function TotalPrice({
variant,
type = "money",
}: TotalPriceProps) {
const totalPrice = useMyStayTotalPriceStore((state) => state.totalPrice)
const totalPoints = useMyStayTotalPriceStore((state) => state.totalPoints)
const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom)
const { vouchers, cheques } = bookedRoom
const intl = useIntl()
if (type === "money") {
return <Price price={totalPrice} variant={variant} />
}
if (type === "voucher") {
return (
<Typography variant={variant}>
<p>
{intl.formatMessage(
{
defaultMessage: "{count} voucher",
},
{ count: vouchers }
)}
</p>
</Typography>
)
}
if (type === "cheque") {
return (
<div className={styles.totalPrice}>
<Typography variant={variant}>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<p>{cheques} CC + </p>
</Typography>
<Price price={totalPrice} variant={variant} />
</div>
)
}
if (totalPrice && totalPrice > 0 && type === "points") {
return (
<div className={styles.totalPrice}>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<Points points={totalPoints} variant={variant} /> +{" "}
<Price price={totalPrice} variant={variant} />
</div>
)
}
return <Points points={totalPoints} variant={variant} />
}

View File

@@ -1,5 +0,0 @@
.totalPrice {
display: flex;
align-items: center;
gap: 10px;
}

View File

@@ -1,54 +1,37 @@
import { Suspense } from "react"
"use client"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
import { useMyStayStore } from "@/stores/my-stay"
import { getIntl } from "@/i18n"
import MultiRoom from "../MultiRoom"
import MultiRoomSkeleton from "../MultiRoom/MultiRoomSkeleton"
import PriceDetails from "../PriceDetails"
import { SingleRoom } from "../SingleRoom"
import { getPriceType } from "../utils/getPriceType"
import MultiRoom from "./MultiRoom"
import SingleRoom from "./SingleRoom"
import TotalPrice from "./TotalPrice"
import styles from "./rooms.module.css"
import { type Hotel, type Room } from "@/types/hotel"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import type { User } from "@/types/user"
import type { SafeUser } from "@/types/user"
interface RoomsProps {
booking: BookingConfirmation["booking"]
room:
| (Room & {
bedType: Room["roomTypes"][number]
})
| null
hotel: Hotel
user: User | null
user: SafeUser
}
export default async function Rooms({
booking,
room,
hotel,
user,
}: RoomsProps) {
const intl = await getIntl()
export default function Rooms({ user }: RoomsProps) {
const intl = useIntl()
const { allRoomsAreCancelled, room, rooms } = useMyStayStore((state) => ({
allRoomsAreCancelled: state.allRoomsAreCancelled,
hotel: state.hotel,
room: state.bookedRoom.room,
rooms: state.rooms,
}))
if (!room) {
return null
}
const linkedBookingPromises = booking.linkedReservations
? booking.linkedReservations.map((linkedBooking) => {
return getBookingConfirmation(linkedBooking.confirmationNumber)
})
: []
const isMultiRoom = booking.linkedReservations.length > 0
const isMultiRoom = rooms.length > 1
return (
<div className={styles.wrapper}>
@@ -66,24 +49,16 @@ export default async function Rooms({
<SingleRoom
bedType={room.bedType}
image={room.images[0]}
hotel={hotel}
user={user}
/>
) : (
<div className={styles.roomsContainer}>
<MultiRoom booking={booking} room={room} user={user} />
{booking.linkedReservations.map((linkedRes, index) => (
{rooms.map((booking, index) => (
<div
key={linkedRes.confirmationNumber}
key={booking.confirmationNumber}
className={styles.roomWrapper}
>
<Suspense fallback={<MultiRoomSkeleton />}>
<MultiRoom
bookingPromise={linkedBookingPromises[index]}
index={index}
user={user}
/>
</Suspense>
<MultiRoom booking={booking} roomNr={index + 1} user={user} />
</div>
))}
</div>
@@ -101,17 +76,10 @@ export default async function Rooms({
{":"}
</p>
</Typography>
<TotalPrice
variant="Title/Subtitle/lg"
type={getPriceType(
booking.cheques,
booking.roomPoints,
booking.vouchers
)}
/>
<TotalPrice />
</div>
<PriceDetails />
{allRoomsAreCancelled ? null : <PriceDetails />}
</div>
)}
</div>