feat: make steps of enter details flow dynamic depending on data

This commit is contained in:
Simon Emanuelsson
2024-11-18 09:13:23 +01:00
committed by Joakim Jäderberg
parent d49e301634
commit c6fc500d9e
62 changed files with 959 additions and 659 deletions

View File

@@ -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")

View File

@@ -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%);
}

View File

@@ -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(() => {

View File

@@ -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%);
}

View File

@@ -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>

View File

@@ -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")),
})

View File

@@ -1,7 +1,6 @@
.form {
display: grid;
gap: var(--Spacing-x2);
padding: var(--Spacing-x3) 0px;
}
.container {

View File

@@ -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"

View File

@@ -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) => {

View File

@@ -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:

View File

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

View File

@@ -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>
)

View File

@@ -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);
}
}
}

View File

@@ -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"
>

View File

@@ -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,
}))

View File

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

View File

@@ -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
>

View File

@@ -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">

View File

@@ -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
>

View File

@@ -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);
}
}

View File

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

View File

@@ -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"
>

View File

@@ -71,7 +71,7 @@ export default function SelectHotelMap({
}
function handlePageRedirect() {
router.push(`${selectHotel[lang]}?${searchParams.toString()}`)
router.push(`${selectHotel(lang)}?${searchParams.toString()}`)
}
const closeButton = (

View File

@@ -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
}

View File

@@ -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}

View File

@@ -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}>