Files
web/packages/booking-flow/lib/components/EnterDetails/Breakfast/index.tsx
Bianca Widstam c473bbc8b0 Merged in fix/BOOK-323-enter-details-scroll-error (pull request #2986)
Fix/BOOK-323 enter details scroll error

* fix(BOOK-323): scroll to invalid element on submit on enter details

* fix(BOOK-323): update error message design

* fix(BOOK-323): clean up

* fix(BOOK-323): scroll to fields in room in right order

* fix(BOOK-323): add id to translations

* fix(BOOK-323): remove undefined

* fix(BOOK-323): fix submitting state

* fix(BOOK-323): use ref in multiroom for scrolling to right element, add membershipNo

* fix(BOOK-323): fix invalid border country

* fix(BOOK-323): use error message component

* fix(BOOK-323): fix invalid focused styling on mobile

* fix(BOOK-323): remove redundant dependency in callback


Approved-by: Erik Tiekstra
2025-10-24 11:30:56 +00:00

186 lines
6.5 KiB
TypeScript

"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useCallback, useEffect, useRef } from "react"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import Body from "@scandic-hotels/design-system/Body"
import RadioCard from "@scandic-hotels/design-system/Form/RadioCard"
import BreakfastBuffetIcon from "@scandic-hotels/design-system/Icons/BreakfastBuffetIcon"
import NoBreakfastBuffetIcon from "@scandic-hotels/design-system/Icons/NoBreakfastBuffetIcon"
import { MessageBanner } from "@scandic-hotels/design-system/MessageBanner"
import { trackBreakfastSelection } from "@scandic-hotels/tracking/booking"
import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast"
import { useRoomContext } from "../../../contexts/EnterDetails/RoomContext"
import { useEnterDetailsStore } from "../../../stores/enter-details"
import { type BreakfastFormSchema, breakfastFormSchema } from "./schema"
import styles from "./breakfast.module.css"
export default function Breakfast() {
const intl = useIntl()
const formRef = useRef<HTMLDivElement>(null)
const packages = useEnterDetailsStore((state) => state.breakfastPackages)
const hotelId = useEnterDetailsStore((state) => state.booking.hotelId)
const { addPreSubmitCallback } = useEnterDetailsStore((state) => ({
addPreSubmitCallback: state.actions.addPreSubmitCallback,
}))
const {
actions: { updateBreakfast },
room,
idx,
} = useRoomContext()
const hasChildrenInRoom = !!room.childrenInRoom?.length
const totalPriceForNoBreakfast = 0
const breakfastSelection = room?.breakfast
? room.breakfast.code
: room?.breakfast === false
? "false"
: undefined
const methods = useForm<BreakfastFormSchema>({
criteriaMode: "all",
mode: "all",
resolver: zodResolver(breakfastFormSchema),
reValidateMode: "onChange",
values: breakfastSelection ? { breakfast: breakfastSelection } : undefined,
})
const onSubmit = useCallback(
(values: BreakfastFormSchema) => {
const pkg = packages.find((p) => p.code === values.breakfast)
if (pkg) {
updateBreakfast(pkg)
} else {
updateBreakfast(false)
}
trackBreakfastSelection({
breakfastPackage: pkg ?? packages[0],
hotelId,
units: pkg ? room.adults : 0,
breakfastOption: pkg ? "breakfast buffet" : "no breakfast",
})
},
[packages, hotelId, room.adults, updateBreakfast]
)
useEffect(() => {
async function callback() {
const isValid = await methods.trigger()
if (!isValid && methods.formState.errors.breakfast) {
return formRef.current ?? undefined
}
return
}
addPreSubmitCallback(`${idx}-breakfast`, callback)
}, [addPreSubmitCallback, methods, idx])
const selectedBreakfast = methods.watch("breakfast")
const handleSubmit = methods.handleSubmit
useEffect(() => {
if (selectedBreakfast) {
handleSubmit(onSubmit)()
}
}, [selectedBreakfast, handleSubmit, onSubmit])
return (
<FormProvider {...methods}>
<div className={styles.container} ref={formRef}>
{hasChildrenInRoom ? (
<Body>
{intl.formatMessage({
id: "enterDetails.breakfast.childrenFreeInfo",
defaultMessage:
"Children's breakfast is always free as part of the adult's breakfast.",
})}
</Body>
) : null}
{methods.formState.errors.breakfast && (
<div className={styles.errorContainer}>
<MessageBanner
text={intl.formatMessage({
id: "enterDetails.breakfast.error.required",
defaultMessage: "Breakfast option is required",
})}
type="error"
textColor="error"
/>
</div>
)}
<form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}>
{packages?.map((pkg) => (
<RadioCard
key={pkg.code}
name="breakfast"
value={pkg.code}
Icon={BreakfastBuffetIcon}
title={intl.formatMessage({
id: "common.breakfastBuffet",
defaultMessage: "Breakfast buffet",
})}
subtitle={
pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
? intl.formatMessage({
id: "common.included",
defaultMessage: "Included",
})
: `+ ${formatPrice(intl, pkg.localPrice.price, pkg.localPrice.currency ?? "")}`
}
subtitleSecondary={intl.formatMessage({
id: "common.perAdultNight",
defaultMessage: "Per adult/night",
})}
description={
hasChildrenInRoom
? intl.formatMessage({
id: "enterDetails.breakfast.freeForKidsUnder12",
defaultMessage: "Free for kids aged 12 and under.",
})
: undefined
}
descriptionSecondary={intl.formatMessage({
id: "enterDetails.breakfast.dietaryOptions",
defaultMessage:
"Includes vegan, gluten-free, and other allergy-friendly options.",
})}
/>
))}
<RadioCard
name="breakfast"
value="false"
Icon={NoBreakfastBuffetIcon}
title={intl.formatMessage({
id: "common.noBreakfast",
defaultMessage: "No breakfast",
})}
subtitle={`+ ${formatPrice(intl, totalPriceForNoBreakfast, packages?.[0].localPrice.currency ?? "")}`}
descriptionSecondary={
hasChildrenInRoom
? intl.formatMessage({
id: "enterDetails.breakfast.addAfterBookingChildren",
defaultMessage:
"Breakfast can be added after booking for an extra cost for adults and kids ages 4 and up.",
})
: intl.formatMessage({
id: "enterDetails.breakfast.addAfterBookingNoChilren",
defaultMessage:
"Breakfast can be added after booking for an additional fee.",
})
}
/>
</form>
</div>
</FormProvider>
)
}