feat: add multiroom signup

This commit is contained in:
Simon Emanuelsson
2025-02-17 15:10:48 +01:00
parent 95917e5e4f
commit 92c5566c59
78 changed files with 2035 additions and 1545 deletions

View File

@@ -9,44 +9,36 @@ import {
type BedTypeEnum,
type ExtraBedTypeEnum,
} from "@/constants/booking"
import { useEnterDetailsStore } from "@/stores/enter-details"
import { selectRoom } from "@/stores/enter-details/helpers"
import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio"
import { useRoomContext } from "@/contexts/Details/Room"
import BedTypeInfo from "./BedTypeInfo"
import { bedTypeFormSchema } from "./schema"
import styles from "./bedOptions.module.css"
import type {
BedTypeFormSchema,
BedTypeProps,
} from "@/types/components/hotelReservation/enterDetails/bedType"
import type { BedTypeFormSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
import type { IconProps } from "@/types/components/icon"
export default function BedType({
bedTypes,
roomIndex,
}: BedTypeProps & { roomIndex: number }) {
const room = useEnterDetailsStore((state) => selectRoom(state, roomIndex))
const initialBedType = room.bedType?.roomTypeCode
const updateBedType = useEnterDetailsStore(
(state) => state.actions.updateBedType
)
export default function BedType() {
const {
actions: { updateBedType },
room: { bedType, bedTypes },
} = useRoomContext()
const initialBedType = bedType?.roomTypeCode
const methods = useForm<BedTypeFormSchema>({
defaultValues: initialBedType ? { bedType: initialBedType } : undefined,
criteriaMode: "all",
mode: "all",
resolver: zodResolver(bedTypeFormSchema),
reValidateMode: "onChange",
values: initialBedType ? { bedType: initialBedType } : undefined,
})
const onSubmit = useCallback(
(bedTypeRoomCode: BedTypeFormSchema) => {
const matchingRoom = bedTypes.find(
const matchingRoom = bedTypes?.find(
(roomType) => roomType.value === bedTypeRoomCode.bedType
)
if (matchingRoom) {
@@ -60,12 +52,6 @@ export default function BedType({
[bedTypes, updateBedType]
)
useEffect(() => {
if (initialBedType) {
methods.setValue("bedType", initialBedType)
}
}, [initialBedType, methods])
useEffect(() => {
if (methods.formState.isSubmitting) {
return

View File

@@ -6,28 +6,25 @@ import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { useEnterDetailsStore } from "@/stores/enter-details"
import { selectRoom } from "@/stores/enter-details/helpers"
import BreakfastChoiceCard from "@/components/HotelReservation/EnterDetails/Breakfast/BreakfastChoiceCard"
import Body from "@/components/TempDesignSystem/Text/Body"
import { useRoomContext } from "@/contexts/Details/Room"
import { breakfastFormSchema } from "./schema"
import styles from "./breakfast.module.css"
import type {
BreakfastFormSchema,
BreakfastProps,
} from "@/types/components/hotelReservation/enterDetails/breakfast"
import type { BreakfastFormSchema } from "@/types/components/hotelReservation/enterDetails/breakfast"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
export default function Breakfast({
packages,
roomIndex,
}: BreakfastProps & { roomIndex: number }) {
export default function Breakfast() {
const intl = useIntl()
const room = useEnterDetailsStore((state) => selectRoom(state, roomIndex))
const packages = useEnterDetailsStore((state) => state.breakfastPackages)
const {
actions: { updateBreakfast },
room,
} = useRoomContext()
const breakfastSelection = room?.breakfast
? room.breakfast.code
@@ -35,14 +32,6 @@ export default function Breakfast({
? "false"
: undefined
const updateBreakfast = useEnterDetailsStore(
(state) => state.actions.updateBreakfast
)
const children = useEnterDetailsStore(
(state) => state.booking.rooms[0].childrenInRoom
)
const methods = useForm<BreakfastFormSchema>({
defaultValues: breakfastSelection
? { breakfast: breakfastSelection }
@@ -65,12 +54,6 @@ export default function Breakfast({
[packages, updateBreakfast]
)
useEffect(() => {
if (breakfastSelection) {
methods.setValue("breakfast", breakfastSelection)
}
}, [breakfastSelection, methods])
useEffect(() => {
if (methods.formState.isSubmitting) {
return
@@ -82,7 +65,7 @@ export default function Breakfast({
return (
<FormProvider {...methods}>
<div className={styles.container}>
{children?.length ? (
{room.childrenInRoom?.length ? (
<Body>
{intl.formatMessage({
id: "Children's breakfast is always free as part of the adult's breakfast.",
@@ -90,7 +73,7 @@ export default function Breakfast({
</Body>
) : null}
<form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}>
{packages.map((pkg) => (
{packages?.map((pkg) => (
<BreakfastChoiceCard
key={pkg.code}
name="breakfast"
@@ -118,7 +101,7 @@ export default function Breakfast({
title: intl.formatMessage({ id: "No breakfast" }),
price: {
total: 0,
currency: packages[0].localPrice.currency,
currency: packages?.[0].localPrice.currency ?? "",
},
description: intl.formatMessage({
id: "You can always change your mind later and add breakfast at the hotel.",

View File

@@ -0,0 +1,73 @@
"use client"
import { useIntl } from "react-intl"
import { CheckIcon } from "@/components/Icons"
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import { useRoomContext } from "@/contexts/Details/Room"
import { formatPrice } from "@/utils/numberFormatting"
import styles from "./joinScandicFriendsCard.module.css"
import type { JoinScandicFriendsCardProps } from "@/types/components/hotelReservation/enterDetails/details"
export default function JoinScandicFriendsCard({
name = "join",
}: JoinScandicFriendsCardProps) {
const intl = useIntl()
const { room, roomNr } = useRoomContext()
if (!room.roomRate.memberRate) {
return null
}
const list = [
{ title: intl.formatMessage({ id: "Earn bonus nights & points" }) },
{ title: intl.formatMessage({ id: "Get member benefits & offers" }) },
{ title: intl.formatMessage({ id: "Join at no cost" }) },
]
const saveOnJoiningLabel = intl.formatMessage(
{
id: "Pay the member price of {amount} for Room {roomNr}",
},
{
amount: formatPrice(
intl,
room.roomRate.memberRate.localPrice.pricePerStay,
room.roomRate.memberRate.localPrice.currency
),
roomNr,
}
)
return (
<div className={styles.cardContainer}>
<Checkbox name={name} className={styles.checkBox}>
<div>
<Caption type="label" textTransform="uppercase" color="red">
{saveOnJoiningLabel}
</Caption>
<Caption color="uiTextHighContrast">
{intl.formatMessage({
id: "I promise to join Scandic Friends before checking in",
})}
</Caption>
</div>
</Checkbox>
<div className={styles.list}>
{list.map((item) => (
<Caption
key={item.title}
className={styles.listItem}
color="uiTextPlaceholder"
>
<CheckIcon color="uiTextPlaceholder" height="20" /> {item.title}
</Caption>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,35 @@
.cardContainer {
align-self: flex-start;
background-color: var(--Base-Surface-Primary-light-Normal);
border: 1px solid var(--Base-Border-Subtle);
border-radius: var(--Corner-radius-Large);
display: grid;
gap: var(--Spacing-x-one-and-half);
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
width: min(100%, 600px);
}
.checkBox {
align-self: center;
}
.list {
display: flex;
gap: var(--Spacing-x1);
flex-direction: column;
}
.listItem {
display: flex;
}
@media screen and (min-width: 768px) {
.cardContainer {
gap: var(--Spacing-x1);
}
.list {
flex-direction: row;
flex-wrap: wrap;
}
}

View File

@@ -0,0 +1,142 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { useEnterDetailsStore } from "@/stores/enter-details"
import Button from "@/components/TempDesignSystem/Button"
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
import Input from "@/components/TempDesignSystem/Form/Input"
import Phone from "@/components/TempDesignSystem/Form/Phone"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import { useRoomContext } from "@/contexts/Details/Room"
import JoinScandicFriendsCard from "./JoinScandicFriendsCard"
import { multiroomDetailsSchema } from "./schema"
import styles from "./details.module.css"
import type { MultiroomDetailsSchema } from "@/types/components/hotelReservation/enterDetails/details"
const formID = "enter-details"
export default function Details() {
const intl = useIntl()
const { activeRoom, canProceedToPayment, lastRoom } = useEnterDetailsStore(
(state) => ({
activeRoom: state.activeRoom,
canProceedToPayment: state.canProceedToPayment,
lastRoom: state.lastRoom,
})
)
const {
actions: { updateDetails },
room,
roomNr,
} = useRoomContext()
const initialData = room.guest
const isPaymentNext = activeRoom === lastRoom
const methods = useForm<MultiroomDetailsSchema>({
criteriaMode: "all",
mode: "all",
resolver: zodResolver(multiroomDetailsSchema),
reValidateMode: "onChange",
values: {
countryCode: initialData.countryCode,
email: initialData.email,
firstName: initialData.firstName,
join: initialData.join,
lastName: initialData.lastName,
membershipNo: initialData.membershipNo,
phoneNumber: initialData.phoneNumber,
},
})
const guestIsGoingToJoin = methods.watch("join")
const guestIsMember = methods.watch("membershipNo")
return (
<FormProvider {...methods}>
<form
className={styles.form}
id={`${formID}-room-${roomNr}`}
onSubmit={methods.handleSubmit(updateDetails)}
>
{guestIsMember ? null : <JoinScandicFriendsCard />}
<div className={styles.container}>
<Footnote
color="uiTextHighContrast"
textTransform="uppercase"
type="label"
className={styles.fullWidth}
>
{intl.formatMessage({ id: "Guest information" })}
</Footnote>
<Input
label={intl.formatMessage({ id: "First name" })}
maxLength={30}
name="firstName"
registerOptions={{ required: true }}
/>
<Input
label={intl.formatMessage({ id: "Last name" })}
maxLength={30}
name="lastName"
registerOptions={{ required: true }}
/>
<CountrySelect
className={styles.fullWidth}
label={intl.formatMessage({ id: "Country" })}
name="countryCode"
registerOptions={{ required: true }}
/>
<Input
className={styles.fullWidth}
label={intl.formatMessage({ id: "Email address" })}
name="email"
registerOptions={{ required: true }}
/>
<Phone
className={styles.fullWidth}
label={intl.formatMessage({ id: "Phone number" })}
name="phoneNumber"
registerOptions={{ required: true }}
/>
{guestIsGoingToJoin ? null : (
<Input
className={styles.fullWidth}
label={intl.formatMessage({ id: "Membership no" })}
name="membershipNo"
type="tel"
/>
)}
</div>
<footer className={styles.footer}>
<Button
disabled={
!(
methods.formState.isValid ||
(isPaymentNext && canProceedToPayment)
)
}
intent="secondary"
size="small"
theme="base"
type="submit"
>
{isPaymentNext
? intl.formatMessage({ id: "Proceed to payment method" })
: intl.formatMessage(
{ id: "Continue to room {nextRoomNumber}" },
{ nextRoomNumber: roomNr + 1 }
)}
</Button>
</footer>
</form>
</FormProvider>
)
}

View File

@@ -0,0 +1,43 @@
import { z } from "zod"
import { phoneValidator } from "@/utils/zod/phoneValidator"
// stringMatcher regex is copied from current web as specified by requirements.
const stringMatcher =
/^[A-Za-z¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ0-9-\s]*$/
const isValidString = (key: string) => stringMatcher.test(key)
export const multiroomDetailsSchema = z.object({
countryCode: z.string().min(1, { message: "Country is required" }),
email: z.string().email({ message: "Email address is required" }),
firstName: z
.string()
.min(1, { message: "First name is required" })
.refine(isValidString, {
message: "First name can't contain any special characters",
}),
join: z.boolean().default(false),
lastName: z
.string()
.min(1, { message: "Last name is required" })
.refine(isValidString, {
message: "Last name can't contain any special characters",
}),
phoneNumber: phoneValidator(),
membershipNo: z
.string()
.optional()
.refine((val) => {
if (val) {
return !val.match(/[^0-9]/g)
}
return true
}, "Only digits are allowed")
.refine((num) => {
if (num) {
return num.match(/^30812(?!(0|1|2))[0-9]{9}$/)
}
return true
}, "Invalid membership number format"),
})

View File

@@ -10,6 +10,7 @@ import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
import Link from "@/components/TempDesignSystem/Link"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import { useRoomContext } from "@/contexts/Details/Room"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
@@ -18,11 +19,15 @@ import styles from "./joinScandicFriendsCard.module.css"
import type { JoinScandicFriendsCardProps } from "@/types/components/hotelReservation/enterDetails/details"
export default function JoinScandicFriendsCard({
name,
memberPrice,
name = "join",
}: JoinScandicFriendsCardProps) {
const lang = useLang()
const intl = useIntl()
const { room } = useRoomContext()
if (!room.roomRate.memberRate) {
return null
}
const list = [
{ title: intl.formatMessage({ id: "Friendly room rates" }) },
@@ -37,8 +42,8 @@ export default function JoinScandicFriendsCard({
{
amount: formatPrice(
intl,
memberPrice?.price ?? 0,
memberPrice?.currency ?? "SEK"
room.roomRate.memberRate.localPrice.pricePerStay,
room.roomRate.memberRate.localPrice.currency
),
}
)
@@ -47,11 +52,9 @@ export default function JoinScandicFriendsCard({
<div className={styles.cardContainer}>
<Checkbox name={name} className={styles.checkBox}>
<div>
{memberPrice ? (
<Caption type="label" textTransform="uppercase" color="red">
{saveOnJoiningLabel}
</Caption>
) : null}
<Caption type="label" textTransform="uppercase" color="red">
{saveOnJoiningLabel}
</Caption>
<Caption
type="label"
textTransform="uppercase"

View File

@@ -2,15 +2,13 @@
import { useIntl } from "react-intl"
import { useEnterDetailsStore } from "@/stores/enter-details"
import { selectRoom } from "@/stores/enter-details/helpers"
import { MagicWandIcon } from "@/components/Icons"
import Modal from "@/components/Modal"
import Button from "@/components/TempDesignSystem/Button"
import Body from "@/components/TempDesignSystem/Text/Body"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import Title from "@/components/TempDesignSystem/Text/Title"
import { useRoomContext } from "@/contexts/Details/Room"
import { formatPrice } from "@/utils/numberFormatting"
import styles from "./modal.module.css"
@@ -24,7 +22,7 @@ export default function MemberPriceModal({
isOpen: boolean
setIsOpen: Dispatch<SetStateAction<boolean>>
}) {
const room = useEnterDetailsStore(selectRoom)
const { room } = useRoomContext()
const memberRate = room.roomRate.memberRate
const intl = useIntl()

View File

@@ -0,0 +1,30 @@
.form {
display: grid;
gap: var(--Spacing-x3);
}
.container {
display: grid;
gap: var(--Spacing-x2);
width: min(100%, 600px);
}
.fullWidth {
grid-column: 1/-1;
}
.footer {
margin-top: var(--Spacing-x1);
}
@media screen and (min-width: 768px) {
.form {
gap: var(--Spacing-x3);
}
.container {
gap: var(--Spacing-x2);
grid-template-columns: 1fr 1fr;
width: min(100%, 600px);
}
}

View File

@@ -5,16 +5,13 @@ import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { useEnterDetailsStore } from "@/stores/enter-details"
import {
selectBookingProgress,
selectRoom,
} from "@/stores/enter-details/helpers"
import Button from "@/components/TempDesignSystem/Button"
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
import Input from "@/components/TempDesignSystem/Form/Input"
import Phone from "@/components/TempDesignSystem/Form/Phone"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import { useRoomContext } from "@/contexts/Details/Room"
import JoinScandicFriendsCard from "./JoinScandicFriendsCard"
import MemberPriceModal from "./MemberPriceModal"
@@ -30,24 +27,26 @@ import type {
} from "@/types/components/hotelReservation/enterDetails/details"
const formID = "enter-details"
export default function Details({
user,
memberPrice,
roomIndex,
}: DetailsProps & { roomIndex: number }) {
export default function Details({ user }: DetailsProps) {
const intl = useIntl()
const [isMemberPriceModalOpen, setIsMemberPriceModalOpen] = useState(false)
const { currentRoomIndex, canProceedToPayment, roomStatuses } =
useEnterDetailsStore(selectBookingProgress)
const room = useEnterDetailsStore((state) => selectRoom(state, roomIndex))
const initialData = room.guest
const updateDetails = useEnterDetailsStore(
(state) => state.actions.updateDetails
const { activeRoom, canProceedToPayment, lastRoom } = useEnterDetailsStore(
(state) => ({
activeRoom: state.activeRoom,
canProceedToPayment: state.canProceedToPayment,
lastRoom: state.lastRoom,
})
)
const {
actions: { updateDetails },
room,
roomNr,
} = useRoomContext()
const initialData = room.guest
const memberRate = room.roomRate.memberRate
const isPaymentNext = currentRoomIndex === roomStatuses.length - 1
const isPaymentNext = activeRoom === lastRoom
const methods = useForm<DetailsSchema>({
criteriaMode: "all",
@@ -70,24 +69,22 @@ export default function Details({
const onSubmit = useCallback(
(values: DetailsSchema) => {
if ((values.join || values.membershipNo) && memberPrice && !user) {
if ((values.join || values.membershipNo) && memberRate && !user) {
setIsMemberPriceModalOpen(true)
}
updateDetails(values)
},
[updateDetails, setIsMemberPriceModalOpen, memberPrice, user]
[updateDetails, setIsMemberPriceModalOpen, memberRate, user]
)
return (
<FormProvider {...methods}>
<form
className={styles.form}
id={`${formID}-room-${roomIndex + 1}`}
id={`${formID}-room-${roomNr}`}
onSubmit={methods.handleSubmit(onSubmit)}
>
{user ? null : (
<JoinScandicFriendsCard name="join" memberPrice={memberPrice} />
)}
{user ? null : <JoinScandicFriendsCard />}
<div className={styles.container}>
<Footnote
color="uiTextHighContrast"
@@ -155,9 +152,9 @@ export default function Details({
{isPaymentNext
? intl.formatMessage({ id: "Proceed to payment method" })
: intl.formatMessage(
{ id: "Continue to room {nextRoomNumber}" },
{ nextRoomNumber: currentRoomIndex + 2 }
)}
{ id: "Continue to room {nextRoomNumber}" },
{ nextRoomNumber: roomNr + 1 }
)}
</Button>
</footer>
<MemberPriceModal

View File

@@ -55,7 +55,6 @@ function isPaymentMethodEnum(value: string): value is PaymentMethodEnum {
}
export default function PaymentClient({
user,
otherPaymentOptions,
savedCreditCards,
mustBeGuaranteed,
@@ -65,17 +64,13 @@ export default function PaymentClient({
const intl = useIntl()
const searchParams = useSearchParams()
const { totalPrice, booking, rooms, bookingProgress } = useEnterDetailsStore(
(state) => {
return {
totalPrice: state.totalPrice,
booking: state.booking,
rooms: state.rooms,
bookingProgress: state.bookingProgress,
}
}
)
const canProceedToPayment = bookingProgress.canProceedToPayment
const { booking, canProceedToPayment, rooms, totalPrice } =
useEnterDetailsStore((state) => ({
booking: state.booking,
canProceedToPayment: state.canProceedToPayment,
rooms: state.rooms,
totalPrice: state.totalPrice,
}))
const setIsSubmittingDisabled = useEnterDetailsStore(
(state) => state.actions.setIsSubmittingDisabled
@@ -120,7 +115,7 @@ export default function PaymentClient({
if (priceChange) {
setPriceChangeData({
oldPrice: rooms[0].roomPrice.perStay.local.price,
oldPrice: rooms[0].room.roomPrice.perStay.local.price,
newPrice: priceChange.totalPrice,
})
} else {
@@ -232,27 +227,27 @@ export default function PaymentClient({
hotelId,
checkInDate: fromDate,
checkOutDate: toDate,
rooms: rooms.map((room, idx) => ({
rooms: rooms.map(({ room }, idx) => ({
adults: room.adults,
childrenAges: room.childrenInRoom?.map((child) => ({
age: child.age,
bedType: bedTypeMap[parseInt(child.bed.toString())],
})),
rateCode:
(user || room.guest.join || room.guest.membershipNo) &&
(room.guest.join || room.guest.membershipNo) &&
booking.rooms[idx].counterRateCode
? booking.rooms[idx].counterRateCode
: booking.rooms[idx].rateCode,
roomTypeCode: room.bedType!.roomTypeCode, // A selection has been made in order to get to this step.
guest: {
becomeMember: room.guest.join,
countryCode: room.guest.countryCode,
dateOfBirth: room.guest.dateOfBirth,
email: room.guest.email,
firstName: room.guest.firstName,
lastName: room.guest.lastName,
email: room.guest.email,
phoneNumber: room.guest.phoneNumber,
countryCode: room.guest.countryCode,
membershipNumber: room.guest.membershipNo,
becomeMember: room.guest.join,
dateOfBirth: room.guest.dateOfBirth,
phoneNumber: room.guest.phoneNumber,
postalCode: room.guest.zipCode,
},
packages: {
@@ -301,7 +296,6 @@ export default function PaymentClient({
fromDate,
toDate,
rooms,
user,
booking,
]
)

View File

@@ -5,7 +5,6 @@ import PaymentClient from "./PaymentClient"
import type { PaymentProps } from "@/types/components/hotelReservation/enterDetails/payment"
export default async function Payment({
user,
otherPaymentOptions,
mustBeGuaranteed,
supportedCards,
@@ -16,7 +15,6 @@ export default async function Payment({
return (
<PaymentClient
user={user}
otherPaymentOptions={otherPaymentOptions}
savedCreditCards={savedCreditCards}
mustBeGuaranteed={mustBeGuaranteed}

View File

@@ -0,0 +1,3 @@
.header {
padding-bottom: var(--Spacing-x3);
}

View File

@@ -0,0 +1,5 @@
import styles from "./header.module.css"
export default function Header({ children }: React.PropsWithChildren) {
return <header className={styles.header}>{children}</header>
}

View File

@@ -0,0 +1,66 @@
"use client"
import { useIntl } from "react-intl"
import { useEnterDetailsStore } from "@/stores/enter-details"
import BedType from "@/components/HotelReservation/EnterDetails/BedType"
import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast"
import Details from "@/components/HotelReservation/EnterDetails/Details/Multiroom"
import Header from "@/components/HotelReservation/EnterDetails/Room/Header"
import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion"
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
import Title from "@/components/TempDesignSystem/Text/Title"
import { useRoomContext } from "@/contexts/Details/Room"
import { StepEnum } from "@/types/enums/step"
export default function Multiroom() {
const intl = useIntl()
const { room, roomNr } = useRoomContext()
const breakfastPackages = useEnterDetailsStore(
(state) => state.breakfastPackages
)
const showBreakfastStep =
!room.breakfastIncluded && !!breakfastPackages?.length
return (
<section>
<Header>
<Title level="h2" as="h4">
{`${intl.formatMessage({ id: "Room" })} ${roomNr}`}
</Title>
</Header>
<SelectedRoom />
{room.bedTypes ? (
<SectionAccordion
header={intl.formatMessage({ id: "Select bed" })}
label={intl.formatMessage({ id: "Request bedtype" })}
step={StepEnum.selectBed}
>
<BedType />
</SectionAccordion>
) : null}
{showBreakfastStep ? (
<SectionAccordion
header={intl.formatMessage({ id: "Food options" })}
label={intl.formatMessage({
id: "Select breakfast options",
})}
step={StepEnum.breakfast}
>
<Breakfast />
</SectionAccordion>
) : null}
<SectionAccordion
header={intl.formatMessage({ id: "Details" })}
step={StepEnum.details}
label={intl.formatMessage({ id: "Enter your details" })}
>
<Details />
</SectionAccordion>
</section>
)
}

View File

@@ -0,0 +1,72 @@
"use client"
import { useIntl } from "react-intl"
import { useEnterDetailsStore } from "@/stores/enter-details"
import BedType from "@/components/HotelReservation/EnterDetails/BedType"
import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast"
import Details from "@/components/HotelReservation/EnterDetails/Details/RoomOne"
import Header from "@/components/HotelReservation/EnterDetails/Room/Header"
import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion"
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
import Title from "@/components/TempDesignSystem/Text/Title"
import { useRoomContext } from "@/contexts/Details/Room"
import { StepEnum } from "@/types/enums/step"
import type { SafeUser } from "@/types/user"
export default function RoomOne({ user }: { user: SafeUser }) {
const intl = useIntl()
const { room } = useRoomContext()
const { breakfastPackages, rooms } = useEnterDetailsStore((state) => ({
breakfastPackages: state.breakfastPackages,
rooms: state.rooms,
}))
const isMultiroom = rooms.length > 1
const showBreakfastStep =
!room.breakfastIncluded && !!breakfastPackages?.length
return (
<section>
{isMultiroom ? (
<Header>
<Title level="h2" as="h4">
{`${intl.formatMessage({ id: "Room" })} 1`}
</Title>
</Header>
) : null}
<SelectedRoom />
{room.bedTypes ? (
<SectionAccordion
header={intl.formatMessage({ id: "Select bed" })}
label={intl.formatMessage({ id: "Request bedtype" })}
step={StepEnum.selectBed}
>
<BedType />
</SectionAccordion>
) : null}
{showBreakfastStep ? (
<SectionAccordion
header={intl.formatMessage({ id: "Food options" })}
label={intl.formatMessage({
id: "Select breakfast options",
})}
step={StepEnum.breakfast}
>
<Breakfast />
</SectionAccordion>
) : null}
<SectionAccordion
header={intl.formatMessage({ id: "Details" })}
step={StepEnum.details}
label={intl.formatMessage({ id: "Enter your details" })}
>
<Details user={user} />
</SectionAccordion>
</section>
)
}

View File

@@ -2,16 +2,10 @@
import { useEffect, useRef, useState } from "react"
import { useIntl } from "react-intl"
import { useEnterDetailsStore } from "@/stores/enter-details"
import {
selectBookingProgress,
selectRoom,
selectRoomStatus,
} from "@/stores/enter-details/helpers"
import { CheckIcon, ChevronDownIcon } from "@/components/Icons"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { useRoomContext } from "@/contexts/Details/Room"
import useStickyPosition from "@/hooks/useStickyPosition"
import styles from "./sectionAccordion.module.css"
@@ -24,32 +18,27 @@ export default function SectionAccordion({
header,
label,
step,
roomIndex,
}: React.PropsWithChildren<SectionAccordionProps>) {
const intl = useIntl()
const roomStatus = useEnterDetailsStore((state) =>
selectRoomStatus(state, roomIndex)
)
const stickyPosition = useStickyPosition({})
const setStep = useEnterDetailsStore((state) => state.actions.setStep)
const { bedType, breakfast } = useEnterDetailsStore((state) =>
selectRoom(state, roomIndex)
)
const { roomStatuses, currentRoomIndex } = useEnterDetailsStore((state) =>
selectBookingProgress(state)
)
const {
actions: { setStep },
currentStep,
isActiveRoom,
room: { bedType, breakfast },
steps,
} = useRoomContext()
const [isComplete, setIsComplete] = useState(false)
const [isOpen, setIsOpen] = useState(false)
const isValid = roomStatus.steps[step]?.isValid ?? false
const isValid = steps[step]?.isValid ?? false
const [title, setTitle] = useState(label)
const noBreakfastTitle = intl.formatMessage({ id: "No breakfast" })
const breakfastTitle = intl.formatMessage({ id: "Breakfast buffet" })
// useScrollToActiveSection(step, steps, roomStatus.currentStep === step)
// useScrollToActiveSection(step, steps, currentStep === step)
useEffect(() => {
if (step === StepEnum.selectBed && bedType) {
@@ -72,14 +61,13 @@ export default function SectionAccordion({
const accordionRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const shouldBeOpen =
roomStatus.currentStep === step && currentRoomIndex === roomIndex
const shouldBeOpen = currentStep === step && isActiveRoom
setIsOpen(shouldBeOpen)
// Scroll to this section when it is opened, but wait for the accordion animations to
// finish, else the height calculations will not be correct and the scroll position
// will be off.
// Scroll to this section when it is opened,
// but wait for the accordion animations to finish,
// else the height calculations will not be correct and
// the scroll position will be off.
if (shouldBeOpen) {
const handleTransitionEnd = () => {
if (accordionRef.current) {
@@ -103,26 +91,15 @@ export default function SectionAccordion({
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentRoomIndex, roomIndex, roomStatus.currentStep, setIsOpen, step])
}, [currentStep, isActiveRoom, setIsOpen, step])
function onModify() {
setStep(step, roomIndex)
function goToStep() {
setStep(step)
}
function close() {
setIsOpen(false)
const nextRoom = roomStatuses.find((room) => !room.isComplete)
const nextStep = nextRoom
? Object.values(nextRoom.steps).find((step) => !step.isValid)?.step
: null
if (nextRoom !== undefined && nextStep !== undefined) {
setStep(nextStep, roomStatuses.indexOf(nextRoom))
} else {
// Time for payment, collapse any open step
setStep(null)
}
goToStep()
}
const textColor =
@@ -143,7 +120,7 @@ export default function SectionAccordion({
</div>
<header className={styles.header}>
<button
onClick={isOpen ? close : onModify}
onClick={isOpen ? close : goToStep}
disabled={!isComplete}
className={styles.modifyButton}
>

View File

@@ -5,38 +5,36 @@ import { useTransition } from "react"
import { useIntl } from "react-intl"
import { selectRate } from "@/constants/routes/hotelReservation"
import { useRateSelectionStore } from "@/stores/select-rate/rate-selection"
import { useEnterDetailsStore } from "@/stores/enter-details"
import { CheckIcon, EditIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { useRoomContext } from "@/contexts/Details/Room"
import useLang from "@/hooks/useLang"
import ToggleSidePeek from "./ToggleSidePeek"
import styles from "./selectedRoom.module.css"
import type { SelectedRoomProps } from "@/types/components/hotelReservation/enterDetails/room"
export default function SelectedRoom({
hotelId,
roomType,
roomTypeCode,
rateDescription,
roomIndex,
searchParamsStr,
}: SelectedRoomProps) {
export default function SelectedRoom() {
const intl = useIntl()
const lang = useLang()
const router = useRouter()
const [isPending, startTransition] = useTransition()
const { modifyRate } = useRateSelectionStore()
const { room, roomNr } = useRoomContext()
const { hotelId, searchParamsStr } = useEnterDetailsStore((state) => ({
hotelId: state.booking.hotelId,
searchParamsStr: state.searchParamString,
}))
function changeRoom() {
modifyRate(roomIndex)
const searchParams = new URLSearchParams(searchParamsStr)
// rooms are index based, thus need for subtraction
searchParams.set("modifyRateIndex", `${roomNr - 1}`)
startTransition(() => {
router.push(`${selectRate(lang)}?${searchParamsStr}`)
router.push(`${selectRate(lang)}?${searchParams.toString()}`)
})
}
@@ -66,8 +64,8 @@ export default function SelectedRoom({
{intl.formatMessage(
{ id: "{roomType} <rate>{rateDescription}</rate>" },
{
roomType: roomType,
rateDescription,
roomType: room.roomType,
rateDescription: room.cancellationText,
rate: (str) => {
return <span className={styles.rate}>{str}</span>
},
@@ -85,12 +83,12 @@ export default function SelectedRoom({
{intl.formatMessage({ id: "Change room" })}
</Button>
</div>
{roomTypeCode && (
{room.roomTypeCode && (
<div className={styles.details}>
<ToggleSidePeek
hotelId={hotelId}
roomTypeCode={roomTypeCode}
intent="text"
roomTypeCode={room.roomTypeCode}
/>
</div>
)}

View File

@@ -8,7 +8,7 @@ import SummaryUI from "./UI"
import type { SummaryProps } from "@/types/components/hotelReservation/summary"
export default function DesktopSummary(props: SummaryProps) {
export default function DesktopSummary({ isMember }: SummaryProps) {
const {
booking,
actions: { toggleSummaryOpen },
@@ -23,8 +23,7 @@ export default function DesktopSummary(props: SummaryProps) {
<SummaryUI
booking={booking}
rooms={rooms}
isMember={props.isMember}
breakfastIncluded={props.breakfastIncluded}
isMember={isMember}
totalPrice={totalPrice}
vat={vat}
toggleSummaryOpen={toggleSummaryOpen}

View File

@@ -11,7 +11,7 @@ import styles from "./mobile.module.css"
import type { SummaryProps } from "@/types/components/hotelReservation/summary"
export default function MobileSummary(props: SummaryProps) {
export default function MobileSummary({ isMember }: SummaryProps) {
const {
booking,
actions: { toggleSummaryOpen },
@@ -22,10 +22,10 @@ export default function MobileSummary(props: SummaryProps) {
const rooms = useEnterDetailsStore((state) => state.rooms)
const showPromo =
!props.isMember &&
!isMember &&
rooms.length === 1 &&
!rooms[0].guest.join &&
!rooms[0].guest.membershipNo
!rooms[0].room.guest.join &&
!rooms[0].room.guest.membershipNo
return (
<div className={styles.mobileSummary}>
@@ -35,8 +35,7 @@ export default function MobileSummary(props: SummaryProps) {
<SummaryUI
booking={booking}
rooms={rooms}
isMember={props.isMember}
breakfastIncluded={props.breakfastIncluded}
isMember={isMember}
totalPrice={totalPrice}
vat={vat}
toggleSummaryOpen={toggleSummaryOpen}

View File

@@ -1,6 +1,6 @@
"use client"
import React from "react"
import { Fragment } from "react"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
@@ -32,7 +32,6 @@ export default function SummaryUI({
rooms,
totalPrice,
isMember,
breakfastIncluded,
vat,
toggleSummaryOpen,
}: EnterDetailsSummaryProps) {
@@ -66,9 +65,11 @@ export default function SummaryUI({
rooms.length === 1 &&
rooms
.slice(0, 1)
.some((r) => !isMember || !r.guest.join || !r.guest.membershipNo)
.some(
(r) => !isMember || !r.room.guest.join || !r.room.guest.membershipNo
)
const memberPrice = getMemberPrice(rooms[0].roomRate)
const memberPrice = getMemberPrice(rooms[0].room.roomRate)
return (
<section className={styles.summary}>
@@ -91,7 +92,7 @@ export default function SummaryUI({
</Button>
</header>
<Divider color="primaryLightSubtle" />
{rooms.map((room, idx) => {
{rooms.map(({ room }, idx) => {
const roomNumber = idx + 1
const adults = room.adults
const childrenInRoom = room.childrenInRoom
@@ -139,7 +140,7 @@ export default function SummaryUI({
}
return (
<React.Fragment key={idx}>
<Fragment key={idx}>
<div
className={styles.addOns}
data-testid={`summary-room-${roomNumber}`}
@@ -272,7 +273,7 @@ export default function SummaryUI({
</Body>
</div>
) : null}
{breakfastIncluded ? (
{room.breakfastIncluded ? (
<div className={styles.entry}>
<Body color="uiTextHighContrast">
{intl.formatMessage({ id: "Breakfast included" })}
@@ -309,7 +310,9 @@ export default function SummaryUI({
<Body color="uiTextHighContrast">
{formatPrice(
intl,
parseInt(room.breakfast.localPrice.totalPrice),
parseInt(room.breakfast.localPrice.price) *
adults *
diff,
room.breakfast.localPrice.currency
)}
</Body>
@@ -337,7 +340,7 @@ export default function SummaryUI({
) : null}
</div>
<Divider color="primaryLightSubtle" />
</React.Fragment>
</Fragment>
)
})}
<div className={styles.total}>
@@ -353,13 +356,13 @@ export default function SummaryUI({
fromDate={booking.fromDate}
toDate={booking.toDate}
rooms={rooms.map((r) => ({
adults: r.adults,
childrenInRoom: r.childrenInRoom,
roomPrice: r.roomPrice,
roomType: r.roomType,
bedType: r.bedType,
breakfast: r.breakfast,
roomFeatures: r.roomFeatures,
adults: r.room.adults,
bedType: r.room.bedType,
breakfast: r.room.breakfast,
childrenInRoom: r.room.childrenInRoom,
roomFeatures: r.room.roomFeatures,
roomPrice: r.room.roomPrice,
roomType: r.room.roomType,
}))}
totalPrice={totalPrice}
vat={vat}

View File

@@ -20,6 +20,7 @@ import SummaryUI from "./UI"
import type { PropsWithChildren } from "react"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
import { StepEnum } from "@/types/enums/step"
import type { RoomState } from "@/types/stores/enter-details"
jest.mock("@/lib/api", () => ({
@@ -42,36 +43,78 @@ function createWrapper(intlConfig: IntlConfig) {
const rooms: RoomState[] = [
{
adults: 2,
childrenInRoom: [{ bed: ChildBedMapEnum.IN_EXTRA_BED, age: 5 }],
bedType: {
description: bedType.queen.description,
roomTypeCode: bedType.queen.value,
currentStep: StepEnum.selectBed,
isComplete: false,
room: {
adults: 2,
bedType: {
description: bedType.queen.description,
roomTypeCode: bedType.queen.value,
},
bedTypes: [],
breakfast: breakfastPackage,
breakfastIncluded: false,
cancellationText: "Non-refundable",
childrenInRoom: [{ bed: ChildBedMapEnum.IN_EXTRA_BED, age: 5 }],
guest: guestDetailsNonMember,
rateDetails: [],
roomFeatures: [],
roomPrice: roomPrice,
roomRate: roomRate,
roomType: "Standard",
roomTypeCode: "QS",
},
steps: {
[StepEnum.selectBed]: {
step: StepEnum.selectBed,
isValid: false,
},
[StepEnum.breakfast]: {
step: StepEnum.breakfast,
isValid: false,
},
[StepEnum.details]: {
step: StepEnum.details,
isValid: false,
},
},
breakfast: breakfastPackage,
guest: guestDetailsNonMember,
roomRate: roomRate,
roomPrice: roomPrice,
roomType: "Standard",
rateDetails: [],
cancellationText: "Non-refundable",
roomFeatures: [],
},
{
adults: 1,
childrenInRoom: [],
bedType: {
description: bedType.king.description,
roomTypeCode: bedType.king.value,
currentStep: StepEnum.selectBed,
isComplete: false,
room: {
adults: 1,
bedType: {
description: bedType.king.description,
roomTypeCode: bedType.king.value,
},
bedTypes: [],
breakfast: undefined,
breakfastIncluded: false,
cancellationText: "Non-refundable",
childrenInRoom: [],
guest: guestDetailsMember,
rateDetails: [],
roomFeatures: [],
roomPrice: roomPrice,
roomRate: roomRate,
roomType: "Standard",
roomTypeCode: "QS",
},
steps: {
[StepEnum.selectBed]: {
step: StepEnum.selectBed,
isValid: false,
},
[StepEnum.breakfast]: {
step: StepEnum.breakfast,
isValid: false,
},
[StepEnum.details]: {
step: StepEnum.details,
isValid: false,
},
},
breakfast: undefined,
guest: guestDetailsMember,
roomRate: roomRate,
roomPrice: roomPrice,
roomType: "Standard",
rateDetails: [],
cancellationText: "Non-refundable",
roomFeatures: [],
},
]
@@ -89,7 +132,6 @@ describe("EnterDetails Summary", () => {
booking={booking}
rooms={rooms.slice(0, 1)}
isMember={false}
breakfastIncluded={false}
totalPrice={{
requested: {
currency: "EUR",
@@ -127,7 +169,6 @@ describe("EnterDetails Summary", () => {
booking={booking}
rooms={rooms}
isMember={false}
breakfastIncluded={false}
totalPrice={{
requested: {
currency: "EUR",

View File

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

View File

@@ -19,9 +19,9 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import ToggleSidePeek from "../../EnterDetails/SelectedRoom/ToggleSidePeek"
import PriceDetailsModal from "../../PriceDetailsModal"
import GuestDetails from "./GuestDetails"
import ToggleSidePeek from "./ToggleSidePeek"
import styles from "./room.module.css"

View File

@@ -164,7 +164,7 @@ export default function PriceDetailsTable({
)}
value={formatPrice(
intl,
parseInt(room.breakfast.localPrice.price),
parseInt(room.breakfast.localPrice.price) * room.adults,
room.breakfast.localPrice.currency
)}
/>
@@ -193,7 +193,9 @@ export default function PriceDetailsTable({
})}
value={formatPrice(
intl,
parseInt(room.breakfast.localPrice.totalPrice),
parseInt(room.breakfast.localPrice.totalPrice) *
room.adults *
diff,
room.breakfast.localPrice.currency
)}
/>

View File

@@ -1,6 +1,6 @@
"use client"
import { useRouter } from "next/navigation"
import { useTransition } from "react"
import { useState, useTransition } from "react"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
@@ -39,6 +39,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
searchParams: state.searchParams,
}))
const [isSubmitting, setIsSubmitting] = useState(false)
const intl = useIntl()
const router = useRouter()
const params = new URLSearchParams(searchParams)
@@ -111,6 +112,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setIsSubmitting(true)
startTransition(() => {
router.push(`details?${params}`)
})
@@ -267,7 +269,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
</div>
<Button
className={styles.continueButton}
disabled={!isAllRoomsSelected}
disabled={!isAllRoomsSelected || isSubmitting}
theme="base"
type="submit"
>

View File

@@ -1,5 +1,4 @@
"use client"
import { useSession } from "next-auth/react"
import { useIntl } from "react-intl"
import { useRatesStore } from "@/stores/select-rate"
@@ -12,19 +11,19 @@ import Chip from "@/components/TempDesignSystem/Chip"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { useRoomContext } from "@/contexts/Room"
import { isValidClientSession } from "@/utils/clientSession"
import { useRoomContext } from "@/contexts/SelectRate/Room"
import styles from "./selectedRoomPanel.module.css"
export default function SelectedRoomPanel() {
const intl = useIntl()
const { data: session } = useSession()
const isUserLoggedIn = isValidClientSession(session)
const { rateDefinitions, roomCategories } = useRatesStore((state) => ({
rateDefinitions: state.roomsAvailability?.rateDefinitions,
roomCategories: state.roomCategories,
}))
const { isUserLoggedIn, rateDefinitions, roomCategories } = useRatesStore(
(state) => ({
isUserLoggedIn: state.isUserLoggedIn,
rateDefinitions: state.roomsAvailability?.rateDefinitions,
roomCategories: state.roomCategories,
})
)
const {
actions: { modifyRate },
isMainRoom,

View File

@@ -6,7 +6,7 @@ import { useRatesStore } from "@/stores/select-rate"
import { ChevronUpIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { useRoomContext } from "@/contexts/Room"
import { useRoomContext } from "@/contexts/SelectRate/Room"
import SelectedRoomPanel from "./SelectedRoomPanel"
import { roomSelectionPanelVariants } from "./variants"

View File

@@ -1,14 +1,13 @@
import { useSearchParams } from "next/navigation"
import { useSession } from "next-auth/react"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import { useRatesStore } from "@/stores/select-rate"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { useRoomContext } from "@/contexts/Room"
import { isValidClientSession } from "@/utils/clientSession"
import { useRoomContext } from "@/contexts/SelectRate/Room"
import { calculatePricesPerNight } from "./utils"
@@ -24,8 +23,7 @@ export default function PriceList({
}: PriceListProps) {
const intl = useIntl()
const { isMainRoom } = useRoomContext()
const { data: session } = useSession()
const isUserLoggedIn = isValidClientSession(session)
const isUserLoggedIn = useRatesStore((state) => state.isUserLoggedIn)
const { localPrice: publicLocalPrice, requestedPrice: publicRequestedPrice } =
publicPrice
@@ -166,20 +164,20 @@ export default function PriceList({
<Caption color="uiTextMediumContrast">
{isUserLoggedIn
? intl.formatMessage(
{ id: "{memberPrice} {currency}" },
{
memberPrice: totalMemberRequestedPricePerNight,
currency: publicRequestedPrice.currency,
}
)
{ id: "{memberPrice} {currency}" },
{
memberPrice: totalMemberRequestedPricePerNight,
currency: publicRequestedPrice.currency,
}
)
: intl.formatMessage(
{ id: "{publicPrice}/{memberPrice} {currency}" },
{
publicPrice: totalPublicRequestedPricePerNight,
memberPrice: totalMemberRequestedPricePerNight,
currency: publicRequestedPrice.currency,
}
)}
{ id: "{publicPrice}/{memberPrice} {currency}" },
{
publicPrice: totalPublicRequestedPricePerNight,
memberPrice: totalMemberRequestedPricePerNight,
currency: publicRequestedPrice.currency,
}
)}
</Caption>
</dd>
</div>

View File

@@ -1,15 +1,15 @@
"use client"
import { useSession } from "next-auth/react"
import { useIntl } from "react-intl"
import { useRatesStore } from "@/stores/select-rate"
import { CheckIcon, InfoCircleIcon } from "@/components/Icons"
import Modal from "@/components/Modal"
import Button from "@/components/TempDesignSystem/Button"
import Label from "@/components/TempDesignSystem/Form/Label"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import { useRoomContext } from "@/contexts/Room"
import { isValidClientSession } from "@/utils/clientSession"
import { useRoomContext } from "@/contexts/SelectRate/Room"
import PriceTable from "./PriceList"
@@ -30,8 +30,7 @@ export default function FlexibilityOption({
rateTitle,
}: FlexibilityOptionProps) {
const intl = useIntl()
const { data: session } = useSession()
const isUserLoggedIn = isValidClientSession(session)
const isUserLoggedIn = useRatesStore((state) => state.isUserLoggedIn)
const {
actions: { selectRate },
isMainRoom,

View File

@@ -1,7 +1,6 @@
"use client"
import { useSearchParams } from "next/navigation"
import { useSession } from "next-auth/react"
import { createElement } from "react"
import { useIntl } from "react-intl"
@@ -15,8 +14,7 @@ import ImageGallery from "@/components/ImageGallery"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { useRoomContext } from "@/contexts/Room"
import { isValidClientSession } from "@/utils/clientSession"
import { useRoomContext } from "@/contexts/SelectRate/Room"
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
import { cardVariants } from "./cardVariants"
@@ -71,8 +69,6 @@ function getBreakfastMessage(
}
export default function RoomCard({ roomConfiguration }: RoomCardProps) {
const { data: session } = useSession()
const isUserLoggedIn = isValidClientSession(session)
const intl = useIntl()
const lessThanFiveRoomsLeft =
roomConfiguration.roomsLeft > 0 && roomConfiguration.roomsLeft < 5
@@ -83,12 +79,14 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
const {
hotelId,
hotelType,
isUserLoggedIn,
petRoomPackage,
rateDefinitions,
roomCategories,
} = useRatesStore((state) => ({
hotelId: state.booking.hotelId,
hotelType: state.hotelType,
isUserLoggedIn: state.isUserLoggedIn,
petRoomPackage: state.petRoomPackage,
rateDefinitions: state.roomsAvailability?.rateDefinitions,
roomCategories: state.roomCategories,
@@ -362,7 +360,7 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
product.productType.member?.rateCode !== undefined)
return (
<FlexibilityOption
key={product.productType.public.rateCode}
key={rate.title}
features={roomConfiguration.features}
isSelected={
isSelectedRateCode &&

View File

@@ -9,7 +9,7 @@ import { useIntl } from "react-intl"
import { useRatesStore } from "@/stores/select-rate"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import { useRoomContext } from "@/contexts/Room"
import { useRoomContext } from "@/contexts/SelectRate/Room"
import styles from "./roomFilter.module.css"

View File

@@ -7,7 +7,7 @@ import { useBookingCodeFilterStore } from "@/stores/bookingCode-filter"
import BookingCodeFilter from "@/components/HotelReservation/SelectHotel/BookingCodeFilter"
import Alert from "@/components/TempDesignSystem/Alert"
import { useRoomContext } from "@/contexts/Room"
import { useRoomContext } from "@/contexts/SelectRate/Room"
import useLang from "@/hooks/useLang"
import RoomCard from "./RoomCard"

View File

@@ -3,7 +3,7 @@ import { useEffect } from "react"
import { useRatesStore } from "@/stores/select-rate"
import RoomProvider from "@/providers/RoomProvider"
import RoomProvider from "@/providers/SelectRate/RoomProvider"
import { trackLowestRoomPrice } from "@/utils/tracking"
import MultiRoomWrapper from "./MultiRoomWrapper"

View File

@@ -1,11 +1,8 @@
"use client"
import { useSession } from "next-auth/react"
import { dt } from "@/lib/dt"
import useLang from "@/hooks/useLang"
import RatesProvider from "@/providers/RatesProvider"
import { isValidClientSession } from "@/utils/clientSession"
import { useHotelPackages, useRoomsAvailability } from "../utils"
import RateSummary from "./RateSummary"
@@ -19,12 +16,11 @@ export function RoomsContainer({
booking,
childArray,
fromDate,
hotelId,
hotelData,
hotelId,
isUserLoggedIn,
toDate,
}: RoomsContainerProps) {
const { data: session } = useSession()
const isUserLoggedIn = isValidClientSession(session)
const lang = useLang()
const fromDateString = dt(fromDate).format("YYYY-MM-DD")

View File

@@ -7,12 +7,14 @@ import { getHotel } from "@/lib/trpc/memoizedRequests"
import { getValidDates } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/getValidDates"
import { getHotelSearchDetails } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils"
import { auth } from "@/auth"
import HotelInfoCard, {
HotelInfoCardSkeleton,
} from "@/components/HotelReservation/SelectRate/HotelInfoCard"
import { RoomsContainer } from "@/components/HotelReservation/SelectRate/RoomsContainer"
import TrackingSDK from "@/components/TrackingSDK"
import { setLang } from "@/i18n/serverContext"
import { isValidSession } from "@/utils/session"
import { convertSearchParamsToObj } from "@/utils/url"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
@@ -47,6 +49,9 @@ export default async function SelectRatePage({
language: params.lang,
})
const session = await auth()
const isUserLoggedIn = isValidSession(session)
const arrivalDate = fromDate.toDate()
const departureDate = toDate.toDate()
@@ -92,12 +97,13 @@ export default async function SelectRatePage({
</Suspense>
<RoomsContainer
hotelData={hotelData}
adultArray={adultsInRoom}
booking={booking}
childArray={childrenInRoom}
fromDate={arrivalDate}
hotelData={hotelData}
hotelId={hotelId}
isUserLoggedIn={isUserLoggedIn}
toDate={departureDate}
/>