feat: refactor NewDates, clean up legacy code
This reverts commit 0c7836fa59.
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -59,6 +59,7 @@ export default async function Rooms({
|
||||
checkOutTime={checkOutTime}
|
||||
confirmationNumber={reservation.confirmationNumber}
|
||||
roomIndex={idx + 1}
|
||||
roomNumber={idx + 2}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
display: flex;
|
||||
gap: var(--Space-x1);
|
||||
padding: var(--Space-x1) 0;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
.container {
|
||||
display: grid;
|
||||
gap: var(--Space-x3);
|
||||
grid-template-rows: auto 1fr auto;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
.dialog {
|
||||
max-width: 690px;
|
||||
}
|
||||
|
||||
.links {
|
||||
display: grid;
|
||||
gap: var(--Space-x05);
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -15,4 +15,5 @@
|
||||
|
||||
.text {
|
||||
color: var(--Text-Interactive-Default);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.dialog {
|
||||
max-width: 690px;
|
||||
}
|
||||
|
||||
.modalText {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.dialog {
|
||||
max-width: 690px;
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -15,8 +15,12 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.price {
|
||||
.wrapper {
|
||||
padding-left: var(--Spacing-x2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.price {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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} />
|
||||
}
|
||||
@@ -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",
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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",
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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",
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
.details {
|
||||
max-width: 100%;
|
||||
padding: 0 var(--Spacing-x2);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.details {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user