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:
committed by
Bianca Widstam
parent
f5f9aba2e5
commit
0c7836fa59
@@ -86,6 +86,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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -9,12 +9,13 @@ import { CancellationRuleEnum } from "@/constants/booking"
|
|||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
|
|
||||||
import Image from "@/components/Image"
|
import Image from "@/components/Image"
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
import RoomDetailsSidePeek from "./RoomDetailsSidePeek"
|
||||||
|
|
||||||
import styles from "./room.module.css"
|
import styles from "./room.module.css"
|
||||||
|
|
||||||
import type { RoomProps } from "@/types/components/hotelReservation/bookingConfirmation/rooms/room"
|
import type { RoomProps } from "@/types/components/hotelReservation/bookingConfirmation/rooms/room"
|
||||||
@@ -25,6 +26,7 @@ export default function Room({
|
|||||||
checkOutTime,
|
checkOutTime,
|
||||||
img,
|
img,
|
||||||
roomName,
|
roomName,
|
||||||
|
roomNumber = 1,
|
||||||
}: RoomProps) {
|
}: RoomProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
@@ -113,16 +115,7 @@ export default function Room({
|
|||||||
<Subtitle color="uiTextHighContrast" type="two">
|
<Subtitle color="uiTextHighContrast" type="two">
|
||||||
{roomName}
|
{roomName}
|
||||||
</Subtitle>
|
</Subtitle>
|
||||||
<Link color="burgundy" href="" variant="icon">
|
<RoomDetailsSidePeek booking={booking} roomNumber={roomNumber} />
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "View room details",
|
|
||||||
})}
|
|
||||||
<MaterialIcon
|
|
||||||
icon="chevron_right"
|
|
||||||
size={20}
|
|
||||||
color="CurrentColor"
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
<ul className={styles.details}>
|
<ul className={styles.details}>
|
||||||
<li className={styles.listItem}>
|
<li className={styles.listItem}>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export default async function BookingConfirmation({
|
|||||||
if (!bookingConfirmation) {
|
if (!bookingConfirmation) {
|
||||||
return notFound()
|
return notFound()
|
||||||
}
|
}
|
||||||
const { booking, hotel, room } = bookingConfirmation
|
const { booking, hotel, room, roomCategories } = bookingConfirmation
|
||||||
if (!room) {
|
if (!room) {
|
||||||
return notFound()
|
return notFound()
|
||||||
}
|
}
|
||||||
@@ -46,6 +46,7 @@ export default async function BookingConfirmation({
|
|||||||
currencyCode={booking.currencyCode}
|
currencyCode={booking.currencyCode}
|
||||||
fromDate={booking.checkInDate}
|
fromDate={booking.checkInDate}
|
||||||
toDate={booking.checkOutDate}
|
toDate={booking.checkOutDate}
|
||||||
|
roomCategories={roomCategories}
|
||||||
rooms={[
|
rooms={[
|
||||||
mapRoomState(booking, room, intl),
|
mapRoomState(booking, room, intl),
|
||||||
// null represents "known but not yet fetched rooms" and is used to render placeholders correctly
|
// null represents "known but not yet fetched rooms" and is used to render placeholders correctly
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,322 @@
|
|||||||
"use client"
|
"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 { SafeUser } from "@/types/user"
|
||||||
|
import type { Guest } from "@/server/routers/booking/output"
|
||||||
|
|
||||||
interface GuestDetailsProps {
|
interface GuestDetailsProps {
|
||||||
selectedRoom?: Room
|
confirmationNumber: string
|
||||||
|
guest: Guest
|
||||||
|
isCancelled: boolean
|
||||||
user: SafeUser
|
user: SafeUser
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GuestDetails({
|
export default function GuestDetails({
|
||||||
selectedRoom,
|
confirmationNumber,
|
||||||
|
guest,
|
||||||
|
isCancelled,
|
||||||
user,
|
user,
|
||||||
}: GuestDetailsProps) {
|
}: GuestDetailsProps) {
|
||||||
const booking = useMyStayStore((state) => state.bookedRoom)
|
const intl = useIntl()
|
||||||
const room = selectedRoom ? selectedRoom : booking
|
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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
"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 { 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 { dt } from "@/lib/dt"
|
||||||
|
import { getBookedHotelRoom } from "@/server/routers/booking/utils"
|
||||||
|
|
||||||
import { IconForFeatureCode } from "@/components/HotelReservation/utils"
|
import { IconForFeatureCode } from "@/components/HotelReservation/utils"
|
||||||
import Image from "@/components/Image"
|
import Image from "@/components/Image"
|
||||||
|
import BookedRoomSidePeek from "@/components/SidePeeks/BookedRoomSidePeek"
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
import IconChip from "@/components/TempDesignSystem/IconChip"
|
import IconChip from "@/components/TempDesignSystem/IconChip"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
@@ -15,11 +18,11 @@ import { formatPrice } from "@/utils/numberFormatting"
|
|||||||
|
|
||||||
import PriceType from "../../PriceType"
|
import PriceType from "../../PriceType"
|
||||||
import { hasModifiableRate } from "../../utils"
|
import { hasModifiableRate } from "../../utils"
|
||||||
import ToggleSidePeek from "./ToggleSidePeek"
|
|
||||||
|
|
||||||
import styles from "./multiRoom.module.css"
|
import styles from "./multiRoom.module.css"
|
||||||
|
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
|
import type { RoomCategories } from "@/types/hotel"
|
||||||
import type { Room } from "@/types/stores/my-stay"
|
import type { Room } from "@/types/stores/my-stay"
|
||||||
import type { SafeUser } from "@/types/user"
|
import type { SafeUser } from "@/types/user"
|
||||||
|
|
||||||
@@ -27,9 +30,15 @@ interface MultiRoomProps {
|
|||||||
booking: Room
|
booking: Room
|
||||||
roomNr: number
|
roomNr: number
|
||||||
user: SafeUser
|
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 intl = useIntl()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
|
|
||||||
@@ -42,7 +51,6 @@ export default function MultiRoom({ booking, roomNr, user }: MultiRoomProps) {
|
|||||||
childrenAges,
|
childrenAges,
|
||||||
confirmationNumber,
|
confirmationNumber,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
hotelId,
|
|
||||||
packages,
|
packages,
|
||||||
rateDefinition,
|
rateDefinition,
|
||||||
room,
|
room,
|
||||||
@@ -94,6 +102,7 @@ export default function MultiRoom({ booking, roomNr, user }: MultiRoomProps) {
|
|||||||
breakfast.localPrice.currency
|
breakfast.localPrice.currency
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
const hotelRoom = getBookedHotelRoom(roomCategories, roomTypeCode)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className={styles.multiRoom}>
|
<article className={styles.multiRoom}>
|
||||||
@@ -167,12 +176,21 @@ export default function MultiRoom({ booking, roomNr, user }: MultiRoomProps) {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.toggleSidePeek}>
|
<div className={styles.toggleSidePeek}>
|
||||||
<ToggleSidePeek
|
<DialogTrigger>
|
||||||
hotelId={hotelId}
|
<ButtonRAC
|
||||||
roomTypeCode={roomTypeCode}
|
aria-label={intl.formatMessage({
|
||||||
user={user}
|
defaultMessage: "View room details",
|
||||||
confirmationNumber={confirmationNumber}
|
})}
|
||||||
/>
|
className={styles.iconContainer}
|
||||||
|
>
|
||||||
|
<MaterialIcon icon="pan_zoom" color="CurrentColor" />
|
||||||
|
</ButtonRAC>
|
||||||
|
<BookedRoomSidePeek
|
||||||
|
hotelRoom={hotelRoom}
|
||||||
|
room={booking}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
</DialogTrigger>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -31,6 +31,13 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.iconContainer {
|
||||||
|
display: flex;
|
||||||
|
border: 1px solid var(--Base-Border-Subtle);
|
||||||
|
border-radius: var(--Corner-radius-Small);
|
||||||
|
padding: var(--Spacing-x-half);
|
||||||
|
}
|
||||||
|
|
||||||
.roomName {
|
.roomName {
|
||||||
color: var(--Scandic-Brand-Burgundy);
|
color: var(--Scandic-Brand-Burgundy);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
import { Button as ButtonRAC, DialogTrigger } from "react-aria-components"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon"
|
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 { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { dt } from "@/lib/dt"
|
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"
|
||||||
@@ -14,26 +16,31 @@ import PriceType from "@/components/HotelReservation/MyStay/PriceType"
|
|||||||
import { hasModifiableRate } from "@/components/HotelReservation/MyStay/utils"
|
import { hasModifiableRate } from "@/components/HotelReservation/MyStay/utils"
|
||||||
import { IconForFeatureCode } from "@/components/HotelReservation/utils"
|
import { IconForFeatureCode } from "@/components/HotelReservation/utils"
|
||||||
import Image from "@/components/Image"
|
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 IconChip from "@/components/TempDesignSystem/IconChip"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
import { formatPrice } from "@/utils/numberFormatting"
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
import ToggleSidePeek from "./ToggleSidePeek"
|
|
||||||
|
|
||||||
import styles from "./room.module.css"
|
import styles from "./room.module.css"
|
||||||
|
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
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"
|
import type { SafeUser } from "@/types/user"
|
||||||
|
|
||||||
interface RoomProps {
|
interface RoomProps {
|
||||||
bedType: Room["roomTypes"][number]
|
bedType: Room["roomTypes"][number]
|
||||||
image: Room["images"][number]
|
image: Room["images"][number]
|
||||||
user: SafeUser
|
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 intl = useIntl()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
|
|
||||||
@@ -46,7 +53,7 @@ export default function SingleRoom({ bedType, image, user }: RoomProps) {
|
|||||||
childrenAges,
|
childrenAges,
|
||||||
confirmationNumber,
|
confirmationNumber,
|
||||||
formattedTotalPrice,
|
formattedTotalPrice,
|
||||||
hotel,
|
guest,
|
||||||
isCancelled,
|
isCancelled,
|
||||||
packages,
|
packages,
|
||||||
priceType,
|
priceType,
|
||||||
@@ -57,10 +64,12 @@ export default function SingleRoom({ bedType, image, user }: RoomProps) {
|
|||||||
roomTypeCode,
|
roomTypeCode,
|
||||||
totalPrice,
|
totalPrice,
|
||||||
vouchers,
|
vouchers,
|
||||||
|
bookedRoom,
|
||||||
} = useMyStayStore((state) => ({
|
} = useMyStayStore((state) => ({
|
||||||
adults: state.bookedRoom.adults,
|
adults: state.bookedRoom.adults,
|
||||||
bookingCode: state.bookedRoom.bookingCode,
|
bookingCode: state.bookedRoom.bookingCode,
|
||||||
breakfast: state.bookedRoom.breakfast,
|
breakfast: state.bookedRoom.breakfast,
|
||||||
|
guest: state.bookedRoom.guest,
|
||||||
checkInDate: state.bookedRoom.checkInDate,
|
checkInDate: state.bookedRoom.checkInDate,
|
||||||
cheques: state.bookedRoom.cheques,
|
cheques: state.bookedRoom.cheques,
|
||||||
childrenAges: state.bookedRoom.childrenAges,
|
childrenAges: state.bookedRoom.childrenAges,
|
||||||
@@ -77,6 +86,7 @@ export default function SingleRoom({ bedType, image, user }: RoomProps) {
|
|||||||
roomTypeCode: state.bookedRoom.roomTypeCode,
|
roomTypeCode: state.bookedRoom.roomTypeCode,
|
||||||
totalPrice: state.bookedRoom.totalPrice,
|
totalPrice: state.bookedRoom.totalPrice,
|
||||||
vouchers: state.bookedRoom.vouchers,
|
vouchers: state.bookedRoom.vouchers,
|
||||||
|
bookedRoom: state.bookedRoom,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if (!roomNumber) {
|
if (!roomNumber) {
|
||||||
@@ -149,6 +159,8 @@ export default function SingleRoom({ bedType, image, user }: RoomProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hotelRoom = getBookedHotelRoom(roomCategories, roomTypeCode)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<article className={styles.room}>
|
<article className={styles.room}>
|
||||||
@@ -187,11 +199,27 @@ export default function SingleRoom({ bedType, image, user }: RoomProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.sidePeek}>
|
<div className={styles.sidePeek}>
|
||||||
<ToggleSidePeek
|
<DialogTrigger>
|
||||||
hotelId={hotel.operaId}
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
roomTypeCode={roomTypeCode}
|
<ButtonRAC className={styles.trigger}>
|
||||||
intent="text"
|
<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>
|
</div>
|
||||||
<div className={styles.booking}>
|
<div className={styles.booking}>
|
||||||
@@ -401,7 +429,12 @@ export default function SingleRoom({ bedType, image, user }: RoomProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.guestDetailsDesktopWrapper}>
|
<div className={styles.guestDetailsDesktopWrapper}>
|
||||||
<GuestDetails user={user} />
|
<GuestDetails
|
||||||
|
confirmationNumber={confirmationNumber}
|
||||||
|
guest={guest}
|
||||||
|
isCancelled={isCancelled}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -454,7 +487,12 @@ export default function SingleRoom({ bedType, image, user }: RoomProps) {
|
|||||||
|
|
||||||
<PriceDetails />
|
<PriceDetails />
|
||||||
<div className={styles.guestDetailsMobileWrapper}>
|
<div className={styles.guestDetailsMobileWrapper}>
|
||||||
<GuestDetails user={user} />
|
<GuestDetails
|
||||||
|
confirmationNumber={confirmationNumber}
|
||||||
|
guest={guest}
|
||||||
|
isCancelled={isCancelled}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,17 @@
|
|||||||
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);
|
||||||
|
|||||||
@@ -20,12 +20,15 @@ interface RoomsProps {
|
|||||||
|
|
||||||
export default function Rooms({ user }: RoomsProps) {
|
export default function Rooms({ user }: RoomsProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const { allRoomsAreCancelled, room, rooms } = useMyStayStore((state) => ({
|
const { allRoomsAreCancelled, room, rooms, roomCategories } = useMyStayStore(
|
||||||
allRoomsAreCancelled: state.allRoomsAreCancelled,
|
(state) => ({
|
||||||
hotel: state.hotel,
|
allRoomsAreCancelled: state.allRoomsAreCancelled,
|
||||||
room: state.bookedRoom.room,
|
hotel: state.hotel,
|
||||||
rooms: state.rooms,
|
room: state.bookedRoom.room,
|
||||||
}))
|
rooms: state.rooms,
|
||||||
|
roomCategories: state.roomCategories,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
if (!room) {
|
if (!room) {
|
||||||
return null
|
return null
|
||||||
@@ -50,6 +53,7 @@ export default function Rooms({ user }: RoomsProps) {
|
|||||||
bedType={room.bedType}
|
bedType={room.bedType}
|
||||||
image={room.images[0]}
|
image={room.images[0]}
|
||||||
user={user}
|
user={user}
|
||||||
|
roomCategories={roomCategories}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.roomsContainer}>
|
<div className={styles.roomsContainer}>
|
||||||
@@ -58,7 +62,12 @@ export default function Rooms({ user }: RoomsProps) {
|
|||||||
key={booking.confirmationNumber}
|
key={booking.confirmationNumber}
|
||||||
className={styles.roomWrapper}
|
className={styles.roomWrapper}
|
||||||
>
|
>
|
||||||
<MultiRoom booking={booking} roomNr={index + 1} user={user} />
|
<MultiRoom
|
||||||
|
booking={booking}
|
||||||
|
roomNr={index + 1}
|
||||||
|
user={user}
|
||||||
|
roomCategories={roomCategories}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
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 { getPriceType } from "@/components/HotelReservation/utils/getPriceType"
|
||||||
|
|
||||||
import { formatChildBedPreferences } from "../utils"
|
import { formatChildBedPreferences } from "../utils"
|
||||||
import { convertToChildType } from "./convertToChildType"
|
|
||||||
import { getPriceType } from "./getPriceType"
|
|
||||||
|
|
||||||
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"
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import type { ToggleSidePeekProps } from "@/types/components/hotelReservation/to
|
|||||||
export default function ToggleSidePeek({
|
export default function ToggleSidePeek({
|
||||||
hotelId,
|
hotelId,
|
||||||
roomTypeCode,
|
roomTypeCode,
|
||||||
intent = "textInverted",
|
|
||||||
title,
|
|
||||||
}: ToggleSidePeekProps) {
|
}: ToggleSidePeekProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const openSidePeek = useSidePeekStore((state) => state.openSidePeek)
|
const openSidePeek = useSidePeekStore((state) => state.openSidePeek)
|
||||||
@@ -22,19 +20,19 @@ export default function ToggleSidePeek({
|
|||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openSidePeek({ key: SidePeekEnum.roomDetails, hotelId, roomTypeCode })
|
openSidePeek({
|
||||||
|
key: SidePeekEnum.roomDetails,
|
||||||
|
hotelId,
|
||||||
|
roomTypeCode,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
theme="base"
|
intent="text"
|
||||||
size="small"
|
size="small"
|
||||||
|
theme="base"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
intent={intent}
|
|
||||||
wrapping
|
wrapping
|
||||||
>
|
>
|
||||||
{title
|
{intl.formatMessage({ defaultMessage: "Room details" })}
|
||||||
? title
|
|
||||||
: intl.formatMessage({
|
|
||||||
defaultMessage: "See room details",
|
|
||||||
})}
|
|
||||||
<MaterialIcon icon="chevron_right" size={14} />
|
<MaterialIcon icon="chevron_right" size={14} />
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -51,14 +51,7 @@ export default function Details({ roomTypeCode }: { roomTypeCode: string }) {
|
|||||||
<RoomSize roomSize={roomSize} />
|
<RoomSize roomSize={roomSize} />
|
||||||
<div className={styles.toggleSidePeek}>
|
<div className={styles.toggleSidePeek}>
|
||||||
{roomTypeCode && (
|
{roomTypeCode && (
|
||||||
<ToggleSidePeek
|
<ToggleSidePeek hotelId={hotelId} roomTypeCode={roomTypeCode} />
|
||||||
hotelId={hotelId}
|
|
||||||
roomTypeCode={roomTypeCode}
|
|
||||||
title={intl.formatMessage({
|
|
||||||
defaultMessage: "Room details",
|
|
||||||
})}
|
|
||||||
intent="text"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,27 +4,19 @@ import { trpc } from "@/lib/trpc/client"
|
|||||||
import useSidePeekStore from "@/stores/sidepeek"
|
import useSidePeekStore from "@/stores/sidepeek"
|
||||||
|
|
||||||
import AmenitiesSidePeek from "@/components/SidePeeks/AmenitiesSidePeek"
|
import AmenitiesSidePeek from "@/components/SidePeeks/AmenitiesSidePeek"
|
||||||
import BookedRoomSidePeek from "@/components/SidePeeks/BookedRoomSidePeek"
|
|
||||||
import HotelSidePeek from "@/components/SidePeeks/HotelSidePeek"
|
import HotelSidePeek from "@/components/SidePeeks/HotelSidePeek"
|
||||||
import RoomSidePeek from "@/components/SidePeeks/RoomSidePeek"
|
import RoomSidePeek from "@/components/SidePeeks/RoomSidePeek"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
export default function HotelReservationSidePeek() {
|
export default function HotelReservationSidePeek() {
|
||||||
const {
|
const { activeSidePeek, hotelId, roomTypeCode, showCTA } = useSidePeekStore(
|
||||||
activeSidePeek,
|
(state) => ({
|
||||||
confirmationNumber,
|
activeSidePeek: state.activeSidePeek,
|
||||||
hotelId,
|
hotelId: state.hotelId,
|
||||||
roomTypeCode,
|
roomTypeCode: state.roomTypeCode,
|
||||||
showCTA,
|
showCTA: state.showCTA,
|
||||||
user,
|
})
|
||||||
} = useSidePeekStore((state) => ({
|
)
|
||||||
activeSidePeek: state.activeSidePeek,
|
|
||||||
confirmationNumber: state.confirmationNumber,
|
|
||||||
hotelId: state.hotelId,
|
|
||||||
roomTypeCode: state.roomTypeCode,
|
|
||||||
showCTA: state.showCTA,
|
|
||||||
user: state.user,
|
|
||||||
}))
|
|
||||||
const close = useSidePeekStore((state) => state.closeSidePeek)
|
const close = useSidePeekStore((state) => state.closeSidePeek)
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
|
|
||||||
@@ -72,15 +64,6 @@ export default function HotelReservationSidePeek() {
|
|||||||
close={close}
|
close={close}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{selectedRoom && (
|
|
||||||
<BookedRoomSidePeek
|
|
||||||
room={selectedRoom}
|
|
||||||
activeSidePeek={activeSidePeek}
|
|
||||||
close={close}
|
|
||||||
user={user}
|
|
||||||
confirmationNumber={confirmationNumber}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ 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 { dt } from "@/lib/dt"
|
||||||
import { useMyStayStore } from "@/stores/my-stay"
|
|
||||||
|
|
||||||
import GuestDetails from "@/components/HotelReservation/MyStay/GuestDetails"
|
import GuestDetails from "@/components/HotelReservation/MyStay/GuestDetails"
|
||||||
import PriceType from "@/components/HotelReservation/MyStay/PriceType"
|
import PriceType from "@/components/HotelReservation/MyStay/PriceType"
|
||||||
@@ -14,7 +13,7 @@ import ImageGallery from "@/components/ImageGallery"
|
|||||||
import Accordion from "@/components/TempDesignSystem/Accordion"
|
import Accordion from "@/components/TempDesignSystem/Accordion"
|
||||||
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
|
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
|
||||||
import IconChip from "@/components/TempDesignSystem/IconChip"
|
import IconChip from "@/components/TempDesignSystem/IconChip"
|
||||||
import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
import SidePeekSelfControlled from "@/components/TempDesignSystem/SidePeekSelfControlled"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||||
import { formatPrice } from "@/utils/numberFormatting"
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
@@ -23,28 +22,60 @@ import RoomDetails from "./RoomDetails"
|
|||||||
|
|
||||||
import styles from "./bookedRoomSidePeek.module.css"
|
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 { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
|
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
import type { BookedRoomSidePeekProps } from "@/types/components/sidePeeks/bookedRoomSidePeek"
|
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({
|
export default function BookedRoomSidePeek({
|
||||||
|
hotelRoom,
|
||||||
room,
|
room,
|
||||||
activeSidePeek,
|
|
||||||
user,
|
user,
|
||||||
confirmationNumber,
|
}: RoomDetailsSidePeekProps) {
|
||||||
close,
|
|
||||||
}: BookedRoomSidePeekProps) {
|
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const rooms = useMyStayStore((state) => state.rooms)
|
|
||||||
|
|
||||||
const bookingRoom = rooms.find(
|
|
||||||
(r) => r.confirmationNumber === confirmationNumber
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!bookingRoom) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
adults,
|
adults,
|
||||||
@@ -53,20 +84,28 @@ export default function BookedRoomSidePeek({
|
|||||||
breakfast,
|
breakfast,
|
||||||
cancellationNumber,
|
cancellationNumber,
|
||||||
checkInDate,
|
checkInDate,
|
||||||
|
cheques,
|
||||||
childrenInRoom,
|
childrenInRoom,
|
||||||
|
confirmationNumber,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
|
guest,
|
||||||
isCancelled,
|
isCancelled,
|
||||||
|
roomName,
|
||||||
packages,
|
packages,
|
||||||
|
priceType,
|
||||||
rateDefinition,
|
rateDefinition,
|
||||||
roomNumber,
|
roomNumber,
|
||||||
|
roomPoints,
|
||||||
terms,
|
terms,
|
||||||
totalPrice,
|
totalPrice,
|
||||||
} = bookingRoom
|
vouchers,
|
||||||
|
} = room
|
||||||
|
|
||||||
const fromDate = dt(checkInDate).locale(lang)
|
const fromDate = dt(checkInDate).locale(lang)
|
||||||
|
|
||||||
const roomDescription = room.descriptions.medium
|
const galleryImages = hotelRoom
|
||||||
const galleryImages = mapApiImagesToGalleryImages(room.images)
|
? mapApiImagesToGalleryImages(hotelRoom.images)
|
||||||
|
: null
|
||||||
|
|
||||||
const adultsMsg = intl.formatMessage(
|
const adultsMsg = intl.formatMessage(
|
||||||
{
|
{
|
||||||
@@ -106,12 +145,10 @@ export default function BookedRoomSidePeek({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hotelRoomName = hotelRoom?.name || roomName
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidePeek
|
<SidePeekSelfControlled title={hotelRoomName}>
|
||||||
title={room.name}
|
|
||||||
isOpen={activeSidePeek === SidePeekEnum.bookedRoomDetails}
|
|
||||||
handleClose={close}
|
|
||||||
>
|
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<div className={styles.roomHeader}>
|
<div className={styles.roomHeader}>
|
||||||
{isCancelled ? (
|
{isCancelled ? (
|
||||||
@@ -180,11 +217,13 @@ export default function BookedRoomSidePeek({
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.mainContent}>
|
<div className={styles.mainContent}>
|
||||||
<div className={styles.imageContainer}>
|
<div className={styles.imageContainer}>
|
||||||
<ImageGallery
|
{galleryImages ? (
|
||||||
images={galleryImages}
|
<ImageGallery
|
||||||
title={room.name}
|
height={280}
|
||||||
height={280}
|
images={galleryImages}
|
||||||
/>
|
title={hotelRoomName}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.roomDetails}>
|
<div className={styles.roomDetails}>
|
||||||
<div className={styles.row}>
|
<div className={styles.row}>
|
||||||
@@ -334,14 +373,14 @@ export default function BookedRoomSidePeek({
|
|||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<PriceType
|
<PriceType
|
||||||
cheques={bookingRoom.cheques}
|
cheques={cheques}
|
||||||
formattedTotalPrice={formattedTotalPrice}
|
formattedTotalPrice={formattedTotalPrice}
|
||||||
isCancelled={bookingRoom.isCancelled}
|
isCancelled={isCancelled}
|
||||||
priceType={bookingRoom.priceType}
|
priceType={priceType}
|
||||||
rateDefinition={bookingRoom.rateDefinition}
|
rateDefinition={rateDefinition}
|
||||||
roomPoints={bookingRoom.roomPoints}
|
roomPoints={roomPoints}
|
||||||
totalPrice={bookingRoom.totalPrice}
|
totalPrice={totalPrice}
|
||||||
vouchers={bookingRoom.vouchers}
|
vouchers={vouchers}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -369,23 +408,30 @@ export default function BookedRoomSidePeek({
|
|||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<GuestDetails selectedRoom={bookingRoom} user={user} />
|
<GuestDetails
|
||||||
|
confirmationNumber={confirmationNumber}
|
||||||
|
guest={guest}
|
||||||
|
isCancelled={isCancelled}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Accordion>
|
{hotelRoom ? (
|
||||||
<AccordionItem
|
<Accordion>
|
||||||
title={intl.formatMessage({
|
<AccordionItem
|
||||||
defaultMessage: "Room details",
|
title={intl.formatMessage({
|
||||||
})}
|
defaultMessage: "Room details",
|
||||||
variant="sidepeek"
|
})}
|
||||||
>
|
variant="sidepeek"
|
||||||
<RoomDetails
|
>
|
||||||
roomDescription={roomDescription}
|
<RoomDetails
|
||||||
roomFacilities={room.roomFacilities}
|
roomDescription={hotelRoom.descriptions.medium}
|
||||||
roomTypes={room.roomTypes}
|
roomFacilities={hotelRoom.roomFacilities}
|
||||||
/>
|
roomTypes={hotelRoom.roomTypes}
|
||||||
</AccordionItem>
|
/>
|
||||||
</Accordion>
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</SidePeek>
|
</SidePeekSelfControlled>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface SidePeekSelfControlledProps {
|
||||||
|
title: string
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ export default function BookingConfirmationProvider({
|
|||||||
currencyCode,
|
currencyCode,
|
||||||
fromDate,
|
fromDate,
|
||||||
toDate,
|
toDate,
|
||||||
|
roomCategories,
|
||||||
rooms,
|
rooms,
|
||||||
vat,
|
vat,
|
||||||
}: BookingConfirmationProviderProps) {
|
}: BookingConfirmationProviderProps) {
|
||||||
@@ -73,6 +74,7 @@ export default function BookingConfirmationProvider({
|
|||||||
fromDate,
|
fromDate,
|
||||||
toDate,
|
toDate,
|
||||||
rooms,
|
rooms,
|
||||||
|
roomCategories,
|
||||||
vat,
|
vat,
|
||||||
isVatCurrency,
|
isVatCurrency,
|
||||||
formattedTotalCost,
|
formattedTotalCost,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export function createBookingConfirmationStore(initialState: InitialState) {
|
|||||||
currencyCode: initialState.currencyCode,
|
currencyCode: initialState.currencyCode,
|
||||||
fromDate: initialState.fromDate,
|
fromDate: initialState.fromDate,
|
||||||
toDate: initialState.toDate,
|
toDate: initialState.toDate,
|
||||||
|
roomCategories: initialState.roomCategories,
|
||||||
vat: initialState.vat,
|
vat: initialState.vat,
|
||||||
formattedTotalCost: initialState.formattedTotalCost,
|
formattedTotalCost: initialState.formattedTotalCost,
|
||||||
isVatCurrency: initialState.isVatCurrency,
|
isVatCurrency: initialState.isVatCurrency,
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export function createMyStayStore({
|
|||||||
savedCreditCards,
|
savedCreditCards,
|
||||||
totalPoints,
|
totalPoints,
|
||||||
totalPrice,
|
totalPrice,
|
||||||
|
roomCategories,
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
closeManageStay() {
|
closeManageStay() {
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ export interface RoomProps {
|
|||||||
checkOutTime: string
|
checkOutTime: string
|
||||||
img: NonNullable<BookingConfirmation["room"]>["images"][number]
|
img: NonNullable<BookingConfirmation["room"]>["images"][number]
|
||||||
roomName: NonNullable<BookingConfirmation["room"]>["name"]
|
roomName: NonNullable<BookingConfirmation["room"]>["name"]
|
||||||
|
roomNumber?: number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { CurrencyEnum } from "../enums/currency"
|
import type { CurrencyEnum } from "../enums/currency"
|
||||||
|
import type { RoomCategories } from "../hotel"
|
||||||
import type { Room } from "../stores/booking-confirmation"
|
import type { Room } from "../stores/booking-confirmation"
|
||||||
|
|
||||||
export interface BookingConfirmationProviderProps
|
export interface BookingConfirmationProviderProps
|
||||||
@@ -7,6 +8,7 @@ export interface BookingConfirmationProviderProps
|
|||||||
currencyCode: CurrencyEnum
|
currencyCode: CurrencyEnum
|
||||||
fromDate: Date
|
fromDate: Date
|
||||||
rooms: (Room | null)[]
|
rooms: (Room | null)[]
|
||||||
|
roomCategories: RoomCategories
|
||||||
toDate: Date
|
toDate: Date
|
||||||
vat: number
|
vat: number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { ChildBedTypeEnum } from "@/constants/booking"
|
import type { ChildBedTypeEnum } from "@/constants/booking"
|
||||||
import type { CurrencyEnum } from "../enums/currency"
|
import type { CurrencyEnum } from "../enums/currency"
|
||||||
|
import type { RoomCategories } from "../hotel"
|
||||||
import type {
|
import type {
|
||||||
BookingConfirmation,
|
BookingConfirmation,
|
||||||
PackageSchema,
|
PackageSchema,
|
||||||
@@ -43,6 +44,7 @@ export interface InitialState {
|
|||||||
rooms: (Room | null)[]
|
rooms: (Room | null)[]
|
||||||
toDate: Date
|
toDate: Date
|
||||||
currencyCode: CurrencyEnum
|
currencyCode: CurrencyEnum
|
||||||
|
roomCategories: RoomCategories
|
||||||
vat: number
|
vat: number
|
||||||
isVatCurrency: boolean
|
isVatCurrency: boolean
|
||||||
formattedTotalCost: string
|
formattedTotalCost: string
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ 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
|
||||||
|
|||||||
Reference in New Issue
Block a user