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}
/>
)}
</>
)
}