feat: refactor NewDates, clean up legacy code

This reverts commit 0c7836fa59.
This commit is contained in:
Simon Emanuelsson
2025-05-03 19:33:04 +02:00
parent c6a0b4ee30
commit db289b80b1
96 changed files with 1603 additions and 1500 deletions

View File

@@ -22,6 +22,7 @@ export function LinkedReservation({
checkOutTime,
confirmationNumber,
roomIndex,
roomNumber,
}: LinkedReservationProps) {
const lang = useLang()
const { data, refetch, isLoading } = trpc.booking.get.useQuery({
@@ -86,7 +87,7 @@ export function LinkedReservation({
checkOutTime={checkOutTime}
img={data.room.images[0]}
roomName={data.room.name}
roomNumber={roomIndex + 1}
roomNumber={roomNumber}
/>
)
}

View File

@@ -1,4 +1,5 @@
"use client"
import { Button as ButtonRAC, DialogTrigger } from "react-aria-components"
import { useIntl } from "react-intl"
@@ -14,7 +15,7 @@ import { convertToChildType } from "@/components/HotelReservation/utils/convertT
import { getPriceType } from "@/components/HotelReservation/utils/getPriceType"
import BookedRoomSidePeek from "@/components/SidePeeks/BookedRoomSidePeek"
import styles from "./RoomDetailsSidePeek.module.css"
import styles from "./sidePeek.module.css"
import type { BreakfastPackage } from "@/types/components/hotelReservation/breakfast"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
@@ -26,6 +27,7 @@ interface RoomDetailsSidePeekProps {
booking: BookingConfirmationSchema
roomNumber?: number
}
export default function RoomDetailsSidePeek({
booking,
roomNumber = 1,
@@ -36,9 +38,11 @@ export default function RoomDetailsSidePeek({
(state) => state.roomCategories
)
const hotelRoom = getBookedHotelRoom(roomCategories, booking.roomTypeCode)
const breakfastPackage = booking.packages.find(
(pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST
)
const breakfast: Omit<BreakfastPackage, "requestedPrice"> | null =
breakfastPackage
? {
@@ -52,21 +56,25 @@ export default function RoomDetailsSidePeek({
packageType: PackageTypeEnum.BreakfastAdult,
}
: null
const childrenInRoom = convertToChildType(
booking.childrenAges,
booking.childBedPreferences
)
const priceType = getPriceType(
booking.cheques,
booking.roomPoints,
booking.vouchers
)
const featuresPackages = booking.packages.filter(
(pkg) =>
pkg.code === RoomPackageCodeEnum.PET_ROOM ||
pkg.code === RoomPackageCodeEnum.ALLERGY_ROOM ||
pkg.code === RoomPackageCodeEnum.ACCESSIBILITY_ROOM
)
const packages = featuresPackages.map((pkg) => ({
code: pkg.code as RoomPackageCodeEnum,
description: pkg.description,
@@ -83,6 +91,7 @@ export default function RoomDetailsSidePeek({
totalPrice: pkg.totalPrice,
},
}))
const room = {
...booking,
bedType: {

View File

@@ -59,6 +59,7 @@ export default async function Rooms({
checkOutTime={checkOutTime}
confirmationNumber={reservation.confirmationNumber}
roomIndex={idx + 1}
roomNumber={idx + 2}
/>
</div>
))}

View File

@@ -1,5 +1,5 @@
"use client"
import { useMemo } from "react"
import { use, useMemo } from "react"
import { useIntl } from "react-intl"
import { Carousel } from "@/components/Carousel"
@@ -78,7 +78,7 @@ function addBreakfastPackage(
}
export function Ancillaries({
ancillaries,
ancillariesPromise,
booking,
packages,
user,
@@ -86,6 +86,7 @@ export function Ancillaries({
refId,
}: AncillariesProps) {
const intl = useIntl()
const ancillaries = use(ancillariesPromise)
/**
* A constructed ancillary for breakfast

View File

@@ -6,6 +6,7 @@
display: flex;
gap: var(--Space-x1);
padding: var(--Space-x1) 0;
text-align: left;
width: 100%;
}

View File

@@ -56,6 +56,13 @@
}
}
@media screen and (max-width: 767px) {
.modal {
bottom: 0;
width: 100%;
}
}
@media screen and (min-width: 768px) {
.overlay {
align-items: center;
@@ -65,6 +72,8 @@
.modal {
border-radius: var(--Corner-radius-Large);
width: min(690px, 100dvw);
display: flex;
min-height: 300px;
min-width: 690px;
}
}

View File

@@ -1,3 +1,7 @@
.dialog {
max-width: 690px;
}
.links {
display: grid;
gap: var(--Space-x05);

View File

@@ -8,7 +8,7 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import styles from "./customerSupport.module.css"
@@ -30,7 +30,7 @@ export default function CustomerSupportModal() {
return (
<Modal>
<Dialog>
<Dialog className={styles.dialog}>
{({ close }) => (
<Modal.Content>
<Modal.Content.Header handleClose={close} title={title}>

View File

@@ -15,4 +15,5 @@
.text {
color: var(--Text-Interactive-Default);
text-align: left;
}

View File

@@ -3,7 +3,7 @@ import { useIntl } from "react-intl"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import Alert from "@/components/TempDesignSystem/Alert"
import { AlertTypeEnum } from "@/types/enums/alert"

View File

@@ -7,7 +7,7 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import { dt } from "@/lib/dt"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import useLang from "@/hooks/useLang"
import CancelStayPriceContainer from "../CancelStayPriceContainer"

View File

@@ -7,7 +7,7 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import { trpc } from "@/lib/trpc/client"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import { toast } from "@/components/TempDesignSystem/Toasts"
import useLang from "@/hooks/useLang"

View File

@@ -1,3 +1,7 @@
.dialog {
max-width: 690px;
}
.modalText {
display: flex;
flex-direction: column;

View File

@@ -2,11 +2,13 @@
import { Dialog, DialogTrigger } from "react-aria-components"
import { useIntl } from "react-intl"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import Alerts from "./Alerts"
import Steps from "./Steps"
import styles from "./cancelStay.module.css"
export default function CancelStay() {
const intl = useIntl()
return (
@@ -15,7 +17,7 @@ export default function CancelStay() {
{intl.formatMessage({ defaultMessage: "Cancel stay" })}
</Modal.Button>
<Modal>
<Dialog>
<Dialog className={styles.dialog}>
{({ close }) => (
<Alerts closeModal={close}>
<Steps closeModal={close} />

View File

@@ -1,7 +1,7 @@
"use client"
import { useIntl } from "react-intl"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import Alert from "@/components/TempDesignSystem/Alert"
import { AlertTypeEnum } from "@/types/enums/alert"

View File

@@ -1,7 +1,7 @@
"use client"
import { useIntl } from "react-intl"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import Alert from "@/components/TempDesignSystem/Alert"
import { AlertTypeEnum } from "@/types/enums/alert"

View File

@@ -1,7 +1,7 @@
"use client"
import { useIntl } from "react-intl"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import Alert from "@/components/TempDesignSystem/Alert"
import { AlertTypeEnum } from "@/types/enums/alert"

View File

@@ -5,7 +5,7 @@ import { dt } from "@/lib/dt"
import { trpc } from "@/lib/trpc/client"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import PriceContainer from "@/components/HotelReservation/MyStay/ReferenceCard/PriceContainer"
import Divider from "@/components/TempDesignSystem/Divider"
import { toast } from "@/components/TempDesignSystem/Toasts"

View File

@@ -1,14 +0,0 @@
.button {
background-color: var(--Main-Grey-White);
border-color: var(--Scandic-Beige-40);
border-style: solid;
border-width: 1px;
border-radius: var(--Corner-radius-Medium);
display: flex;
align-items: center;
justify-content: space-between;
min-width: 0; /* allow shrinkage */
height: 60px;
padding: var(--Spacing-x1) var(--Spacing-x2);
transition: border-color 200ms ease;
}

View File

@@ -1,24 +0,0 @@
"use client"
import { Button as ButtonRAC } from "react-aria-components"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import styles from "./calendarButton.module.css"
interface CalendarButtonProps {
text: string
onClick: () => void
}
export default function CalendarButton({ text, onClick }: CalendarButtonProps) {
return (
<ButtonRAC onPress={onClick} className={styles.button}>
<Typography variant="Body/Paragraph/mdRegular">
<span>{text}</span>
</Typography>
<MaterialIcon icon="calendar_today" />
</ButtonRAC>
)
}

View File

@@ -1,39 +1,33 @@
"use client"
import { useState } from "react"
import { createPortal } from "react-dom"
import { useFormContext } from "react-hook-form"
import {
Button as ButtonRAC,
Dialog,
DialogTrigger,
} from "react-aria-components"
import { useFormContext, useWatch } from "react-hook-form"
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 { useMyStayStore } from "@/stores/my-stay"
import DatePickerSingleDesktop from "@/components/DatePicker/Single/Desktop"
import DatePickerSingleMobile from "@/components/DatePicker/Single/Mobile"
import Modal from "@/components/Modal"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import useLang from "@/hooks/useLang"
import CalendarButton from "./CalendarButton"
import styles from "./newDates.module.css"
import type { DateRange } from "react-day-picker"
export default function NewDates() {
const { checkInDate, checkOutDate } = useMyStayStore((state) => ({
checkInDate: state.mainRoom.checkInDate,
checkOutDate: state.mainRoom.checkOutDate,
}))
const [showCheckInDatePicker, setShowCheckInDatePicker] = useState(false)
const [showCheckOutDatePicker, setShowCheckOutDatePicker] = useState(false)
const [selectedDates, setSelectedDates] = useState<DateRange>(() => ({
from: dt(checkInDate).startOf("day").toDate(),
to: dt(checkOutDate).startOf("day").toDate(),
}))
interface NewDatesProps {
checkInDate: Date
checkOutDate: Date
}
export default function NewDates({ checkInDate, checkOutDate }: NewDatesProps) {
const intl = useIntl()
const lang = useLang()
const { setValue } = useFormContext()
// Calculate default number of days between check-in and check-out
@@ -41,147 +35,111 @@ export default function NewDates() {
.startOf("day")
.diff(dt(checkInDate).startOf("day"), "days")
function showCheckInPicker() {
// Update selected dates before showing picker
setSelectedDates((prev) => ({
from: prev.from ?? dt(checkInDate).startOf("day").toDate(),
to: prev.to ?? dt(checkOutDate).startOf("day").toDate(),
}))
setShowCheckInDatePicker(true)
setShowCheckOutDatePicker(false)
const fromDate = useWatch({ name: "checkInDate" })
const toDate = useWatch({ name: "checkOutDate" })
function handleSelectDate(date: Date, name: "checkInDate" | "checkOutDate") {
setValue(name, dt(date).format("YYYY-MM-DD"))
}
function showCheckOutPicker() {
// Update selected dates before showing picker
setSelectedDates((prev) => ({
from: prev.from ?? dt(checkInDate).startOf("day").toDate(),
to: prev.to ?? dt(checkOutDate).startOf("day").toDate(),
}))
setShowCheckOutDatePicker(true)
setShowCheckInDatePicker(false)
}
function handleCheckInDateSelect(date: Date) {
const newCheckIn = dt(date).startOf("day")
const currentCheckOut = dt(selectedDates.to).startOf("day")
// Calculate new check-out date based on defaultDaysBetween, only if new check-in is after current check-out
const newCheckOut = newCheckIn.isSameOrAfter(currentCheckOut)
? newCheckIn.add(defaultDaysBetween, "days")
: currentCheckOut
// Update selected dates state first
const newDates = {
from: newCheckIn.toDate(),
to: newCheckOut.toDate(),
function handleSelectCheckInDate(checkIn: Date) {
handleSelectDate(checkIn, "checkInDate")
if (dt(checkIn).isSameOrAfter(toDate)) {
handleSelectDate(
dt(checkIn).add(defaultDaysBetween, "days").toDate(),
"checkOutDate"
)
}
setSelectedDates(newDates)
// Then update form values
setValue("checkInDate", newCheckIn.format("YYYY-MM-DD"))
setValue("checkOutDate", newCheckOut.format("YYYY-MM-DD"))
}
function handleCheckOutDateSelect(date: Date) {
const newCheckOut = dt(date).startOf("day")
const currentCheckIn = dt(selectedDates.from).startOf("day")
// Only adjust check-in if new check-out is before current check-in
const newCheckIn = newCheckOut.isBefore(currentCheckIn)
? newCheckOut.subtract(defaultDaysBetween, "days")
: currentCheckIn
// Update selected dates state
const newDates = {
from: newCheckIn.toDate(),
to: newCheckOut.toDate(),
function handleSelectCheckOutDate(checkOut: Date) {
handleSelectDate(checkOut, "checkOutDate")
if (dt(checkOut).isSameOrBefore(fromDate)) {
handleSelectDate(
dt(checkOut).subtract(defaultDaysBetween, "days").toDate(),
"checkInDate"
)
}
setSelectedDates(newDates)
// Then update form values
setValue("checkInDate", newCheckIn.format("YYYY-MM-DD"))
setValue("checkOutDate", newCheckOut.format("YYYY-MM-DD"))
}
const fromDate = selectedDates.from ?? dt(checkInDate).toDate()
const toDate = selectedDates.to ?? dt(checkOutDate).toDate()
const checkInLabel = intl.formatMessage({ defaultMessage: "Check-in" })
const checkOutLabel = intl.formatMessage({ defaultMessage: "Check-out" })
const checkInText = dt(fromDate).locale(lang).format("dddd, DD MMM, YYYY")
const checkOutText = dt(toDate).locale(lang).format("dddd, DD MMM, YYYY")
return (
<>
<div className={styles.container}>
<div className={styles.checkInDate}>
<Caption color="uiTextHighContrast" type="bold">
{intl.formatMessage({
defaultMessage: "Check-in",
})}
</Caption>
<Typography variant="Body/Supporting text (caption)/smBold">
<span className={styles.textDefault}>{checkInLabel}</span>
</Typography>
<CalendarButton
text={dt(selectedDates.from ?? new Date())
.locale(lang)
.format("dddd, DD MMM, YYYY")}
onClick={showCheckInPicker}
/>
<DialogTrigger>
<ButtonRAC className={styles.trigger}>
<Typography variant="Body/Paragraph/mdRegular">
<span>{checkInText}</span>
</Typography>
<MaterialIcon icon="calendar_today" />
</ButtonRAC>
<Modal>
<Dialog>
{({ close }) => (
<>
<DatePickerSingleDesktop
close={close}
handleOnSelect={handleSelectCheckInDate}
selectedDate={fromDate}
startMonth={fromDate}
/>
<DatePickerSingleMobile
close={close}
handleOnSelect={handleSelectCheckInDate}
hideHeader
selectedDate={fromDate}
/>
</>
)}
</Dialog>
</Modal>
</DialogTrigger>
</div>
<div className={styles.checkOutDate}>
<Caption color="uiTextHighContrast" type="bold">
{intl.formatMessage({
defaultMessage: "Check-out",
})}
</Caption>
<CalendarButton
text={dt(selectedDates.to ?? new Date())
.locale(lang)
.format("dddd, DD MMM, YYYY")}
onClick={showCheckOutPicker}
/>
<div className={styles.checkOutDate}>
<Typography variant="Body/Supporting text (caption)/smBold">
<span className={styles.textDefault}>{checkOutLabel}</span>
</Typography>
<DialogTrigger>
<ButtonRAC className={styles.trigger}>
<Typography variant="Body/Paragraph/mdRegular">
<span>{checkOutText}</span>
</Typography>
<MaterialIcon icon="calendar_today" />
</ButtonRAC>
<Modal>
<Dialog>
{({ close }) => (
<>
<DatePickerSingleDesktop
close={close}
handleOnSelect={handleSelectCheckOutDate}
selectedDate={toDate}
startMonth={toDate}
/>
<DatePickerSingleMobile
close={close}
handleOnSelect={handleSelectCheckOutDate}
hideHeader
selectedDate={toDate}
/>
</>
)}
</Dialog>
</Modal>
</DialogTrigger>
</div>
</div>
{showCheckInDatePicker &&
createPortal(
<Modal
isOpen={showCheckInDatePicker}
onToggle={() => setShowCheckInDatePicker(!showCheckInDatePicker)}
>
<DatePickerSingleDesktop
close={() => setShowCheckInDatePicker(false)}
handleOnSelect={handleCheckInDateSelect}
selectedDate={fromDate}
startMonth={fromDate}
/>
<DatePickerSingleMobile
close={() => setShowCheckInDatePicker(false)}
handleOnSelect={handleCheckInDateSelect}
selectedDate={fromDate}
hideHeader
/>
</Modal>,
document.body
)}
{showCheckOutDatePicker &&
createPortal(
<Modal
isOpen={showCheckOutDatePicker}
onToggle={() => setShowCheckOutDatePicker(!showCheckOutDatePicker)}
>
<DatePickerSingleDesktop
close={() => setShowCheckOutDatePicker(false)}
handleOnSelect={handleCheckOutDateSelect}
selectedDate={toDate}
startMonth={toDate}
/>
<DatePickerSingleMobile
close={() => setShowCheckOutDatePicker(false)}
handleOnSelect={handleCheckOutDateSelect}
selectedDate={toDate}
hideHeader
/>
</Modal>,
document.body
)}
</>
)
}

View File

@@ -13,3 +13,23 @@
flex-direction: column;
gap: var(--Spacing-x1);
}
.trigger {
align-items: center;
background-color: var(--Main-Grey-White);
border-color: var(--Scandic-Beige-40);
border-radius: var(--Corner-radius-Medium);
border-style: solid;
border-width: 1px;
display: flex;
height: 60px;
justify-content: space-between;
min-width: 0;
/* allow shrinkage */
padding: var(--Spacing-x1) var(--Spacing-x2);
transition: border-color 200ms ease;
}
.textDefault {
color: var(--Text-Default);
}

View File

@@ -6,7 +6,7 @@ import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import { toast } from "@/components/TempDesignSystem/Toasts"
import NoAvailability from "./Alerts/NoAvailability"
@@ -62,7 +62,7 @@ export default function Form({
/>
<Modal.Content.Body>
{noAvailability && <NoAvailability />}
<NewDates />
<NewDates checkInDate={checkInDate} checkOutDate={checkOutDate} />
</Modal.Content.Body>
<Modal.Content.Footer>
<Modal.Content.Footer.Secondary onClick={closeModal}>

View File

@@ -4,7 +4,7 @@ import { useIntl } from "react-intl"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import { dateHasPassed } from "../utils"
import Alerts from "./Alerts"

View File

@@ -2,8 +2,8 @@
import { DialogTrigger } from "react-aria-components"
import { useIntl } from "react-intl"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import CustomerSupportModal from "@/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
export default function CustomerSupport() {
const intl = useIntl()

View File

@@ -34,15 +34,21 @@
}
.guaranteeCostText {
align-items: flex-end;
display: flex;
flex-direction: column;
}
.baseTextHighContrast {
color: var(--Base-Text-High-contrast);
white-space: nowrap;
}
.textDefault {
color: var(--Text-Default);
}
@media screen and (min-width: 768px) {
.guaranteeCostText {
align-items: flex-end;
}
}

View File

@@ -6,11 +6,13 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import { dateHasPassed } from "../utils"
import Form from "./Form"
import styles from "./guarantee.module.css"
export default function GuaranteeLateArrival() {
const intl = useIntl()
@@ -41,7 +43,7 @@ export default function GuaranteeLateArrival() {
<DialogTrigger>
<Modal.Button icon="check">{text}</Modal.Button>
<Modal>
<Dialog>
<Dialog className={styles.dialog}>
{({ close }) => (
<Modal.Content>
<Modal.Content.Header handleClose={close} title={text}>

View File

@@ -3,7 +3,6 @@
background-color: var(--Surface-Primary-OnSurface-Default);
border-radius: var(--Corner-radius-md);
display: flex;
flex-direction: column;
gap: var(--Space-x2);
justify-content: center;
padding: var(--Space-x15) var(--Space-x3);
@@ -27,3 +26,9 @@
display: flex;
flex-direction: column;
}
@media screen and (min-width: 768px) {
.container {
flex-direction: column;
}
}

View File

@@ -11,7 +11,7 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import Actions from "./Actions"
import Info from "./Info"

View File

@@ -24,7 +24,8 @@
.dialog {
display: grid;
gap: var(--Space-x3);
flex: 1;
gap: var(--Space-x2);
}
.header {
@@ -47,6 +48,16 @@
.content {
display: grid;
gap: var(--Space-x3);
grid-template-columns: 1fr 1fr;
gap: var(--Space-x2);
}
@media screen and (min-width: 768px) {
.dialog {
gap: var(--Space-x3);
}
.content {
gap: var(--Space-x3);
grid-template-columns: 1fr 1fr;
}
}

View File

@@ -4,6 +4,7 @@
border-radius: var(--Corner-radius-rounded);
color: var(--Text-Interactive-Default);
display: flex;
height: 48px;
justify-content: center;
text-decoration: none;
}

View File

@@ -33,8 +33,8 @@ export default function PriceContainer({
{totalChildren > 0 ? `, ${childrenText}` : ""}
</Caption>
</div>
<div className={styles.price}>
<Subtitle color="burgundy" type="one">
<div className={styles.wrapper}>
<Subtitle className={styles.price} color="burgundy" type="one">
{price}
</Subtitle>
</div>

View File

@@ -15,8 +15,12 @@
flex-direction: column;
}
.price {
.wrapper {
padding-left: var(--Spacing-x2);
display: flex;
align-items: center;
}
.price {
white-space: nowrap;
}

View File

@@ -0,0 +1,292 @@
"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 RoomDetailsSidePeek from "./RoomDetailsSidePeek"
import styles from "./room.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 RoomProps {
booking: Room
roomNr: number
user: SafeUser
}
export default function Room({ booking, roomNr, user }: RoomProps) {
const intl = useIntl()
const lang = useLang()
const {
adults,
breakfast,
cancellationNumber,
checkInDate,
cheques,
childrenAges,
confirmationNumber,
currencyCode,
packages,
rateDefinition,
room,
roomName,
roomPoints,
isCancelled,
priceType,
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: "Booking number",
})}
{/* 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}>
<RoomDetailsSidePeek booking={booking} user={user} />
</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,30 @@
"use client"
import { Button as ButtonRAC, DialogTrigger } from "react-aria-components"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import BookedRoomSidePeek from "@/components/SidePeeks/BookedRoomSidePeek"
import styles from "./sidePeek.module.css"
import type { Room as MyStayRoom } from "@/types/stores/my-stay"
import type { SafeUser } from "@/types/user"
interface RoomDetailsSidePeekProps {
booking: MyStayRoom
user: SafeUser
}
export default function RoomDetailsSidePeek({
booking,
user,
}: RoomDetailsSidePeekProps) {
return (
<DialogTrigger>
<ButtonRAC className={styles.trigger}>
<MaterialIcon icon="pan_zoom" color="CurrentColor" />
</ButtonRAC>
<BookedRoomSidePeek hotelRoom={booking.room} room={booking} user={user} />
</DialogTrigger>
)
}

View File

@@ -1,317 +1,70 @@
"use client"
import { Button as ButtonRAC, DialogTrigger } from "react-aria-components"
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 { getBookedHotelRoom } from "@/server/routers/booking/utils"
import { useMyStayStore } from "@/stores/my-stay"
import { IconForFeatureCode } from "@/components/HotelReservation/utils"
import Image from "@/components/Image"
import BookedRoomSidePeek from "@/components/SidePeeks/BookedRoomSidePeek"
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 PriceDetails from "../../PriceDetails"
import TotalPrice from "../TotalPrice"
import Room from "./Room"
import styles from "./multiRoom.module.css"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { RoomCategories } from "@/types/hotel"
import type { Room } from "@/types/stores/my-stay"
import type { SafeUser } from "@/types/user"
interface MultiRoomProps {
booking: Room
roomNr: number
user: SafeUser
roomCategories: RoomCategories
}
export default function MultiRoom({
booking,
roomNr,
user,
roomCategories,
}: MultiRoomProps) {
export default function MultiRoom(props: MultiRoomProps) {
const intl = useIntl()
const lang = useLang()
const { allRoomsAreCancelled, rooms } = useMyStayStore((state) => ({
allRoomsAreCancelled: state.allRoomsAreCancelled,
rooms: state.rooms,
}))
const {
adults,
breakfast,
cancellationNumber,
checkInDate,
cheques,
childrenAges,
confirmationNumber,
currencyCode,
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
)
if (rooms.length <= 1) {
return null
}
const hotelRoom = getBookedHotelRoom(roomCategories, roomTypeCode)
return (
<article className={styles.multiRoom}>
<Typography variant="Title/smRegular">
<h3 className={styles.roomName}>{roomName}</h3>
<div className={styles.wrapper}>
<Typography variant="Title/sm">
<h2 className={styles.title}>
{intl.formatMessage({
defaultMessage: "Your rooms",
})}
</h2>
</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: "Booking number",
})}
{/* 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}>
<DialogTrigger>
<ButtonRAC
aria-label={intl.formatMessage({
defaultMessage: "View room details",
})}
className={styles.iconContainer}
<div className={styles.container}>
<div className={styles.roomsContainer}>
{rooms.map((booking, index) => (
<div
key={booking.confirmationNumber}
className={styles.roomWrapper}
>
<MaterialIcon icon="pan_zoom" color="CurrentColor" />
</ButtonRAC>
<BookedRoomSidePeek
hotelRoom={hotelRoom}
room={booking}
user={user}
/>
</DialogTrigger>
<Room {...props} booking={booking} roomNr={index + 1} />
</div>
))}
</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 className={styles.totalContainer}>
<div className={styles.total}>
<Typography variant="Body/Lead text">
<p>
{intl.formatMessage({
defaultMessage: "Booking total",
})}
</div>
)}
<div className={styles.imageContainer}>
<Image
src={room?.images[0]?.imageSizes.small ?? ""}
alt={roomName}
fill
/>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
{":"}
</p>
</Typography>
<TotalPrice />
</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>
{allRoomsAreCancelled ? null : <PriceDetails />}
</div>
</article>
</div>
)
}

View File

@@ -1,101 +1,63 @@
.multiRoom {
.wrapper {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
gap: var(--Spacing-x3);
}
.title {
color: var(--Scandic-Brand-Burgundy);
padding: 0 var(--Spacing-x2);
}
.cancelled {
opacity: 0.5;
}
.cancellationNumber {
text-decoration: line-through;
}
.multiRoomCard {
.container {
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;
gap: var(--Spacing-x5);
}
.imageContainer {
.roomsContainer {
display: grid;
gap: var(--Spacing-x3);
grid-template-columns: 1fr;
width: 100%;
height: 342px;
position: relative;
}
.iconContainer {
.roomWrapper {
min-width: 0;
width: 100%;
}
.roomWrapper > * {
width: 100%;
}
.totalContainer {
display: flex;
border: 1px solid var(--Base-Border-Subtle);
border-radius: var(--Corner-radius-Small);
padding: var(--Spacing-x-half);
}
.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;
padding: 0 var(--Spacing-x2);
}
.package {
background-color: var(--Main-Grey-White);
padding: var(--Spacing-x-half) var(--Spacing-x1);
border-radius: var(--Corner-radius-Small);
.total {
display: flex;
justify-content: flex-end;
gap: var(--Spacing-x1);
}
@media (min-width: 768px) {
.multiRoom {
.roomsContainer {
grid-template-columns: repeat(2, 1fr);
}
.roomsContainer:has(> *:nth-child(3):last-child) {
grid-template-columns: repeat(3, 1fr);
}
.title {
padding: 0;
}
.totalContainer {
padding: 0;
}
}

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,8 @@
.trigger {
background: none;
border: 1px solid var(--Base-Border-Subtle);
border-radius: var(--Corner-radius-Small);
cursor: pointer;
display: flex;
padding: var(--Spacing-x-half);
}

View File

@@ -0,0 +1,42 @@
"use client"
import { useIntl } from "react-intl"
import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useMyStayStore } from "@/stores/my-stay"
import IconChip from "@/components/TempDesignSystem/IconChip"
export default function BookingCode() {
const intl = useIntl()
const bookingCode = useMyStayStore((state) => state.bookedRoom.bookingCode)
if (!bookingCode) {
return null
}
return (
<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>
)
}

View File

@@ -0,0 +1,30 @@
.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%;
}
@media (min-width: 768px) {
.priceDetails {
align-items: flex-end;
margin: 0 0 0 auto;
width: auto;
}
.price {
justify-content: flex-end;
}
}

View File

@@ -0,0 +1,40 @@
"use client"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useMyStayStore } from "@/stores/my-stay"
import PriceType from "@/components/HotelReservation/MyStay/PriceType"
import styles from "./details.module.css"
export default function PriceDetails() {
const intl = useIntl()
const pricing = useMyStayStore((state) => ({
cheques: state.bookedRoom.cheques,
formattedTotalPrice: state.totalPrice,
isCancelled: state.bookedRoom.isCancelled,
priceType: state.bookedRoom.priceType,
rateDefinition: state.bookedRoom.rateDefinition,
roomPoints: state.bookedRoom.roomPoints,
totalPrice: state.bookedRoom.totalPrice,
vouchers: state.bookedRoom.vouchers,
}))
return (
<div className={styles.priceDetails}>
<div className={styles.price}>
<Typography variant="Body/Lead text">
<p color="uiTextHighContrast">
{intl.formatMessage({
defaultMessage: "Room total",
})}
</p>
</Typography>
<PriceType {...pricing} />
</div>
</div>
)
}

View File

@@ -0,0 +1,13 @@
import BookingCode from "./BookingCode"
import PriceDetails from "./PriceDetails"
import styles from "./information.module.css"
export default function BookingInformation() {
return (
<div className={styles.bookingInformation}>
<BookingCode />
<PriceDetails />
</div>
)
}

View File

@@ -0,0 +1,22 @@
.bookingInformation {
align-items: center;
background-color: var(--Scandic-Beige-10);
border: 1px solid var(--Base-Border-Subtle);
border-radius: var(--Corner-radius-Medium);
display: flex;
flex-direction: column-reverse;
gap: var(--Spacing-x2);
margin: 0 var(--Spacing-x2);
}
@media (min-width: 768px) {
.bookingInformation {
align-items: flex-start;
border: none;
border-radius: 0;
flex-direction: row;
justify-content: space-between;
margin: 0;
padding: var(--Spacing-x-one-and-half);
}
}

View File

@@ -0,0 +1,49 @@
import { useIntl } from "react-intl"
import { useMyStayStore } from "@/stores/my-stay"
import Row from "./Row"
export default function BedPreference() {
const intl = useIntl()
const bedType = useMyStayStore((state) => state.bookedRoom.room?.bedType)
if (!bedType) {
return null
}
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 sameWidth =
bedType.mainBed.widthRange.min === bedType.mainBed.widthRange.max
const widthMsg = sameWidth ? mainBedWidthValueMsg : mainBedWidthRangeMsg
const text = `${bedType.mainBed.description} (${widthMsg})`
return (
<Row
icon="bed"
text={text}
title={intl.formatMessage({
defaultMessage: "Bed preference",
})}
/>
)
}

View File

@@ -0,0 +1,35 @@
"use client"
import { useIntl } from "react-intl"
import { useMyStayStore } from "@/stores/my-stay"
import { formatPrice } from "@/utils/numberFormatting"
import Row from "./Row"
export default function Breakfast() {
const intl = useIntl()
const { breakfast, rateDefinition } = useMyStayStore((state) => ({
breakfast: state.bookedRoom.breakfast,
rateDefinition: state.bookedRoom.rateDefinition,
}))
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
)
}
const title = intl.formatMessage({ defaultMessage: "Breakfast" })
return <Row icon="coffee" text={breakfastPrice} title={title} />
}

View File

@@ -0,0 +1,45 @@
import { useIntl } from "react-intl"
import { useMyStayStore } from "@/stores/my-stay"
import Row from "./Row"
export default function Guests() {
const intl = useIntl()
const { adults, childrenAges } = useMyStayStore((state) => ({
adults: state.bookedRoom.adults,
childrenAges: state.bookedRoom.childrenAges,
}))
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(", ")
return (
<Row
icon="person"
text={childrenAges.length > 0 ? adultsAndChildrenMsg : adultsOnlyMsg}
title={intl.formatMessage({
defaultMessage: "Guests",
})}
/>
)
}

View File

@@ -0,0 +1,47 @@
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import { useMyStayStore } from "@/stores/my-stay"
import { hasModifiableRate } from "@/components/HotelReservation/MyStay/utils"
import useLang from "@/hooks/useLang"
import Row from "./Row"
export default function ModifyBy() {
const intl = useIntl()
const lang = useLang()
const { checkInDate, isModifyable } = useMyStayStore((state) => ({
checkInDate: state.bookedRoom.checkInDate,
isModifyable: hasModifiableRate(
state.bookedRoom.rateDefinition.cancellationRule
),
}))
if (!isModifyable) {
return null
}
const fromDate = dt(checkInDate).locale(lang)
const text = intl.formatMessage(
{
defaultMessage: "Until {time}, {date}",
},
{
time: "18:00",
date: fromDate.format("dddd D MMM"),
}
)
return (
<Row
icon="refresh"
text={text}
title={intl.formatMessage({
defaultMessage: "Modify By",
})}
/>
)
}

View File

@@ -0,0 +1,36 @@
import { useIntl } from "react-intl"
import { useMyStayStore } from "@/stores/my-stay"
import Row from "./Row"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
export default function Packages() {
const intl = useIntl()
const packages = useMyStayStore(
(state) =>
state.bookedRoom.packages
?.filter((item) =>
Object.values(RoomPackageCodeEnum).includes(
item.code as RoomPackageCodeEnum
)
)
.map((item) => item.description) || []
)
if (!packages.length) {
return null
}
return (
<Row
icon="meeting_room"
text={packages.join(", ")}
title={intl.formatMessage({
defaultMessage: "Room classification",
})}
/>
)
}

View File

@@ -0,0 +1,32 @@
"use client"
import {
MaterialIcon,
type MaterialIconProps,
} from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import styles from "./row.module.css"
interface RowProps {
icon: MaterialIconProps["icon"]
text: string
title: string
}
export default function Row({ icon, text, title }: RowProps) {
return (
<div className={styles.row}>
<span className={styles.title}>
<MaterialIcon icon={icon} color="Icon/Default" size={20} />
<Typography variant="Body/Paragraph/mdBold">
<p>{title}</p>
</Typography>
</span>
<div className={styles.content}>
<Typography variant="Body/Paragraph/mdRegular">
<p color="uiTextHighContrast">{text}</p>
</Typography>
</div>
</div>
)
}

View File

@@ -0,0 +1,38 @@
.row {
display: flex;
flex-direction: column;
padding: var(--Spacing-x-one-and-half) 0;
}
.row:last-child {
border-bottom: none;
}
.title {
display: flex;
flex-direction: row;
gap: var(--Spacing-x1);
}
.title svg {
height: 24px;
width: 24px;
}
.content {
padding-left: var(--Spacing-x4);
}
@media (min-width: 768px) {
.row {
align-items: center;
border-bottom: 1px solid var(--Base-Border-Subtle);
flex-direction: row;
justify-content: space-between;
}
.title svg {
height: 20px;
width: 20px;
}
}

View File

@@ -0,0 +1,27 @@
import { useIntl } from "react-intl"
import { useMyStayStore } from "@/stores/my-stay"
import Row from "./Row"
export default function Terms() {
const intl = useIntl()
const cancellationText = useMyStayStore(
(state) => state.bookedRoom.rateDefinition.cancellationText
)
if (!cancellationText) {
return null
}
return (
<Row
icon="contract"
text={cancellationText}
title={intl.formatMessage({
defaultMessage: "Terms",
})}
/>
)
}

View File

@@ -0,0 +1,10 @@
.details {
max-width: 100%;
padding: 0 var(--Spacing-x2);
}
@media (min-width: 768px) {
.details {
padding: 0;
}
}

View File

@@ -0,0 +1,21 @@
import BedPreference from "./BedPreference"
import Breakfast from "./Breakfast"
import Guests from "./Guests"
import ModifyBy from "./ModifyBy"
import Packages from "./Packages"
import Terms from "./Terms"
import styles from "./details.module.css"
export default function Details() {
return (
<div className={styles.details}>
<Guests />
<Terms />
<ModifyBy />
<Breakfast />
<Packages />
<BedPreference />
</div>
)
}

View File

@@ -0,0 +1,39 @@
.header {
display: flex;
gap: var(--Spacing-x-one-and-half);
padding: 0 var(--Spacing-x2);
}
.container {
display: flex;
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);
}
.reference {
display: flex;
gap: var(--Spacing-x-half);
}
.sidePeek {
display: none;
}
@media (min-width: 768px) {
.header {
justify-content: space-between;
align-items: center;
flex-direction: row;
padding: 0;
}
.sidePeek {
display: block;
}
}

View File

@@ -0,0 +1,59 @@
"use client"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useMyStayStore } from "@/stores/my-stay"
import RoomDetailsSidePeek from "../RoomDetailsSidePeek"
import styles from "./header.module.css"
import type { SafeUser } from "@/types/user"
export default function Header({ user }: { user: SafeUser }) {
const intl = useIntl()
const { confirmationNumber, roomNumber } = useMyStayStore((state) => ({
confirmationNumber: state.bookedRoom.confirmationNumber,
roomNumber: state.bookedRoom.roomNumber,
}))
return (
<div className={styles.header}>
<div className={styles.container}>
<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: "Booking number",
})}
{/* 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}>
<RoomDetailsSidePeek user={user} />
</div>
</div>
)
}

View File

@@ -0,0 +1,22 @@
.imageContainer {
height: 220px;
overflow: hidden;
}
.image {
aspect-ratio: 16/9;
height: 220px;
object-fit: cover;
width: 100%;
}
@media (min-width: 768px) {
.imageContainer {
height: 640px;
}
.image {
border-radius: var(--Corner-radius-Medium);
height: 100%;
}
}

View File

@@ -0,0 +1,31 @@
"use client"
import { useMyStayStore } from "@/stores/my-stay"
import Image from "@/components/Image"
import styles from "./img.module.css"
export default function Img() {
const { room, roomName } = useMyStayStore((state) => ({
room: state.bookedRoom.room,
roomName: state.bookedRoom.roomName,
}))
if (!room) {
return null
}
const image = room.images?.[0]
return (
<div className={styles.imageContainer}>
<Image
alt={roomName}
className={styles.image}
height={960}
src={image.imageSizes.small}
width={640}
/>
</div>
)
}

View File

@@ -0,0 +1,38 @@
"use client"
import { useMyStayStore } from "@/stores/my-stay"
import { IconForFeatureCode } from "@/components/HotelReservation/utils"
import styles from "./packages.module.css"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
export default function Packages() {
const packages = useMyStayStore(
(state) =>
state.bookedRoom.packages?.filter((item) =>
Object.values(RoomPackageCodeEnum).includes(
item.code as RoomPackageCodeEnum
)
) || []
)
if (!packages.length) {
return null
}
return (
<div className={styles.packages}>
{packages.map((item) => (
<span className={styles.package} key={item.code}>
<IconForFeatureCode
featureCode={item.code}
size={16}
color="Icon/Interactive/Default"
/>
</span>
))}
</div>
)
}

View File

@@ -0,0 +1,22 @@
.packages {
display: flex;
flex-direction: row;
gap: var(--Spacing-x1);
left: 15px;
position: absolute;
top: 180px;
z-index: 1;
}
.package {
background-color: var(--Main-Grey-White);
border-radius: var(--Corner-radius-Small);
padding: var(--Spacing-x-half) var(--Spacing-x1);
}
@media (min-width: 768px) {
.packages {
left: 25px;
top: 620px;
}
}

View File

@@ -0,0 +1,37 @@
"use client"
import { DialogTrigger } from "react-aria-components"
import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { useMyStayStore } from "@/stores/my-stay"
import BookedRoomSidePeek from "@/components/SidePeeks/BookedRoomSidePeek"
import Button from "@/components/TempDesignSystem/Button"
import type { SafeUser } from "@/types/user"
interface RoomDetailsSidePeekProps {
user: SafeUser
}
export default function RoomDetailsSidePeek({
user,
}: RoomDetailsSidePeekProps) {
const intl = useIntl()
const bookedRoom = useMyStayStore((state) => state.bookedRoom)
return (
<DialogTrigger>
<Button intent="text" size="small" theme="base" variant="icon" wrapping>
{intl.formatMessage({ defaultMessage: "See room details" })}
<MaterialIcon icon="chevron_right" size={14} color="CurrentColor" />
</Button>
<BookedRoomSidePeek
hotelRoom={bookedRoom.room}
room={bookedRoom}
user={user}
/>
</DialogTrigger>
)
}

View File

@@ -1,46 +0,0 @@
"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

@@ -1,94 +1,48 @@
"use client"
import { Button as ButtonRAC, DialogTrigger } from "react-aria-components"
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 { getBookedHotelRoom } from "@/server/routers/booking/utils"
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 BookedRoomSidePeek from "@/components/SidePeeks/BookedRoomSidePeek"
import SkeletonShimmer from "@/components/SkeletonShimmer"
import IconChip from "@/components/TempDesignSystem/IconChip"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import BookingInformation from "./BookingInformation"
import Details from "./Details"
import Header from "./Header"
import Img from "./Img"
import Packages from "./Packages"
import styles from "./room.module.css"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { Room, RoomCategories } from "@/types/hotel"
import type { SafeUser } from "@/types/user"
interface RoomProps {
bedType: Room["roomTypes"][number]
image: Room["images"][number]
user: SafeUser
roomCategories: RoomCategories
}
export default function SingleRoom({
bedType,
image,
user,
roomCategories,
}: RoomProps) {
const intl = useIntl()
const lang = useLang()
export default function SingleRoom({ user }: RoomProps) {
const {
adults,
bookingCode,
breakfast,
checkInDate,
cheques,
childrenAges,
confirmationNumber,
formattedTotalPrice,
guest,
isCancelled,
packages,
priceType,
rateDefinition,
isMultiRoom,
roomName,
roomNumber,
roomPoints,
roomTypeCode,
totalPrice,
vouchers,
bookedRoom,
} = useMyStayStore((state) => ({
adults: state.bookedRoom.adults,
bookingCode: state.bookedRoom.bookingCode,
breakfast: state.bookedRoom.breakfast,
guest: state.bookedRoom.guest,
checkInDate: state.bookedRoom.checkInDate,
cheques: state.bookedRoom.cheques,
childrenAges: state.bookedRoom.childrenAges,
confirmationNumber: state.bookedRoom.confirmationNumber,
formattedTotalPrice: state.totalPrice,
hotel: state.hotel,
guest: state.bookedRoom.guest,
isCancelled: state.bookedRoom.isCancelled,
packages: state.bookedRoom.packages,
priceType: state.bookedRoom.priceType,
rateDefinition: state.bookedRoom.rateDefinition,
isMultiRoom: state.rooms.length > 1,
roomName: state.bookedRoom.roomName,
roomNumber: state.bookedRoom.roomNumber,
roomPoints: state.bookedRoom.roomPoints,
roomTypeCode: state.bookedRoom.roomTypeCode,
totalPrice: state.bookedRoom.totalPrice,
vouchers: state.bookedRoom.vouchers,
bookedRoom: state.bookedRoom,
}))
if (isMultiRoom) {
return null
}
if (!roomNumber) {
return (
<div className={styles.room}>
@@ -98,403 +52,45 @@ export default function SingleRoom({
)
}
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
)
}
const hotelRoom = getBookedHotelRoom(roomCategories, roomTypeCode)
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: "Booking number",
})}
{/* 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}>
<DialogTrigger>
<Typography variant="Body/Supporting text (caption)/smBold">
<ButtonRAC className={styles.trigger}>
<span>
{intl.formatMessage({
defaultMessage: "View room details",
})}
</span>
<MaterialIcon
color="CurrentColor"
icon="chevron_right"
size={20}
<div className={styles.wrapper}>
<div className={styles.container}>
<article className={styles.room}>
<Typography variant="Title/Subtitle/lg">
<p className={styles.roomName}>{roomName}</p>
</Typography>
<Header user={user} />
<div className={styles.booking}>
<div
className={`${styles.content} ${isCancelled ? styles.cancelled : ""}`}
>
<Packages />
<Img />
<div className={styles.roomDetails}>
<Details />
<div className={styles.guestDetailsDesktopWrapper}>
<GuestDetails
confirmationNumber={confirmationNumber}
guest={guest}
isCancelled={isCancelled}
user={user}
/>
</ButtonRAC>
</Typography>
<BookedRoomSidePeek
hotelRoom={hotelRoom}
room={bookedRoom}
user={user}
/>
</DialogTrigger>
</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
confirmationNumber={confirmationNumber}
guest={guest}
isCancelled={isCancelled}
user={user}
/>
</div>
</div>
<BookingInformation />
</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>
<PriceDetails />
<div className={styles.guestDetailsMobileWrapper}>
<GuestDetails
confirmationNumber={confirmationNumber}
guest={guest}
isCancelled={isCancelled}
user={user}
/>
</div>
</div>
<PriceDetails />
<div className={styles.guestDetailsMobileWrapper}>
<GuestDetails
confirmationNumber={confirmationNumber}
guest={guest}
isCancelled={isCancelled}
user={user}
/>
</div>
</article>
</article>
</div>
</div>
)
}

View File

@@ -1,42 +1,28 @@
.wrapper {
display: flex;
flex-direction: column;
gap: var(--Spacing-x3);
}
.container {
display: flex;
flex-direction: column;
gap: var(--Spacing-x5);
}
.room {
background-color: var(--Base-Background-Primary-Normal);
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
background-color: var(--Base-Background-Primary-Normal);
padding: var(--Spacing-x3) 0;
}
.trigger {
align-items: center;
background: none;
border: none;
color: var(--Component-Button-Brand-Secondary-On-fill-Default);
cursor: pointer;
display: flex;
gap: var(--Space-x1);
padding: var(--Space-x025) 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;
@@ -56,136 +42,12 @@
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);
@@ -205,17 +67,6 @@
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);
@@ -227,56 +78,6 @@
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;
}

View File

@@ -1,96 +0,0 @@
"use client"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useMyStayStore } from "@/stores/my-stay"
import PriceDetails from "../PriceDetails"
import MultiRoom from "./MultiRoom"
import SingleRoom from "./SingleRoom"
import TotalPrice from "./TotalPrice"
import styles from "./rooms.module.css"
import type { SafeUser } from "@/types/user"
interface RoomsProps {
user: SafeUser
}
export default function Rooms({ user }: RoomsProps) {
const intl = useIntl()
const { allRoomsAreCancelled, room, rooms, roomCategories } = useMyStayStore(
(state) => ({
allRoomsAreCancelled: state.allRoomsAreCancelled,
hotel: state.hotel,
room: state.bookedRoom.room,
rooms: state.rooms,
roomCategories: state.roomCategories,
})
)
if (!room) {
return null
}
const isMultiRoom = rooms.length > 1
return (
<div className={styles.wrapper}>
{isMultiRoom && (
<Typography variant="Title/sm">
<h2 className={styles.title}>
{intl.formatMessage({
defaultMessage: "Your rooms",
})}
</h2>
</Typography>
)}
<div className={styles.container}>
{!isMultiRoom ? (
<SingleRoom
bedType={room.bedType}
image={room.images[0]}
user={user}
roomCategories={roomCategories}
/>
) : (
<div className={styles.roomsContainer}>
{rooms.map((booking, index) => (
<div
key={booking.confirmationNumber}
className={styles.roomWrapper}
>
<MultiRoom
booking={booking}
roomNr={index + 1}
user={user}
roomCategories={roomCategories}
/>
</div>
))}
</div>
)}
</div>
{isMultiRoom && (
<div className={styles.totalContainer}>
<div className={styles.total}>
<Typography variant="Body/Lead text">
<p>
{intl.formatMessage({
defaultMessage: "Booking total",
})}
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
{":"}
</p>
</Typography>
<TotalPrice />
</div>
{allRoomsAreCancelled ? null : <PriceDetails />}
</div>
)}
</div>
)
}

View File

@@ -1,63 +0,0 @@
.wrapper {
display: flex;
flex-direction: column;
gap: var(--Spacing-x3);
}
.container {
display: flex;
flex-direction: column;
gap: var(--Spacing-x5);
}
.roomsContainer {
display: grid;
gap: var(--Spacing-x3);
width: 100%;
grid-template-columns: 1fr;
}
.roomWrapper {
width: 100%;
min-width: 0;
}
.roomWrapper > * {
width: 100%;
}
.title {
color: var(--Scandic-Brand-Burgundy);
padding: 0 var(--Spacing-x2);
}
.totalContainer {
display: flex;
flex-direction: column;
gap: var(--Spacing-x1);
padding: 0 var(--Spacing-x2);
}
.total {
display: flex;
justify-content: flex-end;
gap: var(--Spacing-x1);
}
@media (min-width: 768px) {
.roomsContainer {
grid-template-columns: repeat(2, 1fr);
}
.roomsContainer:has(> *:nth-child(3):last-child) {
grid-template-columns: repeat(3, 1fr);
}
.title {
padding: 0;
}
.totalContainer {
padding: 0;
}
}

View File

@@ -1,9 +1,8 @@
import { BookingStatusEnum, CancellationRuleEnum } from "@/constants/booking"
import { dt } from "@/lib/dt"
import { convertToChildType } from "@/components/HotelReservation/utils/convertToChildType"
import { getPriceType } from "@/components/HotelReservation/utils/getPriceType"
import { convertToChildType } from "../../utils/convertToChildType"
import { getPriceType } from "../../utils/getPriceType"
import { formatChildBedPreferences } from "../utils"
import type { BreakfastPackage } from "@/types/components/hotelReservation/breakfast"