Merged in fix/SW-2553-sidepeeks (pull request #1919)

Fix/SW-2553 sidepeeks

* fix(SW-2553): apply sidepeek display logic

* chore: move convertToChildType and getPriceType utils

* fix: apply PR requested changes

* fix(SW-2553): fix roomNumber for multiroom

* fix(SW-2553): fix sidepeek for my-stay page


Approved-by: Michael Zetterberg
Approved-by: Bianca Widstam
Approved-by: Matilda Landström
This commit is contained in:
Arvid Norlin
2025-05-02 15:10:34 +00:00
committed by Bianca Widstam
parent f5f9aba2e5
commit 0c7836fa59
33 changed files with 881 additions and 510 deletions

View File

@@ -86,6 +86,7 @@ export function LinkedReservation({
checkOutTime={checkOutTime}
img={data.room.images[0]}
roomName={data.room.name}
roomNumber={roomIndex + 1}
/>
)
}

View File

@@ -0,0 +1,10 @@
.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;
}

View File

@@ -0,0 +1,118 @@
"use client"
import { Button as ButtonRAC, DialogTrigger } from "react-aria-components"
import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { BookingStatusEnum } from "@/constants/booking"
import { trpc } from "@/lib/trpc/client"
import { getBookedHotelRoom } from "@/server/routers/booking/utils"
import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
import { convertToChildType } from "@/components/HotelReservation/utils/convertToChildType"
import { getPriceType } from "@/components/HotelReservation/utils/getPriceType"
import BookedRoomSidePeek from "@/components/SidePeeks/BookedRoomSidePeek"
import styles from "./RoomDetailsSidePeek.module.css"
import type { BreakfastPackage } from "@/types/components/hotelReservation/breakfast"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import { PackageTypeEnum } from "@/types/enums/packages"
import type { BookingConfirmationSchema } from "@/types/trpc/routers/booking/confirmation"
interface RoomDetailsSidePeekProps {
booking: BookingConfirmationSchema
roomNumber?: number
}
export default function RoomDetailsSidePeek({
booking,
roomNumber = 1,
}: RoomDetailsSidePeekProps) {
const intl = useIntl()
const user = trpc.user.getSafely.useQuery()
const roomCategories = useBookingConfirmationStore(
(state) => state.roomCategories
)
const hotelRoom = getBookedHotelRoom(roomCategories, booking.roomTypeCode)
const breakfastPackage = booking.packages.find(
(pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST
)
const breakfast: Omit<BreakfastPackage, "requestedPrice"> | null =
breakfastPackage
? {
code: breakfastPackage.code,
description: breakfastPackage.description,
localPrice: {
currency: breakfastPackage.currency,
price: breakfastPackage.unitPrice,
totalPrice: breakfastPackage.totalPrice,
},
packageType: PackageTypeEnum.BreakfastAdult,
}
: null
const childrenInRoom = convertToChildType(
booking.childrenAges,
booking.childBedPreferences
)
const priceType = getPriceType(
booking.cheques,
booking.roomPoints,
booking.vouchers
)
const featuresPackages = booking.packages.filter(
(pkg) =>
pkg.code === RoomPackageCodeEnum.PET_ROOM ||
pkg.code === RoomPackageCodeEnum.ALLERGY_ROOM ||
pkg.code === RoomPackageCodeEnum.ACCESSIBILITY_ROOM
)
const packages = featuresPackages.map((pkg) => ({
code: pkg.code as RoomPackageCodeEnum,
description: pkg.description,
inventories: [],
itemCode: "",
localPrice: {
currency: pkg.currency,
price: pkg.unitPrice,
totalPrice: pkg.totalPrice,
},
requestedPrice: {
currency: pkg.currency,
price: pkg.unitPrice,
totalPrice: pkg.totalPrice,
},
}))
const room = {
...booking,
bedType: {
description: hotelRoom?.bedType.mainBed.description ?? "",
roomTypeCode: hotelRoom?.bedType.code ?? "",
},
breakfast,
childrenInRoom,
isCancelled: booking.reservationStatus === BookingStatusEnum.Cancelled,
packages,
priceType,
roomName: hotelRoom?.name ?? "",
roomNumber,
terms: booking.rateDefinition.cancellationText,
}
return (
<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={room}
user={user.data ?? null}
/>
</DialogTrigger>
)
}

View File

@@ -9,12 +9,13 @@ import { CancellationRuleEnum } from "@/constants/booking"
import { dt } from "@/lib/dt"
import Image from "@/components/Image"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import useLang from "@/hooks/useLang"
import RoomDetailsSidePeek from "./RoomDetailsSidePeek"
import styles from "./room.module.css"
import type { RoomProps } from "@/types/components/hotelReservation/bookingConfirmation/rooms/room"
@@ -25,6 +26,7 @@ export default function Room({
checkOutTime,
img,
roomName,
roomNumber = 1,
}: RoomProps) {
const intl = useIntl()
const lang = useLang()
@@ -113,16 +115,7 @@ export default function Room({
<Subtitle color="uiTextHighContrast" type="two">
{roomName}
</Subtitle>
<Link color="burgundy" href="" variant="icon">
{intl.formatMessage({
defaultMessage: "View room details",
})}
<MaterialIcon
icon="chevron_right"
size={20}
color="CurrentColor"
/>
</Link>
<RoomDetailsSidePeek booking={booking} roomNumber={roomNumber} />
</div>
<ul className={styles.details}>
<li className={styles.listItem}>

View File

@@ -30,7 +30,7 @@ export default async function BookingConfirmation({
if (!bookingConfirmation) {
return notFound()
}
const { booking, hotel, room } = bookingConfirmation
const { booking, hotel, room, roomCategories } = bookingConfirmation
if (!room) {
return notFound()
}
@@ -46,6 +46,7 @@ export default async function BookingConfirmation({
currencyCode={booking.currencyCode}
fromDate={booking.checkInDate}
toDate={booking.checkOutDate}
roomCategories={roomCategories}
rooms={[
mapRoomState(booking, room, intl),
// null represents "known but not yet fetched rooms" and is used to render placeholders correctly

View File

@@ -1,315 +0,0 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useRouter } from "next/navigation"
import { useState } from "react"
import { Dialog } from "react-aria-components"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { trpc } from "@/lib/trpc/client"
import MembershipLevelIcon from "@/components/Levels/Icon"
import Modal from "@/components/Modal"
import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions"
import Button from "@/components/TempDesignSystem/Button"
import { toast } from "@/components/TempDesignSystem/Toasts"
import useLang from "@/hooks/useLang"
import ModifyContact from "../ModifyContact"
import styles from "./guestDetails.module.css"
import {
type ModifyContactSchema,
modifyContactSchema,
} from "@/types/components/hotelReservation/myStay/modifyContact"
import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay"
import type { Room } from "@/types/stores/my-stay"
import type { SafeUser } from "@/types/user"
interface DetailsProps {
booking: Room
user: SafeUser
}
export default function Details({ booking, user }: DetailsProps) {
const intl = useIntl()
const lang = useLang()
const router = useRouter()
const utils = trpc.useUtils()
const [currentStep, setCurrentStep] = useState(MODAL_STEPS.INITIAL)
const [isLoading, setIsLoading] = useState(false)
const [isModifyGuestDetailsOpen, setIsModifyGuestDetailsOpen] =
useState(false)
const form = useForm<ModifyContactSchema>({
resolver: zodResolver(modifyContactSchema),
defaultValues: {
firstName: booking.guest.firstName,
lastName: booking.guest.lastName,
email: booking.guest.email,
phoneNumber: booking.guest.phoneNumber,
countryCode: booking.guest.countryCode,
},
})
const isFirstStep = currentStep === MODAL_STEPS.INITIAL
const isMemberBooking =
booking.guest.membershipNumber === user?.membership?.membershipNumber
const updateGuest = trpc.booking.update.useMutation({
onMutate: () => setIsLoading(true),
onSuccess: (data) => {
if (data) {
utils.booking.get.invalidate({
confirmationNumber: data.confirmationNumber,
})
toast.success(
intl.formatMessage({
defaultMessage: "Guest details updated",
})
)
setIsModifyGuestDetailsOpen(false)
setCurrentStep(MODAL_STEPS.INITIAL)
} else {
toast.error(
intl.formatMessage({
defaultMessage: "Failed to update guest details",
})
)
}
},
onError: () => {
toast.error(
intl.formatMessage({
defaultMessage: "Failed to update guest details",
})
)
},
onSettled: () => {
setIsLoading(false)
},
})
async function onSubmit(data: ModifyContactSchema) {
updateGuest.mutate({
confirmationNumber: booking.confirmationNumber,
guest: {
email: data.email,
phoneNumber: data.phoneNumber,
countryCode: data.countryCode,
},
})
}
function handleModifyMemberDetails() {
const expirationTime = Date.now() + 10 * 60 * 1000
sessionStorage.setItem(
"myStayReturnRoute",
JSON.stringify({
path: window.location.href,
expiry: expirationTime,
})
)
router.push(`/${lang}/scandic-friends/my-pages/profile/edit`)
}
return (
<div className={styles.guestDetails}>
{isMemberBooking && user.membership && (
<div className={styles.userDetails}>
<div className={styles.userDetailsTitle}>
<Typography variant="Title/Overline/sm">
<p>
{intl.formatMessage({
defaultMessage: "Your member tier",
})}
</p>
</Typography>
</div>
<div className={styles.memberLevel}>
<MembershipLevelIcon
level={user.membership.membershipLevel}
color="red"
rows={1}
className={styles.memberLevelIcon}
/>
</div>
<div className={styles.totalPoints}>
<div className={styles.totalPointsText}>
<MaterialIcon icon="diamond" color="Icon/Intense" />
<Typography variant="Title/Overline/sm">
<p>
{intl.formatMessage({
defaultMessage: "My total points",
})}
</p>
</Typography>
</div>
<Typography variant="Body/Paragraph/mdRegular">
<p>{user.membership.currentPoints}</p>
</Typography>
</div>
</div>
)}
<div className={styles.guest}>
<Typography variant="Body/Paragraph/mdBold">
<p>
{booking.guest.firstName} {booking.guest.lastName}
</p>
</Typography>
{isMemberBooking && user.membership && (
<Typography variant="Body/Paragraph/mdRegular">
<p className={styles.memberNumber}>
{intl.formatMessage(
{
defaultMessage: "Member no. {nr}",
},
{
nr: user.membership.membershipNumber,
}
)}
</p>
</Typography>
)}
<div className={styles.contactInfoMobile}>
<Typography variant="Body/Supporting text (caption)/smRegular">
<p color="uiTextHighContrast">{booking.guest.email}</p>
</Typography>
<Typography variant="Body/Supporting text (caption)/smRegular">
<p color="uiTextHighContrast">{booking.guest.phoneNumber}</p>
</Typography>
</div>
<div className={styles.contactInfoDesktop}>
<Typography variant="Body/Paragraph/mdRegular">
<p color="uiTextHighContrast">{booking.guest.email}</p>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
<p color="uiTextHighContrast">{booking.guest.phoneNumber}</p>
</Typography>
</div>
</div>
{isMemberBooking ? (
<Button
variant="icon"
color="burgundy"
intent={"secondary"}
onClick={handleModifyMemberDetails}
disabled={booking.isCancelled}
size="small"
>
<MaterialIcon
icon="edit"
color="Icon/Interactive/Default"
size={20}
/>
<Typography variant="Body/Paragraph/mdRegular">
<span>
{intl.formatMessage({
defaultMessage: "Modify guest details",
})}
</span>
</Typography>
</Button>
) : (
<>
<Button
variant="icon"
color="burgundy"
intent="secondary"
onClick={() =>
setIsModifyGuestDetailsOpen(!isModifyGuestDetailsOpen)
}
disabled={booking.isCancelled}
size="small"
>
<MaterialIcon
icon="edit"
color={
booking.isCancelled
? "Icon/Interactive/Disabled"
: "Icon/Interactive/Default"
}
size={20}
/>
<Typography variant="Body/Paragraph/mdRegular">
<span>
{intl.formatMessage({
defaultMessage: "Modify guest details",
})}
</span>
</Typography>
</Button>
{isModifyGuestDetailsOpen && (
<Modal
withActions
hideHeader
isOpen={isModifyGuestDetailsOpen}
onToggle={setIsModifyGuestDetailsOpen}
>
<Dialog
aria-label={intl.formatMessage({
defaultMessage: "Modify guest details",
})}
>
{({ close }) => (
<FormProvider {...form}>
<ModalContentWithActions
title={intl.formatMessage({
defaultMessage: "Modify guest details",
})}
onClose={() => setIsModifyGuestDetailsOpen(false)}
content={
booking.guest && (
<ModifyContact
guest={booking.guest}
isFirstStep={isFirstStep}
/>
)
}
primaryAction={{
label: isFirstStep
? intl.formatMessage({
defaultMessage: "Save updates",
})
: intl.formatMessage({
defaultMessage: "Confirm",
}),
onClick: isFirstStep
? () => setCurrentStep(MODAL_STEPS.CONFIRMATION)
: () => form.handleSubmit(onSubmit)(),
disabled: !form.formState.isValid || isLoading,
intent: isFirstStep ? "secondary" : "primary",
}}
secondaryAction={{
label: isFirstStep
? intl.formatMessage({
defaultMessage: "Back",
})
: intl.formatMessage({
defaultMessage: "Cancel",
}),
onClick: () => {
close()
setCurrentStep(MODAL_STEPS.INITIAL)
},
}}
/>
</FormProvider>
)}
</Dialog>
</Modal>
)}
</>
)}
</div>
)
}

View File

@@ -1,22 +1,322 @@
"use client"
import { useMyStayStore } from "@/stores/my-stay"
import { zodResolver } from "@hookform/resolvers/zod"
import { useRouter } from "next/navigation"
import { useState } from "react"
import { Dialog } from "react-aria-components"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import Details from "./Details"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import type { Room } from "@/types/stores/my-stay"
import { trpc } from "@/lib/trpc/client"
import MembershipLevelIcon from "@/components/Levels/Icon"
import Modal from "@/components/Modal"
import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions"
import Button from "@/components/TempDesignSystem/Button"
import { toast } from "@/components/TempDesignSystem/Toasts"
import useLang from "@/hooks/useLang"
import ModifyContact from "../ModifyContact"
import styles from "./guestDetails.module.css"
import {
type ModifyContactSchema,
modifyContactSchema,
} from "@/types/components/hotelReservation/myStay/modifyContact"
import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay"
import type { SafeUser } from "@/types/user"
import type { Guest } from "@/server/routers/booking/output"
interface GuestDetailsProps {
selectedRoom?: Room
confirmationNumber: string
guest: Guest
isCancelled: boolean
user: SafeUser
}
export default function GuestDetails({
selectedRoom,
confirmationNumber,
guest,
isCancelled,
user,
}: GuestDetailsProps) {
const booking = useMyStayStore((state) => state.bookedRoom)
const room = selectedRoom ? selectedRoom : booking
const intl = useIntl()
const lang = useLang()
const router = useRouter()
const utils = trpc.useUtils()
const [currentStep, setCurrentStep] = useState(MODAL_STEPS.INITIAL)
const [isLoading, setIsLoading] = useState(false)
return <Details booking={room} user={user} />
const [isModifyGuestDetailsOpen, setIsModifyGuestDetailsOpen] =
useState(false)
const form = useForm<ModifyContactSchema>({
resolver: zodResolver(modifyContactSchema),
defaultValues: {
firstName: guest.firstName,
lastName: guest.lastName,
email: guest.email,
phoneNumber: guest.phoneNumber,
countryCode: guest.countryCode,
},
})
const isFirstStep = currentStep === MODAL_STEPS.INITIAL
const isMemberBooking =
guest.membershipNumber === user?.membership?.membershipNumber
const updateGuest = trpc.booking.update.useMutation({
onMutate: () => setIsLoading(true),
onSuccess: (data) => {
if (data) {
utils.booking.get.invalidate({
confirmationNumber: data.confirmationNumber,
})
toast.success(
intl.formatMessage({
defaultMessage: "Guest details updated",
})
)
setIsModifyGuestDetailsOpen(false)
setCurrentStep(MODAL_STEPS.INITIAL)
} else {
toast.error(
intl.formatMessage({
defaultMessage: "Failed to update guest details",
})
)
}
},
onError: () => {
toast.error(
intl.formatMessage({
defaultMessage: "Failed to update guest details",
})
)
},
onSettled: () => {
setIsLoading(false)
},
})
async function onSubmit(data: ModifyContactSchema) {
updateGuest.mutate({
confirmationNumber,
guest: {
email: data.email,
phoneNumber: data.phoneNumber,
countryCode: data.countryCode,
},
})
}
function handleModifyMemberDetails() {
const expirationTime = Date.now() + 10 * 60 * 1000
sessionStorage.setItem(
"myStayReturnRoute",
JSON.stringify({
path: window.location.href,
expiry: expirationTime,
})
)
router.push(`/${lang}/scandic-friends/my-pages/profile/edit`)
}
return (
<div className={styles.guestDetails}>
{isMemberBooking && user.membership && (
<div className={styles.userDetails}>
<div className={styles.userDetailsTitle}>
<Typography variant="Title/Overline/sm">
<p>
{intl.formatMessage({
defaultMessage: "Your member tier",
})}
</p>
</Typography>
</div>
<div className={styles.memberLevel}>
<MembershipLevelIcon
level={user.membership.membershipLevel}
color="red"
rows={1}
className={styles.memberLevelIcon}
/>
</div>
<div className={styles.totalPoints}>
<div className={styles.totalPointsText}>
<MaterialIcon icon="diamond" color="Icon/Intense" />
<Typography variant="Title/Overline/sm">
<p>
{intl.formatMessage({
defaultMessage: "My total points",
})}
</p>
</Typography>
</div>
<Typography variant="Body/Paragraph/mdRegular">
<p>{user.membership.currentPoints}</p>
</Typography>
</div>
</div>
)}
<div className={styles.guest}>
<Typography variant="Body/Paragraph/mdBold">
<p>
{guest.firstName} {guest.lastName}
</p>
</Typography>
{isMemberBooking && user.membership && (
<Typography variant="Body/Paragraph/mdRegular">
<p className={styles.memberNumber}>
{intl.formatMessage(
{
defaultMessage: "Member no. {nr}",
},
{
nr: user.membership.membershipNumber,
}
)}
</p>
</Typography>
)}
<div className={styles.contactInfoMobile}>
<Typography variant="Body/Supporting text (caption)/smRegular">
<p color="uiTextHighContrast">{guest.email}</p>
</Typography>
<Typography variant="Body/Supporting text (caption)/smRegular">
<p color="uiTextHighContrast">{guest.phoneNumber}</p>
</Typography>
</div>
<div className={styles.contactInfoDesktop}>
<Typography variant="Body/Paragraph/mdRegular">
<p color="uiTextHighContrast">{guest.email}</p>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
<p color="uiTextHighContrast">{guest.phoneNumber}</p>
</Typography>
</div>
</div>
{isMemberBooking ? (
<Button
variant="icon"
color="burgundy"
intent="secondary"
onClick={handleModifyMemberDetails}
disabled={isCancelled}
size="small"
>
<MaterialIcon
icon="edit"
color="Icon/Interactive/Default"
size={20}
/>
<Typography variant="Body/Paragraph/mdRegular">
<span>
{intl.formatMessage({
defaultMessage: "Modify guest details",
})}
</span>
</Typography>
</Button>
) : (
<>
<Button
variant="icon"
color="burgundy"
intent="secondary"
onClick={() =>
setIsModifyGuestDetailsOpen(!isModifyGuestDetailsOpen)
}
disabled={isCancelled}
size="small"
>
<MaterialIcon
icon="edit"
color={
isCancelled
? "Icon/Interactive/Disabled"
: "Icon/Interactive/Default"
}
size={20}
/>
<Typography variant="Body/Paragraph/mdRegular">
<span>
{intl.formatMessage({
defaultMessage: "Modify guest details",
})}
</span>
</Typography>
</Button>
{isModifyGuestDetailsOpen && (
<Modal
withActions
hideHeader
isOpen={isModifyGuestDetailsOpen}
onToggle={setIsModifyGuestDetailsOpen}
>
<Dialog
aria-label={intl.formatMessage({
defaultMessage: "Modify guest details",
})}
>
{({ close }) => (
<FormProvider {...form}>
<ModalContentWithActions
title={intl.formatMessage({
defaultMessage: "Modify guest details",
})}
onClose={() => setIsModifyGuestDetailsOpen(false)}
content={
guest && (
<ModifyContact
guest={guest}
isFirstStep={isFirstStep}
/>
)
}
primaryAction={{
label: isFirstStep
? intl.formatMessage({
defaultMessage: "Save updates",
})
: intl.formatMessage({
defaultMessage: "Confirm",
}),
onClick: isFirstStep
? () => setCurrentStep(MODAL_STEPS.CONFIRMATION)
: form.handleSubmit(onSubmit),
disabled: !form.formState.isValid || isLoading,
intent: isFirstStep ? "secondary" : "primary",
}}
secondaryAction={{
label: isFirstStep
? intl.formatMessage({
defaultMessage: "Back",
})
: intl.formatMessage({
defaultMessage: "Cancel",
}),
onClick: () => {
close()
setCurrentStep(MODAL_STEPS.INITIAL)
},
}}
/>
</FormProvider>
)}
</Dialog>
</Modal>
)}
</>
)}
</div>
)
}

View File

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

View File

@@ -1,13 +1,16 @@
"use client"
import { Button as ButtonRAC, DialogTrigger } from "react-aria-components"
import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { dt } from "@/lib/dt"
import { getBookedHotelRoom } from "@/server/routers/booking/utils"
import { IconForFeatureCode } from "@/components/HotelReservation/utils"
import Image from "@/components/Image"
import BookedRoomSidePeek from "@/components/SidePeeks/BookedRoomSidePeek"
import Divider from "@/components/TempDesignSystem/Divider"
import IconChip from "@/components/TempDesignSystem/IconChip"
import useLang from "@/hooks/useLang"
@@ -15,11 +18,11 @@ import { formatPrice } from "@/utils/numberFormatting"
import PriceType from "../../PriceType"
import { hasModifiableRate } from "../../utils"
import ToggleSidePeek from "./ToggleSidePeek"
import styles from "./multiRoom.module.css"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { RoomCategories } from "@/types/hotel"
import type { Room } from "@/types/stores/my-stay"
import type { SafeUser } from "@/types/user"
@@ -27,9 +30,15 @@ interface MultiRoomProps {
booking: Room
roomNr: number
user: SafeUser
roomCategories: RoomCategories
}
export default function MultiRoom({ booking, roomNr, user }: MultiRoomProps) {
export default function MultiRoom({
booking,
roomNr,
user,
roomCategories,
}: MultiRoomProps) {
const intl = useIntl()
const lang = useLang()
@@ -42,7 +51,6 @@ export default function MultiRoom({ booking, roomNr, user }: MultiRoomProps) {
childrenAges,
confirmationNumber,
currencyCode,
hotelId,
packages,
rateDefinition,
room,
@@ -94,6 +102,7 @@ export default function MultiRoom({ booking, roomNr, user }: MultiRoomProps) {
breakfast.localPrice.currency
)
}
const hotelRoom = getBookedHotelRoom(roomCategories, roomTypeCode)
return (
<article className={styles.multiRoom}>
@@ -167,12 +176,21 @@ export default function MultiRoom({ booking, roomNr, user }: MultiRoomProps) {
</Typography>
</div>
<div className={styles.toggleSidePeek}>
<ToggleSidePeek
hotelId={hotelId}
roomTypeCode={roomTypeCode}
user={user}
confirmationNumber={confirmationNumber}
/>
<DialogTrigger>
<ButtonRAC
aria-label={intl.formatMessage({
defaultMessage: "View room details",
})}
className={styles.iconContainer}
>
<MaterialIcon icon="pan_zoom" color="CurrentColor" />
</ButtonRAC>
<BookedRoomSidePeek
hotelRoom={hotelRoom}
room={booking}
user={user}
/>
</DialogTrigger>
</div>
</div>
<div

View File

@@ -31,6 +31,13 @@
position: relative;
}
.iconContainer {
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);
}

View File

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

View File

@@ -1,4 +1,5 @@
"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"
@@ -6,6 +7,7 @@ import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { dt } from "@/lib/dt"
import { getBookedHotelRoom } from "@/server/routers/booking/utils"
import { useMyStayStore } from "@/stores/my-stay"
import GuestDetails from "@/components/HotelReservation/MyStay/GuestDetails"
@@ -14,26 +16,31 @@ import PriceType from "@/components/HotelReservation/MyStay/PriceType"
import { hasModifiableRate } from "@/components/HotelReservation/MyStay/utils"
import { IconForFeatureCode } from "@/components/HotelReservation/utils"
import Image from "@/components/Image"
import BookedRoomSidePeek from "@/components/SidePeeks/BookedRoomSidePeek"
import SkeletonShimmer from "@/components/SkeletonShimmer"
import IconChip from "@/components/TempDesignSystem/IconChip"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import ToggleSidePeek from "./ToggleSidePeek"
import styles from "./room.module.css"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { Room } from "@/types/hotel"
import type { Room, RoomCategories } from "@/types/hotel"
import type { SafeUser } from "@/types/user"
interface RoomProps {
bedType: Room["roomTypes"][number]
image: Room["images"][number]
user: SafeUser
roomCategories: RoomCategories
}
export default function SingleRoom({ bedType, image, user }: RoomProps) {
export default function SingleRoom({
bedType,
image,
user,
roomCategories,
}: RoomProps) {
const intl = useIntl()
const lang = useLang()
@@ -46,7 +53,7 @@ export default function SingleRoom({ bedType, image, user }: RoomProps) {
childrenAges,
confirmationNumber,
formattedTotalPrice,
hotel,
guest,
isCancelled,
packages,
priceType,
@@ -57,10 +64,12 @@ export default function SingleRoom({ bedType, image, user }: RoomProps) {
roomTypeCode,
totalPrice,
vouchers,
bookedRoom,
} = useMyStayStore((state) => ({
adults: state.bookedRoom.adults,
bookingCode: state.bookedRoom.bookingCode,
breakfast: state.bookedRoom.breakfast,
guest: state.bookedRoom.guest,
checkInDate: state.bookedRoom.checkInDate,
cheques: state.bookedRoom.cheques,
childrenAges: state.bookedRoom.childrenAges,
@@ -77,6 +86,7 @@ export default function SingleRoom({ bedType, image, user }: RoomProps) {
roomTypeCode: state.bookedRoom.roomTypeCode,
totalPrice: state.bookedRoom.totalPrice,
vouchers: state.bookedRoom.vouchers,
bookedRoom: state.bookedRoom,
}))
if (!roomNumber) {
@@ -149,6 +159,8 @@ export default function SingleRoom({ bedType, image, user }: RoomProps) {
)
}
const hotelRoom = getBookedHotelRoom(roomCategories, roomTypeCode)
return (
<div>
<article className={styles.room}>
@@ -187,11 +199,27 @@ export default function SingleRoom({ bedType, image, user }: RoomProps) {
</div>
</div>
<div className={styles.sidePeek}>
<ToggleSidePeek
hotelId={hotel.operaId}
roomTypeCode={roomTypeCode}
intent="text"
/>
<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}>
@@ -401,7 +429,12 @@ export default function SingleRoom({ bedType, image, user }: RoomProps) {
</div>
</div>
<div className={styles.guestDetailsDesktopWrapper}>
<GuestDetails user={user} />
<GuestDetails
confirmationNumber={confirmationNumber}
guest={guest}
isCancelled={isCancelled}
user={user}
/>
</div>
</div>
</div>
@@ -454,7 +487,12 @@ export default function SingleRoom({ bedType, image, user }: RoomProps) {
<PriceDetails />
<div className={styles.guestDetailsMobileWrapper}>
<GuestDetails user={user} />
<GuestDetails
confirmationNumber={confirmationNumber}
guest={guest}
isCancelled={isCancelled}
user={user}
/>
</div>
</article>
</div>

View File

@@ -6,6 +6,17 @@
padding: var(--Spacing-x3) 0;
}
.trigger {
align-items: center;
background: none;
border: none;
color: var(--Component-Button-Brand-Secondary-On-fill-Default);
cursor: pointer;
display: flex;
gap: var(--Space-x1);
padding: var(--Space-x025) 0;
}
.roomName {
color: var(--Scandic-Brand-Burgundy);
padding: 0 var(--Spacing-x2);

View File

@@ -20,12 +20,15 @@ interface RoomsProps {
export default function Rooms({ user }: RoomsProps) {
const intl = useIntl()
const { allRoomsAreCancelled, room, rooms } = useMyStayStore((state) => ({
allRoomsAreCancelled: state.allRoomsAreCancelled,
hotel: state.hotel,
room: state.bookedRoom.room,
rooms: state.rooms,
}))
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
@@ -50,6 +53,7 @@ export default function Rooms({ user }: RoomsProps) {
bedType={room.bedType}
image={room.images[0]}
user={user}
roomCategories={roomCategories}
/>
) : (
<div className={styles.roomsContainer}>
@@ -58,7 +62,12 @@ export default function Rooms({ user }: RoomsProps) {
key={booking.confirmationNumber}
className={styles.roomWrapper}
>
<MultiRoom booking={booking} roomNr={index + 1} user={user} />
<MultiRoom
booking={booking}
roomNr={index + 1}
user={user}
roomCategories={roomCategories}
/>
</div>
))}
</div>

View File

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

View File

@@ -13,8 +13,6 @@ import type { ToggleSidePeekProps } from "@/types/components/hotelReservation/to
export default function ToggleSidePeek({
hotelId,
roomTypeCode,
intent = "textInverted",
title,
}: ToggleSidePeekProps) {
const intl = useIntl()
const openSidePeek = useSidePeekStore((state) => state.openSidePeek)
@@ -22,19 +20,19 @@ export default function ToggleSidePeek({
return (
<Button
onClick={() =>
openSidePeek({ key: SidePeekEnum.roomDetails, hotelId, roomTypeCode })
openSidePeek({
key: SidePeekEnum.roomDetails,
hotelId,
roomTypeCode,
})
}
theme="base"
intent="text"
size="small"
theme="base"
variant="icon"
intent={intent}
wrapping
>
{title
? title
: intl.formatMessage({
defaultMessage: "See room details",
})}
{intl.formatMessage({ defaultMessage: "Room details" })}
<MaterialIcon icon="chevron_right" size={14} />
</Button>
)

View File

@@ -51,14 +51,7 @@ export default function Details({ roomTypeCode }: { roomTypeCode: string }) {
<RoomSize roomSize={roomSize} />
<div className={styles.toggleSidePeek}>
{roomTypeCode && (
<ToggleSidePeek
hotelId={hotelId}
roomTypeCode={roomTypeCode}
title={intl.formatMessage({
defaultMessage: "Room details",
})}
intent="text"
/>
<ToggleSidePeek hotelId={hotelId} roomTypeCode={roomTypeCode} />
)}
</div>
</div>

View File

@@ -4,27 +4,19 @@ import { trpc } from "@/lib/trpc/client"
import useSidePeekStore from "@/stores/sidepeek"
import AmenitiesSidePeek from "@/components/SidePeeks/AmenitiesSidePeek"
import BookedRoomSidePeek from "@/components/SidePeeks/BookedRoomSidePeek"
import HotelSidePeek from "@/components/SidePeeks/HotelSidePeek"
import RoomSidePeek from "@/components/SidePeeks/RoomSidePeek"
import useLang from "@/hooks/useLang"
export default function HotelReservationSidePeek() {
const {
activeSidePeek,
confirmationNumber,
hotelId,
roomTypeCode,
showCTA,
user,
} = useSidePeekStore((state) => ({
activeSidePeek: state.activeSidePeek,
confirmationNumber: state.confirmationNumber,
hotelId: state.hotelId,
roomTypeCode: state.roomTypeCode,
showCTA: state.showCTA,
user: state.user,
}))
const { activeSidePeek, hotelId, roomTypeCode, showCTA } = useSidePeekStore(
(state) => ({
activeSidePeek: state.activeSidePeek,
hotelId: state.hotelId,
roomTypeCode: state.roomTypeCode,
showCTA: state.showCTA,
})
)
const close = useSidePeekStore((state) => state.closeSidePeek)
const lang = useLang()
@@ -72,15 +64,6 @@ export default function HotelReservationSidePeek() {
close={close}
/>
)}
{selectedRoom && (
<BookedRoomSidePeek
room={selectedRoom}
activeSidePeek={activeSidePeek}
close={close}
user={user}
confirmationNumber={confirmationNumber}
/>
)}
</>
)
}

View File

@@ -5,7 +5,6 @@ import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { dt } from "@/lib/dt"
import { useMyStayStore } from "@/stores/my-stay"
import GuestDetails from "@/components/HotelReservation/MyStay/GuestDetails"
import PriceType from "@/components/HotelReservation/MyStay/PriceType"
@@ -14,7 +13,7 @@ import ImageGallery from "@/components/ImageGallery"
import Accordion from "@/components/TempDesignSystem/Accordion"
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
import IconChip from "@/components/TempDesignSystem/IconChip"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import SidePeekSelfControlled from "@/components/TempDesignSystem/SidePeekSelfControlled"
import useLang from "@/hooks/useLang"
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
import { formatPrice } from "@/utils/numberFormatting"
@@ -23,28 +22,60 @@ import RoomDetails from "./RoomDetails"
import styles from "./bookedRoomSidePeek.module.css"
import type { BreakfastPackage } from "@/types/components/hotelReservation/breakfast"
import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
import type { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
import type { BookedRoomSidePeekProps } from "@/types/components/sidePeeks/bookedRoomSidePeek"
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { Room as HotelRoom } from "@/types/hotel"
import type { Packages } from "@/types/requests/packages"
import type { BookingConfirmationSchema } from "@/types/trpc/routers/booking/confirmation"
import type { SafeUser } from "@/types/user"
export type PartialHotelRoom = Pick<
HotelRoom,
"descriptions" | "images" | "name" | "roomFacilities" | "roomTypes"
>
export type Room = Pick<
BookingConfirmationSchema,
| "adults"
| "bookingCode"
| "cancellationNumber"
| "checkInDate"
| "cheques"
| "confirmationNumber"
| "currencyCode"
| "guest"
| "rateDefinition"
| "roomPoints"
| "totalPrice"
| "vouchers"
> & {
bedType: BedTypeSchema
breakfast: Omit<BreakfastPackage, "requestedPrice"> | null
childrenInRoom: Child[]
isCancelled: boolean
packages: Packages | null
priceType: PriceTypeEnum
roomName: string
roomNumber: number
terms: string | null
}
interface RoomDetailsSidePeekProps {
hotelRoom: PartialHotelRoom | null
room: Room
user: SafeUser
}
export default function BookedRoomSidePeek({
hotelRoom,
room,
activeSidePeek,
user,
confirmationNumber,
close,
}: BookedRoomSidePeekProps) {
}: RoomDetailsSidePeekProps) {
const intl = useIntl()
const lang = useLang()
const rooms = useMyStayStore((state) => state.rooms)
const bookingRoom = rooms.find(
(r) => r.confirmationNumber === confirmationNumber
)
if (!bookingRoom) {
return null
}
const {
adults,
@@ -53,20 +84,28 @@ export default function BookedRoomSidePeek({
breakfast,
cancellationNumber,
checkInDate,
cheques,
childrenInRoom,
confirmationNumber,
currencyCode,
guest,
isCancelled,
roomName,
packages,
priceType,
rateDefinition,
roomNumber,
roomPoints,
terms,
totalPrice,
} = bookingRoom
vouchers,
} = room
const fromDate = dt(checkInDate).locale(lang)
const roomDescription = room.descriptions.medium
const galleryImages = mapApiImagesToGalleryImages(room.images)
const galleryImages = hotelRoom
? mapApiImagesToGalleryImages(hotelRoom.images)
: null
const adultsMsg = intl.formatMessage(
{
@@ -106,12 +145,10 @@ export default function BookedRoomSidePeek({
)
}
const hotelRoomName = hotelRoom?.name || roomName
return (
<SidePeek
title={room.name}
isOpen={activeSidePeek === SidePeekEnum.bookedRoomDetails}
handleClose={close}
>
<SidePeekSelfControlled title={hotelRoomName}>
<div className={styles.wrapper}>
<div className={styles.roomHeader}>
{isCancelled ? (
@@ -180,11 +217,13 @@ export default function BookedRoomSidePeek({
</div>
<div className={styles.mainContent}>
<div className={styles.imageContainer}>
<ImageGallery
images={galleryImages}
title={room.name}
height={280}
/>
{galleryImages ? (
<ImageGallery
height={280}
images={galleryImages}
title={hotelRoomName}
/>
) : null}
</div>
<div className={styles.roomDetails}>
<div className={styles.row}>
@@ -334,14 +373,14 @@ export default function BookedRoomSidePeek({
</Typography>
<PriceType
cheques={bookingRoom.cheques}
cheques={cheques}
formattedTotalPrice={formattedTotalPrice}
isCancelled={bookingRoom.isCancelled}
priceType={bookingRoom.priceType}
rateDefinition={bookingRoom.rateDefinition}
roomPoints={bookingRoom.roomPoints}
totalPrice={bookingRoom.totalPrice}
vouchers={bookingRoom.vouchers}
isCancelled={isCancelled}
priceType={priceType}
rateDefinition={rateDefinition}
roomPoints={roomPoints}
totalPrice={totalPrice}
vouchers={vouchers}
/>
</div>
</div>
@@ -369,23 +408,30 @@ export default function BookedRoomSidePeek({
</Typography>
)}
<GuestDetails selectedRoom={bookingRoom} user={user} />
<GuestDetails
confirmationNumber={confirmationNumber}
guest={guest}
isCancelled={isCancelled}
user={user}
/>
</div>
<Accordion>
<AccordionItem
title={intl.formatMessage({
defaultMessage: "Room details",
})}
variant="sidepeek"
>
<RoomDetails
roomDescription={roomDescription}
roomFacilities={room.roomFacilities}
roomTypes={room.roomTypes}
/>
</AccordionItem>
</Accordion>
{hotelRoom ? (
<Accordion>
<AccordionItem
title={intl.formatMessage({
defaultMessage: "Room details",
})}
variant="sidepeek"
>
<RoomDetails
roomDescription={hotelRoom.descriptions.medium}
roomFacilities={hotelRoom.roomFacilities}
roomTypes={hotelRoom.roomTypes}
/>
</AccordionItem>
</Accordion>
) : null}
</div>
</SidePeek>
</SidePeekSelfControlled>
)
}

View File

@@ -0,0 +1,15 @@
import type { SidePeekSelfControlledProps } from "./sidePeek"
// 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.
// This component is used to provide SEO information for the sidepeek content.
export default function SidePeekSEO({
title,
children,
}: React.PropsWithChildren<Pick<SidePeekSelfControlledProps, "title">>) {
return (
<div className="sr-only">
<h2>{title}</h2>
{children}
</div>
)
}

View File

@@ -0,0 +1,68 @@
"use client"
import { useEffect } from "react"
import { Dialog, Modal, ModalOverlay } from "react-aria-components"
import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import useSetOverflowVisibleOnRA from "@/hooks/useSetOverflowVisibleOnRA"
import Button from "../Button"
import SidePeekSEO from "./SidePeekSEO"
import styles from "./sidePeekSelfControlled.module.css"
import type { SidePeekSelfControlledProps } from "./sidePeek"
export default function SidePeekSelfControlled({
children,
title,
}: React.PropsWithChildren<SidePeekSelfControlledProps>) {
const intl = useIntl()
return (
<>
<ModalOverlay className={styles.overlay} isDismissable>
<Modal className={styles.modal}>
<Dialog className={styles.dialog} aria-label={title}>
{({ close }) => (
<aside className={styles.sidePeek}>
<header className={styles.header}>
{title ? (
<Typography variant="Title/md" className={styles.heading}>
<h2>{title}</h2>
</Typography>
) : null}
<Button
aria-label={intl.formatMessage({
defaultMessage: "Close",
})}
className={styles.closeButton}
intent="text"
onPress={close}
>
<MaterialIcon
icon="close"
color="Icon/Interactive/Default"
/>
</Button>
</header>
<div className={styles.sidePeekContent}>{children}</div>
<KeepBodyVisible />
</aside>
)}
</Dialog>
</Modal>
</ModalOverlay>
<SidePeekSEO title={title}>{children}</SidePeekSEO>
</>
)
}
function KeepBodyVisible() {
const toggle = useSetOverflowVisibleOnRA()
useEffect(() => {
toggle(true)
return () => toggle(false)
}, [toggle])
return null
}

View File

@@ -0,0 +1,3 @@
export interface SidePeekSelfControlledProps {
title: string
}

View File

@@ -0,0 +1,102 @@
.modal {
--sidepeek-desktop-width: 560px;
}
@keyframes slide-in {
from {
right: calc(-1 * var(--sidepeek-desktop-width));
}
to {
right: 0;
}
}
@keyframes slide-up {
from {
top: 100vh;
}
to {
top: 0;
}
}
.overlay {
position: fixed;
inset: 0;
z-index: var(--sidepeek-z-index);
background-color: var(--UI-Opacity-Almost-Black-30);
}
.modal {
position: fixed;
top: 0;
right: auto;
bottom: 0;
width: 100%;
height: 100vh;
background-color: var(--Base-Background-Primary-Normal);
z-index: var(--sidepeek-z-index);
}
.modal[data-entering] {
animation: slide-up 300ms;
}
.modal[data-exiting] {
animation: slide-up 300ms reverse;
}
.dialog {
height: 100%;
outline: none;
}
.sidePeek {
display: grid;
grid-template-rows: min-content auto;
height: 100%;
}
.header {
display: flex;
justify-content: flex-end;
border-bottom: 1px solid var(--Base-Border-Subtle);
align-items: center;
padding: var(--Spacing-x4);
}
.header:has(> h2) {
justify-content: space-between;
}
.closeButton {
padding: 0;
}
.heading {
color: var(--Text-Heading);
}
.sidePeekContent {
padding: var(--Spacing-x4);
overflow-y: auto;
}
@media screen and (min-width: 1367px) {
.modal {
top: 0;
right: 0px;
width: var(--sidepeek-desktop-width);
height: 100vh;
}
.modal[data-entering] {
animation: slide-in 250ms;
}
.modal[data-exiting] {
animation: slide-in 250ms reverse;
}
}

View File

@@ -0,0 +1,10 @@
export type SidePeekProps = {
activeContent: string | null
onClose: (isOpen: boolean) => void
}
export type SidePeekContentProps = {
title?: string
contentKey: string
isActive?: boolean
onClose?: () => void
}

View File

@@ -18,6 +18,7 @@ export default function BookingConfirmationProvider({
currencyCode,
fromDate,
toDate,
roomCategories,
rooms,
vat,
}: BookingConfirmationProviderProps) {
@@ -73,6 +74,7 @@ export default function BookingConfirmationProvider({
fromDate,
toDate,
rooms,
roomCategories,
vat,
isVatCurrency,
formattedTotalCost,

View File

@@ -15,6 +15,7 @@ export function createBookingConfirmationStore(initialState: InitialState) {
currencyCode: initialState.currencyCode,
fromDate: initialState.fromDate,
toDate: initialState.toDate,
roomCategories: initialState.roomCategories,
vat: initialState.vat,
formattedTotalCost: initialState.formattedTotalCost,
isVatCurrency: initialState.isVatCurrency,

View File

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

View File

@@ -6,4 +6,5 @@ export interface RoomProps {
checkOutTime: string
img: NonNullable<BookingConfirmation["room"]>["images"][number]
roomName: NonNullable<BookingConfirmation["room"]>["name"]
roomNumber?: number
}

View File

@@ -1,4 +1,5 @@
import type { CurrencyEnum } from "../enums/currency"
import type { RoomCategories } from "../hotel"
import type { Room } from "../stores/booking-confirmation"
export interface BookingConfirmationProviderProps
@@ -7,6 +8,7 @@ export interface BookingConfirmationProviderProps
currencyCode: CurrencyEnum
fromDate: Date
rooms: (Room | null)[]
roomCategories: RoomCategories
toDate: Date
vat: number
}

View File

@@ -1,5 +1,6 @@
import type { ChildBedTypeEnum } from "@/constants/booking"
import type { CurrencyEnum } from "../enums/currency"
import type { RoomCategories } from "../hotel"
import type {
BookingConfirmation,
PackageSchema,
@@ -43,6 +44,7 @@ export interface InitialState {
rooms: (Room | null)[]
toDate: Date
currencyCode: CurrencyEnum
roomCategories: RoomCategories
vat: number
isVatCurrency: boolean
formattedTotalCost: string

View File

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