feat: refactor NewDates, clean up legacy code

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

View File

@@ -28,7 +28,8 @@ import BookingSummary from "@/components/HotelReservation/MyStay/BookingSummary"
import { Header } from "@/components/HotelReservation/MyStay/Header" import { Header } from "@/components/HotelReservation/MyStay/Header"
import Promo from "@/components/HotelReservation/MyStay/Promo" import Promo from "@/components/HotelReservation/MyStay/Promo"
import { ReferenceCard } from "@/components/HotelReservation/MyStay/ReferenceCard" import { ReferenceCard } from "@/components/HotelReservation/MyStay/ReferenceCard"
import Rooms from "@/components/HotelReservation/MyStay/Rooms" import MultiRoom from "@/components/HotelReservation/MyStay/Rooms/MultiRoom"
import SingleRoom from "@/components/HotelReservation/MyStay/Rooms/SingleRoom"
import SidePeek from "@/components/HotelReservation/SidePeek" import SidePeek from "@/components/HotelReservation/SidePeek"
import Image from "@/components/Image" import Image from "@/components/Image"
import { getIntl } from "@/i18n" import { getIntl } from "@/i18n"
@@ -116,6 +117,11 @@ export default async function MyStay({
rooms: booking.linkedReservations, rooms: booking.linkedReservations,
}) })
const ancillariesInput = {
fromDate,
hotelId: hotel.operaId,
toDate,
}
const packagesInput = { const packagesInput = {
adults: booking.adults, adults: booking.adults,
children: booking.childrenAges.length, children: booking.childrenAges.length,
@@ -141,21 +147,27 @@ export default async function MyStay({
if (shouldFetchBreakfastPackages) { if (shouldFetchBreakfastPackages) {
void getPackages(packagesInput) void getPackages(packagesInput)
} }
void getSavedPaymentCardsSafely(savedPaymentCardsInput) if (user) {
void getSavedPaymentCardsSafely(savedPaymentCardsInput)
const ancillaryPackages = await getAncillaryPackages({ }
fromDate, if (booking.showAncillaries) {
hotelId: hotel.operaId, void getAncillaryPackages(ancillariesInput)
toDate, }
})
let breakfastPackages = null let breakfastPackages = null
if (shouldFetchBreakfastPackages) { if (shouldFetchBreakfastPackages) {
breakfastPackages = await getPackages(packagesInput) breakfastPackages = await getPackages(packagesInput)
} }
const savedCreditCards = await getSavedPaymentCardsSafely( let savedCreditCards = null
savedPaymentCardsInput if (user) {
) savedCreditCards = await getSavedPaymentCardsSafely(
savedPaymentCardsInput
)
}
let ancillaryPackagesPromise = null
if (booking.showAncillaries) {
ancillaryPackagesPromise = getAncillaryPackages(ancillariesInput)
}
const imageSrc = const imageSrc =
hotel.hotelContent.images.imageSizes.large ?? hotel.hotelContent.images.imageSizes.large ??
@@ -196,9 +208,9 @@ export default async function MyStay({
<Header cityName={hotel.cityName} name={hotel.name} /> <Header cityName={hotel.cityName} name={hotel.name} />
<ReferenceCard /> <ReferenceCard />
</div> </div>
{booking.showAncillaries && ( {booking.showAncillaries && ancillaryPackagesPromise && (
<Ancillaries <Ancillaries
ancillaries={ancillaryPackages} ancillariesPromise={ancillaryPackagesPromise}
booking={booking} booking={booking}
packages={breakfastPackages} packages={breakfastPackages}
user={user} user={user}
@@ -207,7 +219,8 @@ export default async function MyStay({
/> />
)} )}
<Rooms user={user} /> <SingleRoom user={user} />
<MultiRoom user={user} />
<BookingSummary hotel={hotel} /> <BookingSummary hotel={hotel} />
<Promo <Promo

View File

@@ -28,7 +28,8 @@ import BookingSummary from "@/components/HotelReservation/MyStay/BookingSummary"
import { Header } from "@/components/HotelReservation/MyStay/Header" import { Header } from "@/components/HotelReservation/MyStay/Header"
import Promo from "@/components/HotelReservation/MyStay/Promo" import Promo from "@/components/HotelReservation/MyStay/Promo"
import { ReferenceCard } from "@/components/HotelReservation/MyStay/ReferenceCard" import { ReferenceCard } from "@/components/HotelReservation/MyStay/ReferenceCard"
import Rooms from "@/components/HotelReservation/MyStay/Rooms" import MultiRoom from "@/components/HotelReservation/MyStay/Rooms/MultiRoom"
import SingleRoom from "@/components/HotelReservation/MyStay/Rooms/SingleRoom"
import SidePeek from "@/components/HotelReservation/SidePeek" import SidePeek from "@/components/HotelReservation/SidePeek"
import Image from "@/components/Image" import Image from "@/components/Image"
import { getIntl } from "@/i18n" import { getIntl } from "@/i18n"
@@ -113,6 +114,11 @@ export default async function MyStay({
rooms: booking.linkedReservations, rooms: booking.linkedReservations,
}) })
const ancillariesInput = {
fromDate,
hotelId: hotel.operaId,
toDate,
}
const packagesInput = { const packagesInput = {
adults: booking.adults, adults: booking.adults,
children: booking.childrenAges.length, children: booking.childrenAges.length,
@@ -133,26 +139,32 @@ export default async function MyStay({
(pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST (pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST
) )
const breakfastIncluded = booking.rateDefinition.breakfastIncluded const breakfastIncluded = booking.rateDefinition.breakfastIncluded
const alreadyHasABreakfastSelection = const shouldFetchBreakfastPackages =
!hasBreakfastPackage && !breakfastIncluded !hasBreakfastPackage && !breakfastIncluded
if (alreadyHasABreakfastSelection) { if (shouldFetchBreakfastPackages) {
void getPackages(packagesInput) void getPackages(packagesInput)
} }
void getSavedPaymentCardsSafely(savedPaymentCardsInput) if (user) {
void getSavedPaymentCardsSafely(savedPaymentCardsInput)
const ancillaryPackages = await getAncillaryPackages({ }
fromDate, if (booking.showAncillaries) {
hotelId: hotel.operaId, void getAncillaryPackages(ancillariesInput)
toDate, }
})
let breakfastPackages = null let breakfastPackages = null
if (alreadyHasABreakfastSelection) { if (shouldFetchBreakfastPackages) {
breakfastPackages = await getPackages(packagesInput) breakfastPackages = await getPackages(packagesInput)
} }
const savedCreditCards = await getSavedPaymentCardsSafely( let savedCreditCards = null
savedPaymentCardsInput if (user) {
) savedCreditCards = await getSavedPaymentCardsSafely(
savedPaymentCardsInput
)
}
let ancillaryPackagesPromise = null
if (booking.showAncillaries) {
ancillaryPackagesPromise = getAncillaryPackages(ancillariesInput)
}
const imageSrc = const imageSrc =
hotel.hotelContent.images.imageSizes.large ?? hotel.hotelContent.images.imageSizes.large ??
@@ -193,9 +205,9 @@ export default async function MyStay({
<Header cityName={hotel.cityName} name={hotel.name} /> <Header cityName={hotel.cityName} name={hotel.name} />
<ReferenceCard /> <ReferenceCard />
</div> </div>
{booking.showAncillaries && ( {booking.showAncillaries && ancillaryPackagesPromise && (
<Ancillaries <Ancillaries
ancillaries={ancillaryPackages} ancillariesPromise={ancillaryPackagesPromise}
booking={booking} booking={booking}
packages={breakfastPackages} packages={breakfastPackages}
user={user} user={user}
@@ -204,7 +216,8 @@ export default async function MyStay({
/> />
)} )}
<Rooms user={user} /> <SingleRoom user={user} />
<MultiRoom user={user} />
<BookingSummary hotel={hotel} /> <BookingSummary hotel={hotel} />
<Promo <Promo

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,6 @@
.container { .container {
display: grid; display: grid;
gap: var(--Space-x3); gap: var(--Space-x3);
grid-template-rows: auto 1fr auto;
height: 100%;
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ import { useIntl } from "react-intl"
import { useMyStayStore } from "@/stores/my-stay" import { 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 Alert from "@/components/TempDesignSystem/Alert"
import { AlertTypeEnum } from "@/types/enums/alert" import { AlertTypeEnum } from "@/types/enums/alert"

View File

@@ -7,7 +7,7 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import { dt } from "@/lib/dt" import { dt } from "@/lib/dt"
import { useMyStayStore } from "@/stores/my-stay" 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 useLang from "@/hooks/useLang"
import CancelStayPriceContainer from "../CancelStayPriceContainer" import CancelStayPriceContainer from "../CancelStayPriceContainer"

View File

@@ -7,7 +7,7 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import { trpc } from "@/lib/trpc/client" import { trpc } from "@/lib/trpc/client"
import { useMyStayStore } from "@/stores/my-stay" 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 { toast } from "@/components/TempDesignSystem/Toasts"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
"use client" "use client"
import { useIntl } from "react-intl" 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 Alert from "@/components/TempDesignSystem/Alert"
import { AlertTypeEnum } from "@/types/enums/alert" import { AlertTypeEnum } from "@/types/enums/alert"

View File

@@ -1,7 +1,7 @@
"use client" "use client"
import { useIntl } from "react-intl" 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 Alert from "@/components/TempDesignSystem/Alert"
import { AlertTypeEnum } from "@/types/enums/alert" import { AlertTypeEnum } from "@/types/enums/alert"

View File

@@ -1,7 +1,7 @@
"use client" "use client"
import { useIntl } from "react-intl" 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 Alert from "@/components/TempDesignSystem/Alert"
import { AlertTypeEnum } from "@/types/enums/alert" import { AlertTypeEnum } from "@/types/enums/alert"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import { useIntl } from "react-intl"
import { useMyStayStore } from "@/stores/my-stay" 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 { dateHasPassed } from "../utils"
import Alerts from "./Alerts" import Alerts from "./Alerts"

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import { useMyStayStore } from "@/stores/my-stay" import { 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 Actions from "./Actions"
import Info from "./Info" import Info from "./Info"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,30 @@
"use client"
import { Button as ButtonRAC, DialogTrigger } from "react-aria-components"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import BookedRoomSidePeek from "@/components/SidePeeks/BookedRoomSidePeek"
import styles from "./sidePeek.module.css"
import type { Room as MyStayRoom } from "@/types/stores/my-stay"
import type { SafeUser } from "@/types/user"
interface RoomDetailsSidePeekProps {
booking: MyStayRoom
user: SafeUser
}
export default function RoomDetailsSidePeek({
booking,
user,
}: RoomDetailsSidePeekProps) {
return (
<DialogTrigger>
<ButtonRAC className={styles.trigger}>
<MaterialIcon icon="pan_zoom" color="CurrentColor" />
</ButtonRAC>
<BookedRoomSidePeek hotelRoom={booking.room} room={booking} user={user} />
</DialogTrigger>
)
}

View File

@@ -1,317 +1,70 @@
"use client" "use client"
import { Button as ButtonRAC, DialogTrigger } from "react-aria-components"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { dt } from "@/lib/dt" import { useMyStayStore } from "@/stores/my-stay"
import { getBookedHotelRoom } from "@/server/routers/booking/utils"
import { IconForFeatureCode } from "@/components/HotelReservation/utils" import PriceDetails from "../../PriceDetails"
import Image from "@/components/Image" import TotalPrice from "../TotalPrice"
import BookedRoomSidePeek from "@/components/SidePeeks/BookedRoomSidePeek" import Room from "./Room"
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 styles from "./multiRoom.module.css" 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" import type { SafeUser } from "@/types/user"
interface MultiRoomProps { interface MultiRoomProps {
booking: Room
roomNr: number
user: SafeUser user: SafeUser
roomCategories: RoomCategories
} }
export default function MultiRoom({ export default function MultiRoom(props: MultiRoomProps) {
booking,
roomNr,
user,
roomCategories,
}: MultiRoomProps) {
const intl = useIntl() const intl = useIntl()
const lang = useLang() const { allRoomsAreCancelled, rooms } = useMyStayStore((state) => ({
allRoomsAreCancelled: state.allRoomsAreCancelled,
rooms: state.rooms,
}))
const { if (rooms.length <= 1) {
adults, return null
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
)
} }
const hotelRoom = getBookedHotelRoom(roomCategories, roomTypeCode)
return ( return (
<article className={styles.multiRoom}> <div className={styles.wrapper}>
<Typography variant="Title/smRegular"> <Typography variant="Title/sm">
<h3 className={styles.roomName}>{roomName}</h3> <h2 className={styles.title}>
{intl.formatMessage({
defaultMessage: "Your rooms",
})}
</h2>
</Typography> </Typography>
<div className={styles.roomHeader}> <div className={styles.container}>
{isCancelled ? ( <div className={styles.roomsContainer}>
<IconChip {rooms.map((booking, index) => (
color={"red"} <div
icon={ key={booking.confirmationNumber}
<MaterialIcon className={styles.roomWrapper}
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}
> >
<MaterialIcon icon="pan_zoom" color="CurrentColor" /> <Room {...props} booking={booking} roomNr={index + 1} />
</ButtonRAC> </div>
<BookedRoomSidePeek ))}
hotelRoom={hotelRoom}
room={booking}
user={user}
/>
</DialogTrigger>
</div> </div>
</div> </div>
<div <div className={styles.totalContainer}>
className={`${styles.multiRoomCard} ${isCancelled ? styles.cancelled : ""}`} <div className={styles.total}>
> <Typography variant="Body/Lead text">
{packages?.some((item) => <p>
Object.values(RoomPackageCodeEnum).includes( {intl.formatMessage({
item.code as RoomPackageCodeEnum defaultMessage: "Booking total",
)
) && (
<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> {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
)} {":"}
<div className={styles.imageContainer}> </p>
<Image </Typography>
src={room?.images[0]?.imageSizes.small ?? ""} <TotalPrice />
alt={roomName}
fill
/>
</div> </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"> {allRoomsAreCancelled ? null : <PriceDetails />}
{/* 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> </div>
</article> </div>
) )
} }

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
.trigger {
background: none;
border: 1px solid var(--Base-Border-Subtle);
border-radius: var(--Corner-radius-Small);
cursor: pointer;
display: flex;
padding: var(--Spacing-x-half);
}

View File

@@ -0,0 +1,42 @@
"use client"
import { useIntl } from "react-intl"
import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useMyStayStore } from "@/stores/my-stay"
import IconChip from "@/components/TempDesignSystem/IconChip"
export default function BookingCode() {
const intl = useIntl()
const bookingCode = useMyStayStore((state) => state.bookedRoom.bookingCode)
if (!bookingCode) {
return null
}
return (
<Typography variant="Body/Supporting text (caption)/smBold">
<IconChip
color="blue"
icon={<DiscountIcon color="Icon/Feedback/Information" />}
>
{intl.formatMessage(
{
defaultMessage: "<strong>Booking code</strong>: {value}",
},
{
value: bookingCode,
strong: (text) => (
<Typography variant="Body/Supporting text (caption)/smBold">
<strong>{text}</strong>
</Typography>
),
}
)}
</IconChip>
</Typography>
)
}

View File

@@ -0,0 +1,30 @@
.priceDetails {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--Spacing-x1);
padding: var(--Spacing-x-one-and-half) 0;
width: calc(100% - var(--Spacing-x4));
justify-content: center;
margin: 0 auto;
}
.price {
align-items: center;
display: flex;
gap: var(--Spacing-x1);
justify-content: space-between;
width: 100%;
}
@media (min-width: 768px) {
.priceDetails {
align-items: flex-end;
margin: 0 0 0 auto;
width: auto;
}
.price {
justify-content: flex-end;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,35 @@
"use client"
import { useIntl } from "react-intl"
import { useMyStayStore } from "@/stores/my-stay"
import { formatPrice } from "@/utils/numberFormatting"
import Row from "./Row"
export default function Breakfast() {
const intl = useIntl()
const { breakfast, rateDefinition } = useMyStayStore((state) => ({
breakfast: state.bookedRoom.breakfast,
rateDefinition: state.bookedRoom.rateDefinition,
}))
let breakfastPrice = intl.formatMessage({
defaultMessage: "No breakfast",
})
if (rateDefinition.breakfastIncluded) {
breakfastPrice = intl.formatMessage({
defaultMessage: "Included",
})
} else if (breakfast) {
breakfastPrice = formatPrice(
intl,
breakfast.localPrice.totalPrice,
breakfast.localPrice.currency
)
}
const title = intl.formatMessage({ defaultMessage: "Breakfast" })
return <Row icon="coffee" text={breakfastPrice} title={title} />
}

View File

@@ -0,0 +1,45 @@
import { useIntl } from "react-intl"
import { useMyStayStore } from "@/stores/my-stay"
import Row from "./Row"
export default function Guests() {
const intl = useIntl()
const { adults, childrenAges } = useMyStayStore((state) => ({
adults: state.bookedRoom.adults,
childrenAges: state.bookedRoom.childrenAges,
}))
const adultsMsg = intl.formatMessage(
{
defaultMessage: "{adults, plural, one {# adult} other {# adults}}",
},
{
adults,
}
)
const childrenMsg = intl.formatMessage(
{
defaultMessage: "{children, plural, one {# child} other {# children}}",
},
{
children: childrenAges.length,
}
)
const adultsOnlyMsg = adultsMsg
const adultsAndChildrenMsg = [adultsMsg, childrenMsg].join(", ")
return (
<Row
icon="person"
text={childrenAges.length > 0 ? adultsAndChildrenMsg : adultsOnlyMsg}
title={intl.formatMessage({
defaultMessage: "Guests",
})}
/>
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,39 @@
.header {
display: flex;
gap: var(--Spacing-x-one-and-half);
padding: 0 var(--Spacing-x2);
}
.container {
display: flex;
gap: var(--Spacing-x-one-and-half);
}
.chip {
background-color: var(--Scandic-Peach-30);
color: var(--Scandic-Red-100);
border-radius: var(--Corner-radius-Small);
padding: var(--Spacing-x-half) var(--Spacing-x1);
}
.reference {
display: flex;
gap: var(--Spacing-x-half);
}
.sidePeek {
display: none;
}
@media (min-width: 768px) {
.header {
justify-content: space-between;
align-items: center;
flex-direction: row;
padding: 0;
}
.sidePeek {
display: block;
}
}

View File

@@ -0,0 +1,59 @@
"use client"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useMyStayStore } from "@/stores/my-stay"
import RoomDetailsSidePeek from "../RoomDetailsSidePeek"
import styles from "./header.module.css"
import type { SafeUser } from "@/types/user"
export default function Header({ user }: { user: SafeUser }) {
const intl = useIntl()
const { confirmationNumber, roomNumber } = useMyStayStore((state) => ({
confirmationNumber: state.bookedRoom.confirmationNumber,
roomNumber: state.bookedRoom.roomNumber,
}))
return (
<div className={styles.header}>
<div className={styles.container}>
<div className={styles.chip}>
<Typography variant="Tag/sm">
<span>
{intl.formatMessage(
{
defaultMessage: "Room {roomIndex}",
},
{
roomIndex: roomNumber,
}
)}
</span>
</Typography>
</div>
<div className={styles.reference}>
<Typography variant="Body/Paragraph/mdBold">
<span>
{intl.formatMessage({
defaultMessage: "Booking number",
})}
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
{":"}
</span>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
<span>{confirmationNumber}</span>
</Typography>
</div>
</div>
<div className={styles.sidePeek}>
<RoomDetailsSidePeek user={user} />
</div>
</div>
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,94 +1,48 @@
"use client" "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 { 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 { useMyStayStore } from "@/stores/my-stay"
import GuestDetails from "@/components/HotelReservation/MyStay/GuestDetails" import GuestDetails from "@/components/HotelReservation/MyStay/GuestDetails"
import PriceDetails from "@/components/HotelReservation/MyStay/PriceDetails" 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 SkeletonShimmer from "@/components/SkeletonShimmer"
import IconChip from "@/components/TempDesignSystem/IconChip"
import useLang from "@/hooks/useLang" import BookingInformation from "./BookingInformation"
import { formatPrice } from "@/utils/numberFormatting" import Details from "./Details"
import Header from "./Header"
import Img from "./Img"
import Packages from "./Packages"
import styles from "./room.module.css" 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" import type { SafeUser } from "@/types/user"
interface RoomProps { interface RoomProps {
bedType: Room["roomTypes"][number]
image: Room["images"][number]
user: SafeUser user: SafeUser
roomCategories: RoomCategories
} }
export default function SingleRoom({ export default function SingleRoom({ user }: RoomProps) {
bedType,
image,
user,
roomCategories,
}: RoomProps) {
const intl = useIntl()
const lang = useLang()
const { const {
adults,
bookingCode,
breakfast,
checkInDate,
cheques,
childrenAges,
confirmationNumber, confirmationNumber,
formattedTotalPrice,
guest, guest,
isCancelled, isCancelled,
packages, isMultiRoom,
priceType,
rateDefinition,
roomName, roomName,
roomNumber, roomNumber,
roomPoints,
roomTypeCode,
totalPrice,
vouchers,
bookedRoom,
} = useMyStayStore((state) => ({ } = 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, confirmationNumber: state.bookedRoom.confirmationNumber,
formattedTotalPrice: state.totalPrice, guest: state.bookedRoom.guest,
hotel: state.hotel,
isCancelled: state.bookedRoom.isCancelled, isCancelled: state.bookedRoom.isCancelled,
packages: state.bookedRoom.packages, isMultiRoom: state.rooms.length > 1,
priceType: state.bookedRoom.priceType,
rateDefinition: state.bookedRoom.rateDefinition,
roomName: state.bookedRoom.roomName, roomName: state.bookedRoom.roomName,
roomNumber: state.bookedRoom.roomNumber, 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) { if (!roomNumber) {
return ( return (
<div className={styles.room}> <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 ( return (
<div> <div className={styles.wrapper}>
<article className={styles.room}> <div className={styles.container}>
<Typography variant="Title/Subtitle/lg"> <article className={styles.room}>
<p className={styles.roomName}>{roomName}</p> <Typography variant="Title/Subtitle/lg">
</Typography> <p className={styles.roomName}>{roomName}</p>
<div className={styles.roomHeader}> </Typography>
<div className={styles.roomHeaderContent}> <Header user={user} />
<div className={styles.chip}> <div className={styles.booking}>
<Typography variant="Tag/sm"> <div
<span> className={`${styles.content} ${isCancelled ? styles.cancelled : ""}`}
{intl.formatMessage( >
{ <Packages />
defaultMessage: "Room {roomIndex}", <Img />
}, <div className={styles.roomDetails}>
{ <Details />
roomIndex: roomNumber, <div className={styles.guestDetailsDesktopWrapper}>
} <GuestDetails
)} confirmationNumber={confirmationNumber}
</span> guest={guest}
</Typography> isCancelled={isCancelled}
</div> user={user}
<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}
/> />
</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> </div>
<div className={styles.guestDetailsDesktopWrapper}>
<GuestDetails
confirmationNumber={confirmationNumber}
guest={guest}
isCancelled={isCancelled}
user={user}
/>
</div>
</div> </div>
<BookingInformation />
</div> </div>
<div className={styles.bookingInformation}> <PriceDetails />
{bookingCode && ( <div className={styles.guestDetailsMobileWrapper}>
<Typography variant="Body/Supporting text (caption)/smBold"> <GuestDetails
<IconChip confirmationNumber={confirmationNumber}
color="blue" guest={guest}
icon={<DiscountIcon color="Icon/Feedback/Information" />} isCancelled={isCancelled}
> user={user}
{intl.formatMessage( />
{
defaultMessage: "<strong>Booking code</strong>: {value}",
},
{
value: bookingCode,
strong: (text) => (
<Typography variant="Body/Supporting text (caption)/smBold">
<strong>{text}</strong>
</Typography>
),
}
)}
</IconChip>
</Typography>
)}
<div className={styles.priceDetails}>
<div className={styles.price}>
<Typography variant="Body/Lead text">
<p color="uiTextHighContrast">
{intl.formatMessage({
defaultMessage: "Room total",
})}
</p>
</Typography>
<PriceType
cheques={cheques}
formattedTotalPrice={formattedTotalPrice}
isCancelled={isCancelled}
rateDefinition={rateDefinition}
priceType={priceType}
roomPoints={roomPoints}
totalPrice={totalPrice}
vouchers={vouchers}
/>
</div>
</div>
</div> </div>
</div> </article>
</div>
<PriceDetails />
<div className={styles.guestDetailsMobileWrapper}>
<GuestDetails
confirmationNumber={confirmationNumber}
guest={guest}
isCancelled={isCancelled}
user={user}
/>
</div>
</article>
</div> </div>
) )
} }

View File

@@ -1,42 +1,28 @@
.wrapper {
display: flex;
flex-direction: column;
gap: var(--Spacing-x3);
}
.container {
display: flex;
flex-direction: column;
gap: var(--Spacing-x5);
}
.room { .room {
background-color: var(--Base-Background-Primary-Normal);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--Spacing-x2); gap: var(--Spacing-x2);
background-color: var(--Base-Background-Primary-Normal);
padding: var(--Spacing-x3) 0; 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 { .roomName {
color: var(--Scandic-Brand-Burgundy); color: var(--Scandic-Brand-Burgundy);
padding: 0 var(--Spacing-x2); 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 { .booking {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -56,136 +42,12 @@
opacity: 0.5; 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 { .roomDetails {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--Spacing-x5); 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 { .guestDetailsMobileWrapper {
display: block; display: block;
padding: 0 var(--Spacing-x2); padding: 0 var(--Spacing-x2);
@@ -205,17 +67,6 @@
padding: 0; padding: 0;
} }
.roomHeader {
justify-content: space-between;
align-items: center;
flex-direction: row;
padding: 0;
}
.sidePeek {
display: block;
}
.booking { .booking {
border-radius: var(--Corner-radius-Large); border-radius: var(--Corner-radius-Large);
background-color: var(--Base-Background-Primary-Normal); background-color: var(--Base-Background-Primary-Normal);
@@ -227,56 +78,6 @@
width: var(--max-width-content); 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 { .guestDetailsMobileWrapper {
display: none; display: none;
} }

View File

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

View File

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

View File

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

View File

@@ -32,7 +32,7 @@ import type { Packages } from "@/types/requests/packages"
import type { BookingConfirmationSchema } from "@/types/trpc/routers/booking/confirmation" import type { BookingConfirmationSchema } from "@/types/trpc/routers/booking/confirmation"
import type { SafeUser } from "@/types/user" import type { SafeUser } from "@/types/user"
export type PartialHotelRoom = Pick< type PartialHotelRoom = Pick<
HotelRoom, HotelRoom,
"descriptions" | "images" | "name" | "roomFacilities" | "roomTypes" "descriptions" | "images" | "name" | "roomFacilities" | "roomTypes"
> >

View File

@@ -18,13 +18,15 @@ export function AncillaryCard({ ancillary }: AncillaryCardProps) {
return ( return (
<article className={styles.ancillaryCard}> <article className={styles.ancillaryCard}>
<div className={styles.imageContainer}> <div className={styles.imageContainer}>
<Image {ancillary.imageUrl ? (
className={styles.image} <Image
src={ancillary.imageUrl} className={styles.image}
alt={ancillary.title} src={ancillary.imageUrl}
fill alt={ancillary.title}
style={{ opacity: ancillary.imageOpacity ?? 1 }} fill
/> style={{ opacity: ancillary.imageOpacity ?? 1 }}
/>
) : null}
</div> </div>
<div className={styles.contentContainer}> <div className={styles.contentContainer}>
<div> <div>

View File

@@ -1,4 +1,5 @@
import type { SidePeekSelfControlledProps } from "./sidePeek" import type { SidePeekSelfControlledProps } from "./sidePeek"
// Sidepeeks generally have important content that should be indexed by search engines. // Sidepeeks generally have important content that should be indexed by search engines.
// The content is hidden behind a modal, but it is still important for SEO. // The content is hidden behind a modal, but it is still important for SEO.
// This component is used to provide SEO information for the sidepeek content. // This component is used to provide SEO information for the sidepeek content.

View File

@@ -1,4 +1,5 @@
"use client" "use client"
import { useEffect } from "react" import { useEffect } from "react"
import { Dialog, Modal, ModalOverlay } from "react-aria-components" import { Dialog, Modal, ModalOverlay } from "react-aria-components"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
@@ -54,10 +55,12 @@ export default function SidePeekSelfControlled({
</Dialog> </Dialog>
</Modal> </Modal>
</ModalOverlay> </ModalOverlay>
<SidePeekSEO title={title}>{children}</SidePeekSEO> <SidePeekSEO title={title}>{children}</SidePeekSEO>
</> </>
) )
} }
function KeepBodyVisible() { function KeepBodyVisible() {
const toggle = useSetOverflowVisibleOnRA() const toggle = useSetOverflowVisibleOnRA()
useEffect(() => { useEffect(() => {

View File

@@ -38,6 +38,7 @@
height: 100vh; height: 100vh;
background-color: var(--Base-Background-Primary-Normal); background-color: var(--Base-Background-Primary-Normal);
z-index: var(--sidepeek-z-index); z-index: var(--sidepeek-z-index);
outline: none;
} }
.modal[data-entering] { .modal[data-entering] {

View File

@@ -2,6 +2,7 @@ export type SidePeekProps = {
activeContent: string | null activeContent: string | null
onClose: (isOpen: boolean) => void onClose: (isOpen: boolean) => void
} }
export type SidePeekContentProps = { export type SidePeekContentProps = {
title?: string title?: string
contentKey: string contentKey: string

View File

@@ -73,8 +73,8 @@ export default function BookingConfirmationProvider({
currencyCode, currencyCode,
fromDate, fromDate,
toDate, toDate,
rooms,
roomCategories, roomCategories,
rooms,
vat, vat,
isVatCurrency, isVatCurrency,
formattedTotalCost, formattedTotalCost,

View File

@@ -10,13 +10,13 @@ import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmat
import type { Lang } from "@/constants/languages" import type { Lang } from "@/constants/languages"
export function getBookedHotelRoom( export function getBookedHotelRoom(
rooms: Room[] | undefined, rooms: Room[],
roomTypeCode: BookingConfirmation["booking"]["roomTypeCode"] roomTypeCode: BookingConfirmation["booking"]["roomTypeCode"]
) { ) {
if (!rooms?.length || !roomTypeCode) { if (!rooms.length || !roomTypeCode) {
return null return null
} }
const room = rooms?.find((r) => { const room = rooms.find((r) => {
return r.roomTypes.find((roomType) => roomType.code === roomTypeCode) return r.roomTypes.find((roomType) => roomType.code === roomTypeCode)
}) })
if (!room) { if (!room) {

View File

@@ -74,7 +74,6 @@ export function createMyStayStore({
savedCreditCards, savedCreditCards,
totalPoints, totalPoints,
totalPrice, totalPrice,
roomCategories,
actions: { actions: {
closeManageStay() { closeManageStay() {

View File

@@ -3,6 +3,7 @@ export interface LinkedReservationProps {
checkOutTime: string checkOutTime: string
confirmationNumber: string confirmationNumber: string
roomIndex: number roomIndex: number
roomNumber: number
} }
export interface RetryProps { export interface RetryProps {

View File

@@ -13,7 +13,7 @@ export type SelectedAncillary = Ancillary["ancillaryContent"][number]
export type Packages = z.output<typeof packagesSchema> export type Packages = z.output<typeof packagesSchema>
export interface AncillariesProps extends Pick<BookingConfirmation, "booking"> { export interface AncillariesProps extends Pick<BookingConfirmation, "booking"> {
ancillaries: Ancillaries | null ancillariesPromise: Promise<Ancillaries | null>
packages: Packages | null packages: Packages | null
user: User | null user: User | null
savedCreditCards: CreditCard[] | null savedCreditCards: CreditCard[] | null

View File

@@ -1,13 +1,11 @@
import type { Room } from "@/types/hotel" import type { Room } from "@/types/hotel"
import type { SafeUser } from "@/types/user" import type { SafeUser } from "@/types/user"
import type { SidePeekEnum } from "../hotelReservation/sidePeek"
export type BookedRoomSidePeekProps = { export type BookedRoomSidePeekProps = {
room: Room
activeSidePeek: SidePeekEnum | null
close: () => void close: () => void
user: SafeUser
confirmationNumber: string confirmationNumber: string
room: Room
user: SafeUser
} }
export type RoomDetailsProps = { export type RoomDetailsProps = {

View File

@@ -7,8 +7,8 @@ export interface BookingConfirmationProviderProps
bookingCode: string | null bookingCode: string | null
currencyCode: CurrencyEnum currencyCode: CurrencyEnum
fromDate: Date fromDate: Date
rooms: (Room | null)[]
roomCategories: RoomCategories roomCategories: RoomCategories
rooms: (Room | null)[]
toDate: Date toDate: Date
vat: number vat: number
} }

View File

@@ -76,7 +76,6 @@ export interface MyStayState {
savedCreditCards: CreditCard[] | null savedCreditCards: CreditCard[] | null
totalPoints: number totalPoints: number
totalPrice: string totalPrice: string
roomCategories: RoomCategories
} }
export interface InitialState export interface InitialState