feat: add multiroom signup
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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"),
|
||||
})
|
||||
@@ -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"
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.header {
|
||||
padding-bottom: var(--Spacing-x3);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import styles from "./header.module.css"
|
||||
|
||||
export default function Header({ children }: React.PropsWithChildren) {
|
||||
return <header className={styles.header}>{children}</header>
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user