feat: make steps of enter details flow dynamic depending on data
This commit is contained in:
committed by
Joakim Jäderberg
parent
d49e301634
commit
c6fc500d9e
@@ -35,7 +35,7 @@ export default function Form({
|
||||
const locationData: Location = JSON.parse(decodeURIComponent(data.location))
|
||||
|
||||
const bookingFlowPage =
|
||||
locationData.type == "cities" ? selectHotel[lang] : selectRate[lang]
|
||||
locationData.type == "cities" ? selectHotel(lang) : selectRate(lang)
|
||||
const bookingWidgetParams = new URLSearchParams(data.date)
|
||||
|
||||
if (locationData.type == "cities")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
.form {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
|
||||
padding-bottom: var(--Spacing-x3);
|
||||
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
|
||||
width: min(600px, 100%);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useCallback, useEffect } from "react"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
import { useDetailsStore } from "@/stores/details"
|
||||
import { useStepsStore } from "@/stores/steps"
|
||||
|
||||
import { KingBedIcon } from "@/components/Icons"
|
||||
import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio"
|
||||
@@ -19,22 +20,18 @@ import type {
|
||||
} from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||
|
||||
export default function BedType({ bedTypes }: BedTypeProps) {
|
||||
const bedType = useEnterDetailsStore((state) => state.userData.bedType)
|
||||
const bedType = useDetailsStore((state) => state.data.bedType?.roomTypeCode)
|
||||
const completeStep = useStepsStore((state) => state.completeStep)
|
||||
const updateBedType = useDetailsStore((state) => state.actions.updateBedType)
|
||||
|
||||
const methods = useForm<BedTypeFormSchema>({
|
||||
defaultValues: bedType?.roomTypeCode
|
||||
? {
|
||||
bedType: bedType.roomTypeCode,
|
||||
}
|
||||
: undefined,
|
||||
defaultValues: bedType ? { bedType } : undefined,
|
||||
criteriaMode: "all",
|
||||
mode: "all",
|
||||
resolver: zodResolver(bedTypeFormSchema),
|
||||
reValidateMode: "onChange",
|
||||
})
|
||||
|
||||
const completeStep = useEnterDetailsStore((state) => state.completeStep)
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(bedTypeRoomCode: BedTypeFormSchema) => {
|
||||
const matchingRoom = bedTypes.find(
|
||||
@@ -45,10 +42,11 @@ export default function BedType({ bedTypes }: BedTypeProps) {
|
||||
description: matchingRoom.description,
|
||||
roomTypeCode: matchingRoom.value,
|
||||
}
|
||||
completeStep({ bedType })
|
||||
updateBedType(bedType)
|
||||
completeStep()
|
||||
}
|
||||
},
|
||||
[completeStep, bedTypes]
|
||||
[bedTypes, completeStep, updateBedType]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -2,6 +2,5 @@
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
|
||||
padding-bottom: var(--Spacing-x3);
|
||||
width: min(600px, 100%);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import { useCallback, useEffect } from "react"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
import { useDetailsStore } from "@/stores/details"
|
||||
import { useStepsStore } from "@/stores/steps"
|
||||
|
||||
import { Highlight } from "@/components/TempDesignSystem/Form/ChoiceCard/_Card"
|
||||
import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio"
|
||||
@@ -23,34 +24,37 @@ import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
export default function Breakfast({ packages }: BreakfastProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
const breakfast = useEnterDetailsStore((state) => state.userData.breakfast)
|
||||
const breakfast = useDetailsStore(({ data }) =>
|
||||
data.breakfast
|
||||
? data.breakfast.code
|
||||
: data.breakfast === false
|
||||
? "false"
|
||||
: data.breakfast
|
||||
)
|
||||
const updateBreakfast = useDetailsStore(
|
||||
(state) => state.actions.updateBreakfast
|
||||
)
|
||||
const completeStep = useStepsStore((state) => state.completeStep)
|
||||
|
||||
let defaultValues = undefined
|
||||
if (breakfast === BreakfastPackageEnum.NO_BREAKFAST) {
|
||||
defaultValues = { breakfast: BreakfastPackageEnum.NO_BREAKFAST }
|
||||
} else if (breakfast?.code) {
|
||||
defaultValues = { breakfast: breakfast.code }
|
||||
}
|
||||
const methods = useForm<BreakfastFormSchema>({
|
||||
defaultValues,
|
||||
defaultValues: breakfast ? { breakfast } : undefined,
|
||||
criteriaMode: "all",
|
||||
mode: "all",
|
||||
resolver: zodResolver(breakfastFormSchema),
|
||||
reValidateMode: "onChange",
|
||||
})
|
||||
|
||||
const completeStep = useEnterDetailsStore((state) => state.completeStep)
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(values: BreakfastFormSchema) => {
|
||||
const pkg = packages?.find((p) => p.code === values.breakfast)
|
||||
if (pkg) {
|
||||
completeStep({ breakfast: pkg })
|
||||
updateBreakfast(pkg)
|
||||
} else {
|
||||
completeStep({ breakfast: BreakfastPackageEnum.NO_BREAKFAST })
|
||||
updateBreakfast(false)
|
||||
}
|
||||
completeStep()
|
||||
},
|
||||
[completeStep, packages]
|
||||
[completeStep, packages, updateBreakfast]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -61,10 +65,6 @@ export default function Breakfast({ packages }: BreakfastProps) {
|
||||
return () => subscription.unsubscribe()
|
||||
}, [methods, onSubmit])
|
||||
|
||||
if (!packages) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}>
|
||||
@@ -100,7 +100,6 @@ export default function Breakfast({ packages }: BreakfastProps) {
|
||||
/>
|
||||
))}
|
||||
<RadioCard
|
||||
id={BreakfastPackageEnum.NO_BREAKFAST}
|
||||
name="breakfast"
|
||||
subtitle={intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
@@ -113,7 +112,7 @@ export default function Breakfast({ packages }: BreakfastProps) {
|
||||
id: "You can always change your mind later and add breakfast at the hotel.",
|
||||
})}
|
||||
title={intl.formatMessage({ id: "No breakfast" })}
|
||||
value={BreakfastPackageEnum.NO_BREAKFAST}
|
||||
value="false"
|
||||
/>
|
||||
</form>
|
||||
</FormProvider>
|
||||
|
||||
@@ -2,14 +2,10 @@ import { z } from "zod"
|
||||
|
||||
import { breakfastPackageSchema } from "@/server/routers/hotels/output"
|
||||
|
||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
|
||||
export const breakfastStoreSchema = z.object({
|
||||
breakfast: breakfastPackageSchema.or(
|
||||
z.literal(BreakfastPackageEnum.NO_BREAKFAST)
|
||||
),
|
||||
breakfast: breakfastPackageSchema.or(z.literal(false)),
|
||||
})
|
||||
|
||||
export const breakfastFormSchema = z.object({
|
||||
breakfast: z.string().or(z.literal(BreakfastPackageEnum.NO_BREAKFAST)),
|
||||
breakfast: z.string().or(z.literal("false")),
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
.form {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
padding: var(--Spacing-x3) 0px;
|
||||
}
|
||||
|
||||
.container {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useCallback } from "react"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
import { useDetailsStore } from "@/stores/details"
|
||||
import { useStepsStore } from "@/stores/steps"
|
||||
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
||||
@@ -24,19 +26,22 @@ import type {
|
||||
const formID = "enter-details"
|
||||
export default function Details({ user }: DetailsProps) {
|
||||
const intl = useIntl()
|
||||
const initialData = useEnterDetailsStore((state) => ({
|
||||
countryCode: state.userData.countryCode,
|
||||
email: state.userData.email,
|
||||
firstName: state.userData.firstName,
|
||||
lastName: state.userData.lastName,
|
||||
phoneNumber: state.userData.phoneNumber,
|
||||
join: state.userData.join,
|
||||
dateOfBirth: state.userData.dateOfBirth,
|
||||
zipCode: state.userData.zipCode,
|
||||
termsAccepted: state.userData.termsAccepted,
|
||||
membershipNo: state.userData.membershipNo,
|
||||
const initialData = useDetailsStore((state) => ({
|
||||
countryCode: state.data.countryCode,
|
||||
email: state.data.email,
|
||||
firstName: state.data.firstName,
|
||||
lastName: state.data.lastName,
|
||||
phoneNumber: state.data.phoneNumber,
|
||||
join: state.data.join,
|
||||
dateOfBirth: state.data.dateOfBirth,
|
||||
zipCode: state.data.zipCode,
|
||||
termsAccepted: state.data.termsAccepted,
|
||||
membershipNo: state.data.membershipNo,
|
||||
}))
|
||||
|
||||
const updateDetails = useDetailsStore((state) => state.actions.updateDetails)
|
||||
const completeStep = useStepsStore((state) => state.completeStep)
|
||||
|
||||
const methods = useForm<DetailsSchema>({
|
||||
defaultValues: {
|
||||
countryCode: user?.address?.countryCode ?? initialData.countryCode,
|
||||
@@ -56,14 +61,20 @@ export default function Details({ user }: DetailsProps) {
|
||||
reValidateMode: "onChange",
|
||||
})
|
||||
|
||||
const completeStep = useEnterDetailsStore((state) => state.completeStep)
|
||||
const onSubmit = useCallback(
|
||||
(values: DetailsSchema) => {
|
||||
updateDetails(values)
|
||||
completeStep()
|
||||
},
|
||||
[completeStep, updateDetails]
|
||||
)
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form
|
||||
className={styles.form}
|
||||
id={formID}
|
||||
onSubmit={methods.handleSubmit(completeStep)}
|
||||
onSubmit={methods.handleSubmit(onSubmit)}
|
||||
>
|
||||
{user ? null : <Signup name="join" />}
|
||||
<Footnote
|
||||
@@ -107,7 +118,7 @@ export default function Details({ user }: DetailsProps) {
|
||||
readOnly={!!user}
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
{user ? null : (
|
||||
{user || methods.watch("join") ? null : (
|
||||
<Input
|
||||
className={styles.membershipNo}
|
||||
label={intl.formatMessage({ id: "Membership no" })}
|
||||
@@ -119,7 +130,6 @@ export default function Details({ user }: DetailsProps) {
|
||||
<footer className={styles.footer}>
|
||||
<Button
|
||||
disabled={!methods.formState.isValid}
|
||||
form={formID}
|
||||
intent="secondary"
|
||||
size="small"
|
||||
theme="base"
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
import { useCallback, useEffect } from "react"
|
||||
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
import { useStepsStore } from "@/stores/steps"
|
||||
|
||||
export default function HistoryStateManager() {
|
||||
const setCurrentStep = useEnterDetailsStore((state) => state.setCurrentStep)
|
||||
const currentStep = useEnterDetailsStore((state) => state.currentStep)
|
||||
const setCurrentStep = useStepsStore((state) => state.setStep)
|
||||
const currentStep = useStepsStore((state) => state.currentStep)
|
||||
|
||||
const handleBackButton = useCallback(
|
||||
(event: PopStateEvent) => {
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from "@/constants/currentWebHrefs"
|
||||
import { env } from "@/env/client"
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
import { useDetailsStore } from "@/stores/details"
|
||||
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
@@ -40,7 +40,6 @@ import styles from "./payment.module.css"
|
||||
|
||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
import { PaymentProps } from "@/types/components/hotelReservation/selectRate/section"
|
||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
|
||||
const maxRetries = 4
|
||||
const retryInterval = 2000
|
||||
@@ -61,12 +60,9 @@ export default function Payment({
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
const queryParams = useSearchParams()
|
||||
const { userData, roomData, setIsSubmittingDisabled } = useEnterDetailsStore(
|
||||
(state) => ({
|
||||
userData: state.userData,
|
||||
roomData: state.roomData,
|
||||
setIsSubmittingDisabled: state.setIsSubmittingDisabled,
|
||||
})
|
||||
const { booking, ...userData } = useDetailsStore((state) => state.data)
|
||||
const setIsSubmittingDisabled = useDetailsStore(
|
||||
(state) => state.actions.setIsSubmittingDisabled
|
||||
)
|
||||
|
||||
const {
|
||||
@@ -82,7 +78,7 @@ export default function Payment({
|
||||
dateOfBirth,
|
||||
zipCode,
|
||||
} = userData
|
||||
const { toDate, fromDate, rooms: rooms, hotel } = roomData
|
||||
const { toDate, fromDate, rooms, hotel } = booking
|
||||
|
||||
const [confirmationNumber, setConfirmationNumber] = useState<string>("")
|
||||
const [availablePaymentOptions, setAvailablePaymentOptions] =
|
||||
@@ -204,7 +200,7 @@ export default function Payment({
|
||||
postalCode: zipCode,
|
||||
},
|
||||
packages: {
|
||||
breakfast: breakfast !== BreakfastPackageEnum.NO_BREAKFAST,
|
||||
breakfast: !!(breakfast && breakfast.code),
|
||||
allergyFriendly:
|
||||
room.packages?.includes(RoomPackageCodeEnum.ALLERGY_ROOM) ?? false,
|
||||
petFriendly:
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
"use client"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { PropsWithChildren, useRef } from "react"
|
||||
|
||||
import {
|
||||
EnterDetailsContext,
|
||||
type EnterDetailsStore,
|
||||
initEditDetailsState,
|
||||
} from "@/stores/enter-details"
|
||||
|
||||
import { EnterDetailsProviderProps } from "@/types/components/hotelReservation/enterDetails/store"
|
||||
|
||||
export default function EnterDetailsProvider({
|
||||
step,
|
||||
isMember,
|
||||
children,
|
||||
}: PropsWithChildren<EnterDetailsProviderProps>) {
|
||||
const searchParams = useSearchParams()
|
||||
const initialStore = useRef<EnterDetailsStore>()
|
||||
if (!initialStore.current) {
|
||||
initialStore.current = initEditDetailsState(step, searchParams, isMember)
|
||||
}
|
||||
|
||||
return (
|
||||
<EnterDetailsContext.Provider value={initialStore.current}>
|
||||
{children}
|
||||
</EnterDetailsContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
import { useDetailsStore } from "@/stores/details"
|
||||
import { useStepsStore } from "@/stores/steps"
|
||||
|
||||
import { CheckIcon, ChevronDownIcon } from "@/components/Icons"
|
||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||
@@ -10,12 +11,9 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
|
||||
import styles from "./sectionAccordion.module.css"
|
||||
|
||||
import {
|
||||
StepEnum,
|
||||
StepStoreKeys,
|
||||
} from "@/types/components/hotelReservation/enterDetails/step"
|
||||
import { StepStoreKeys } from "@/types/components/hotelReservation/enterDetails/step"
|
||||
import { SectionAccordionProps } from "@/types/components/hotelReservation/selectRate/sectionAccordion"
|
||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
import { StepEnum } from "@/types/enums/step"
|
||||
|
||||
export default function SectionAccordion({
|
||||
header,
|
||||
@@ -24,12 +22,12 @@ export default function SectionAccordion({
|
||||
children,
|
||||
}: React.PropsWithChildren<SectionAccordionProps>) {
|
||||
const intl = useIntl()
|
||||
const currentStep = useEnterDetailsStore((state) => state.currentStep)
|
||||
const currentStep = useStepsStore((state) => state.currentStep)
|
||||
const [isComplete, setIsComplete] = useState(false)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const isValid = useEnterDetailsStore((state) => state.isValid[step])
|
||||
const navigate = useEnterDetailsStore((state) => state.navigate)
|
||||
const stepData = useEnterDetailsStore((state) => state.userData)
|
||||
const isValid = useDetailsStore((state) => state.isValid[step])
|
||||
const navigate = useStepsStore((state) => state.navigate)
|
||||
const stepData = useDetailsStore((state) => state.data)
|
||||
const stepStoreKey = StepStoreKeys[step]
|
||||
const [title, setTitle] = useState(label)
|
||||
|
||||
@@ -39,9 +37,12 @@ export default function SectionAccordion({
|
||||
value && setTitle(value.description)
|
||||
}
|
||||
// If breakfast step, check if an option has been selected
|
||||
if (step === StepEnum.breakfast && stepData.breakfast) {
|
||||
if (
|
||||
step === StepEnum.breakfast &&
|
||||
(stepData.breakfast || stepData.breakfast === false)
|
||||
) {
|
||||
const value = stepData.breakfast
|
||||
if (value === BreakfastPackageEnum.NO_BREAKFAST) {
|
||||
if (value === false) {
|
||||
setTitle(intl.formatMessage({ id: "No breakfast" }))
|
||||
} else {
|
||||
setTitle(intl.formatMessage({ id: "Breakfast buffet" }))
|
||||
@@ -94,7 +95,9 @@ export default function SectionAccordion({
|
||||
)}
|
||||
</button>
|
||||
</header>
|
||||
<div className={styles.content}>{children}</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.contentWrapper}>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
|
||||
.main {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3);
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
|
||||
padding-bottom: var(--Spacing-x3);
|
||||
@@ -80,6 +79,10 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.contentWrapper {
|
||||
padding-top: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.wrapper {
|
||||
gap: var(--Spacing-x3);
|
||||
@@ -98,4 +101,4 @@
|
||||
content: "";
|
||||
border-left: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
import { selectRate } from "@/constants/routes/hotelReservation"
|
||||
|
||||
import { CheckIcon, EditIcon } from "@/components/Icons"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import ToggleSidePeek from "./ToggleSidePeek"
|
||||
|
||||
@@ -21,8 +22,7 @@ export default function SelectedRoom({
|
||||
rateDescription,
|
||||
}: SelectedRoomProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
const selectRateUrl = useEnterDetailsStore((state) => state.selectRateUrl)
|
||||
const lang = useLang()
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
@@ -53,7 +53,8 @@ export default function SelectedRoom({
|
||||
<Link
|
||||
className={styles.button}
|
||||
color="burgundy"
|
||||
href={selectRateUrl}
|
||||
href={selectRate(lang)}
|
||||
keepSearchParams
|
||||
size="small"
|
||||
variant="icon"
|
||||
>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { PropsWithChildren } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
import { useDetailsStore } from "@/stores/details"
|
||||
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
@@ -17,9 +17,9 @@ export function SummaryBottomSheet({ children }: PropsWithChildren) {
|
||||
const intl = useIntl()
|
||||
|
||||
const { isSummaryOpen, toggleSummaryOpen, totalPrice, isSubmittingDisabled } =
|
||||
useEnterDetailsStore((state) => ({
|
||||
useDetailsStore((state) => ({
|
||||
isSummaryOpen: state.isSummaryOpen,
|
||||
toggleSummaryOpen: state.toggleSummaryOpen,
|
||||
toggleSummaryOpen: state.actions.toggleSummaryOpen,
|
||||
totalPrice: state.totalPrice,
|
||||
isSubmittingDisabled: state.isSubmittingDisabled,
|
||||
}))
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ChevronDown } from "react-feather"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
import { EnterDetailsState, useEnterDetailsStore } from "@/stores/enter-details"
|
||||
import { useDetailsStore } from "@/stores/details"
|
||||
|
||||
import { ArrowRightIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
@@ -18,45 +18,39 @@ import useLang from "@/hooks/useLang"
|
||||
|
||||
import styles from "./summary.module.css"
|
||||
|
||||
import { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||
import { RoomsData } from "@/types/components/hotelReservation/enterDetails/bookingData"
|
||||
import { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast"
|
||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||
import type { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast"
|
||||
import type { SummaryProps } from "@/types/components/hotelReservation/enterDetails/summary"
|
||||
import type { DetailsState } from "@/types/stores/details"
|
||||
|
||||
function storeSelector(state: EnterDetailsState) {
|
||||
function storeSelector(state: DetailsState) {
|
||||
return {
|
||||
fromDate: state.roomData.fromDate,
|
||||
toDate: state.roomData.toDate,
|
||||
bedType: state.userData.bedType,
|
||||
breakfast: state.userData.breakfast,
|
||||
toggleSummaryOpen: state.toggleSummaryOpen,
|
||||
setTotalPrice: state.setTotalPrice,
|
||||
fromDate: state.data.booking.fromDate,
|
||||
toDate: state.data.booking.toDate,
|
||||
bedType: state.data.bedType,
|
||||
breakfast: state.data.breakfast,
|
||||
toggleSummaryOpen: state.actions.toggleSummaryOpen,
|
||||
setTotalPrice: state.actions.setTotalPrice,
|
||||
totalPrice: state.totalPrice,
|
||||
}
|
||||
}
|
||||
|
||||
export default function Summary({
|
||||
showMemberPrice,
|
||||
room,
|
||||
}: {
|
||||
showMemberPrice: boolean
|
||||
room: RoomsData
|
||||
}) {
|
||||
export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
||||
const [chosenBed, setChosenBed] = useState<BedTypeSchema>()
|
||||
const [chosenBreakfast, setChosenBreakfast] = useState<
|
||||
BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST
|
||||
BreakfastPackage | false
|
||||
>()
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const {
|
||||
fromDate,
|
||||
toDate,
|
||||
bedType,
|
||||
breakfast,
|
||||
fromDate,
|
||||
setTotalPrice,
|
||||
totalPrice,
|
||||
toDate,
|
||||
toggleSummaryOpen,
|
||||
} = useEnterDetailsStore(storeSelector)
|
||||
totalPrice,
|
||||
} = useDetailsStore(storeSelector)
|
||||
|
||||
const diff = dt(toDate).diff(fromDate, "days")
|
||||
|
||||
@@ -88,36 +82,39 @@ export default function Summary({
|
||||
setChosenBed(bedType)
|
||||
setChosenBreakfast(breakfast)
|
||||
|
||||
if (breakfast && breakfast !== BreakfastPackageEnum.NO_BREAKFAST) {
|
||||
setTotalPrice({
|
||||
local: {
|
||||
price: roomsPriceLocal + parseInt(breakfast.localPrice.totalPrice),
|
||||
currency: room.localPrice.currency,
|
||||
},
|
||||
euro:
|
||||
room.euroPrice && roomsPriceEuro
|
||||
? {
|
||||
if (breakfast || breakfast === false) {
|
||||
setChosenBreakfast(breakfast)
|
||||
if (breakfast === false) {
|
||||
setTotalPrice({
|
||||
local: {
|
||||
price: roomsPriceLocal,
|
||||
currency: room.localPrice.currency,
|
||||
},
|
||||
euro:
|
||||
room.euroPrice && roomsPriceEuro
|
||||
? {
|
||||
price: roomsPriceEuro,
|
||||
currency: room.euroPrice.currency,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
} else {
|
||||
setTotalPrice({
|
||||
local: {
|
||||
price: roomsPriceLocal + parseInt(breakfast.localPrice.totalPrice),
|
||||
currency: room.localPrice.currency,
|
||||
},
|
||||
euro:
|
||||
room.euroPrice && roomsPriceEuro
|
||||
? {
|
||||
price:
|
||||
roomsPriceEuro +
|
||||
parseInt(breakfast.requestedPrice.totalPrice),
|
||||
currency: room.euroPrice.currency,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
} else {
|
||||
setTotalPrice({
|
||||
local: {
|
||||
price: roomsPriceLocal,
|
||||
currency: room.localPrice.currency,
|
||||
},
|
||||
euro:
|
||||
room.euroPrice && roomsPriceEuro
|
||||
? {
|
||||
price: roomsPriceEuro,
|
||||
currency: room.euroPrice.currency,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [
|
||||
bedType,
|
||||
@@ -187,24 +184,24 @@ export default function Summary({
|
||||
</div>
|
||||
{room.packages
|
||||
? room.packages.map((roomPackage) => (
|
||||
<div className={styles.entry} key={roomPackage.code}>
|
||||
<div>
|
||||
<Body color="uiTextHighContrast">
|
||||
{roomPackage.description}
|
||||
</Body>
|
||||
</div>
|
||||
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{
|
||||
amount: roomPackage.localPrice.price,
|
||||
currency: roomPackage.localPrice.currency,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
<div className={styles.entry} key={roomPackage.code}>
|
||||
<div>
|
||||
<Body color="uiTextHighContrast">
|
||||
{roomPackage.description}
|
||||
</Body>
|
||||
</div>
|
||||
))
|
||||
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{
|
||||
amount: roomPackage.localPrice.price,
|
||||
currency: roomPackage.localPrice.currency,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
{chosenBed ? (
|
||||
<div className={styles.entry}>
|
||||
@@ -224,37 +221,36 @@ export default function Summary({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{chosenBreakfast ? (
|
||||
chosenBreakfast === BreakfastPackageEnum.NO_BREAKFAST ? (
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "No breakfast" })}
|
||||
</Body>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{ amount: "0", currency: room.localPrice.currency }
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "Breakfast buffet" })}
|
||||
</Body>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{
|
||||
amount: chosenBreakfast.localPrice.totalPrice,
|
||||
currency: chosenBreakfast.localPrice.currency,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
{chosenBreakfast === false ? (
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "No breakfast" })}
|
||||
</Body>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{ amount: "0", currency: room.localPrice.currency }
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
) : chosenBreakfast?.code ? (
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "Breakfast buffet" })}
|
||||
</Body>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{
|
||||
amount: chosenBreakfast.localPrice.totalPrice,
|
||||
currency: chosenBreakfast.localPrice.currency,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
</div >
|
||||
<Divider color="primaryLightSubtle" />
|
||||
<div className={styles.total}>
|
||||
<div className={styles.entry}>
|
||||
@@ -295,6 +291,6 @@ export default function Summary({
|
||||
</div>
|
||||
<Divider className={styles.bottomDivider} color="primaryLightSubtle" />
|
||||
</div>
|
||||
</section>
|
||||
</section >
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export default function HotelPriceList({
|
||||
className={styles.button}
|
||||
>
|
||||
<Link
|
||||
href={`${selectRate[lang]}?hotel=${hotelId}`}
|
||||
href={`${selectRate(lang)}?hotel=${hotelId}`}
|
||||
color="none"
|
||||
keepSearchParams
|
||||
>
|
||||
|
||||
@@ -93,7 +93,7 @@ export default function HotelCard({
|
||||
</address>
|
||||
<Link
|
||||
className={styles.addressMobile}
|
||||
href={`${selectHotelMap[lang]}?selectedHotel=${hotelData.name}`}
|
||||
href={`${selectHotelMap(lang)}?selectedHotel=${hotelData.name}`}
|
||||
keepSearchParams
|
||||
>
|
||||
<Caption color="baseTextMediumContrast" type="underline">
|
||||
|
||||
@@ -104,7 +104,7 @@ export default function HotelCardDialog({
|
||||
|
||||
<Button asChild theme="base" size="small" className={styles.button}>
|
||||
<Link
|
||||
href={`${selectRate[lang]}?hotel=${data.operaId}`}
|
||||
href={`${selectRate(lang)}?hotel=${data.operaId}`}
|
||||
color="none"
|
||||
keepSearchParams
|
||||
>
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
.hotelSelectionHeader {
|
||||
background-color: var(--Base-Surface-Subtle-Normal);
|
||||
padding: var(--Spacing-x3) var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.hotelSelectionHeaderWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x3);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.titleContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.descriptionContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.address {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.dividerContainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.hotelSelectionHeader {
|
||||
padding: var(--Spacing-x4) 0;
|
||||
}
|
||||
|
||||
.hotelSelectionHeaderWrapper {
|
||||
flex-direction: row;
|
||||
gap: var(--Spacing-x6);
|
||||
margin: 0 auto;
|
||||
/* simulates padding on viewport smaller than --max-width-navigation */
|
||||
width: min(
|
||||
calc(100dvw - (var(--Spacing-x2) * 2)),
|
||||
var(--max-width-navigation)
|
||||
);
|
||||
}
|
||||
|
||||
.titleContainer > h1 {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dividerContainer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.address {
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import styles from "./hotelSelectionHeader.module.css"
|
||||
|
||||
import { HotelSelectionHeaderProps } from "@/types/components/hotelReservation/selectRate/hotelSelectionHeader"
|
||||
|
||||
export default function HotelSelectionHeader({
|
||||
hotel,
|
||||
}: HotelSelectionHeaderProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<header className={styles.hotelSelectionHeader}>
|
||||
<div className={styles.hotelSelectionHeaderWrapper}>
|
||||
<div className={styles.titleContainer}>
|
||||
<Title as="h3" level="h1">
|
||||
{hotel.name}
|
||||
</Title>
|
||||
<address className={styles.address}>
|
||||
<Caption color="textMediumContrast">
|
||||
{hotel.address.streetAddress}, {hotel.address.city}
|
||||
</Caption>
|
||||
<div>
|
||||
<Divider variant="vertical" color="subtle" />
|
||||
</div>
|
||||
<Caption color="textMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "Distance in km to city centre" },
|
||||
{ number: hotel.location.distanceToCentre }
|
||||
)}
|
||||
</Caption>
|
||||
</address>
|
||||
</div>
|
||||
<div className={styles.dividerContainer}>
|
||||
<Divider variant="vertical" color="subtle" />
|
||||
</div>
|
||||
<div className={styles.descriptionContainer}>
|
||||
<Body color="baseTextHighContrast">
|
||||
{hotel.hotelContent.texts.descriptions.short}
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export default function MobileMapButtonContainer({
|
||||
<div className={styles.buttonContainer}>
|
||||
<Button asChild variant="icon" intent="secondary" size="small">
|
||||
<Link
|
||||
href={`${selectHotelMap[lang]}`}
|
||||
href={selectHotelMap(lang)}
|
||||
keepSearchParams
|
||||
color="burgundy"
|
||||
>
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function SelectHotelMap({
|
||||
}
|
||||
|
||||
function handlePageRedirect() {
|
||||
router.push(`${selectHotel[lang]}?${searchParams.toString()}`)
|
||||
router.push(`${selectHotel(lang)}?${searchParams.toString()}`)
|
||||
}
|
||||
|
||||
const closeButton = (
|
||||
|
||||
@@ -53,26 +53,3 @@ export function getQueryParamsForEnterDetails(
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export function createSelectRateUrl(roomData: BookingData) {
|
||||
const { hotel, fromDate, toDate } = roomData
|
||||
const params = new URLSearchParams({ fromDate, toDate, hotel })
|
||||
|
||||
roomData.rooms.forEach((room, index) => {
|
||||
params.set(`room[${index}].adults`, room.adults.toString())
|
||||
|
||||
if (room.children) {
|
||||
room.children.forEach((child, childIndex) => {
|
||||
params.set(
|
||||
`room[${index}].child[${childIndex}].age`,
|
||||
child.age.toString()
|
||||
)
|
||||
params.set(
|
||||
`room[${index}].child[${childIndex}].bed`,
|
||||
child.bed.toString()
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
return params
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export default function Card({
|
||||
iconHeight = 32,
|
||||
iconWidth = 32,
|
||||
declined = false,
|
||||
defaultChecked,
|
||||
highlightSubtitle = false,
|
||||
id,
|
||||
list,
|
||||
@@ -45,6 +46,7 @@ export default function Card({
|
||||
<input
|
||||
{...register(name)}
|
||||
aria-hidden
|
||||
defaultChecked={defaultChecked}
|
||||
id={id || name}
|
||||
hidden
|
||||
type={type}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
|
||||
import Label from "@/components/TempDesignSystem/Form/Label"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import useSetOverflowVisibleOnRA from "@/hooks/useSetOverflowVisibleOnRA"
|
||||
|
||||
import SelectChevron from "../Form/SelectChevron"
|
||||
|
||||
@@ -39,6 +40,7 @@ export default function Select({
|
||||
discreet = false,
|
||||
}: SelectProps) {
|
||||
const [rootDiv, setRootDiv] = useState<SelectPortalContainer>(undefined)
|
||||
const setOverflowVisible = useSetOverflowVisibleOnRA()
|
||||
|
||||
function setRef(node: SelectPortalContainerArgs) {
|
||||
if (node) {
|
||||
@@ -60,6 +62,7 @@ export default function Select({
|
||||
onSelectionChange={handleOnSelect}
|
||||
placeholder={placeholder}
|
||||
selectedKey={value as Key}
|
||||
onOpenChange={setOverflowVisible}
|
||||
>
|
||||
<Body asChild fontOnly>
|
||||
<Button className={styles.input} data-testid={name}>
|
||||
|
||||
Reference in New Issue
Block a user