diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Details/Multiroom/index.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Details/Multiroom/index.tsx index 0006d8a66..6b678a1ea 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Details/Multiroom/index.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Details/Multiroom/index.tsx @@ -1,11 +1,9 @@ "use client" import { zodResolver } from "@hookform/resolvers/zod" -import { useEffect, useMemo } from "react" +import { useCallback, useEffect, useMemo } from "react" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" -import { Button } from "@scandic-hotels/design-system/Button" - import { useEnterDetailsStore } from "@/stores/enter-details" import SpecialRequests from "@/components/HotelReservation/EnterDetails/Details/SpecialRequests" @@ -26,16 +24,12 @@ const formID = "enter-details" export default function Details() { const intl = useIntl() - const { canProceedToPayment, lastRoom, rooms } = useEnterDetailsStore( - (state) => ({ - canProceedToPayment: state.canProceedToPayment, - lastRoom: state.lastRoom, - rooms: state.rooms, - }) - ) + const { rooms } = useEnterDetailsStore((state) => ({ + rooms: state.rooms, + })) const { - actions: { updateDetails }, + actions: { updateDetails, setIncomplete }, idx, room, roomNr, @@ -58,7 +52,6 @@ export default function Details() { [idx, rooms] ) - const isPaymentNext = idx === lastRoom const methods = useForm({ criteriaMode: "all", mode: "all", @@ -78,6 +71,16 @@ export default function Details() { }, }) + const updateDetailsStore = useCallback(() => { + if (methods.formState.isValid) { + methods.handleSubmit(updateDetails)() + } else { + setIncomplete() + } + }, [methods, setIncomplete, updateDetails]) + + useEffect(updateDetailsStore, [methods.formState.isValid, updateDetailsStore]) + // Trigger validation of the room manually when another room changes its data. // Only do it if the field has a value, to avoid error states before the user // has filled anything in. @@ -125,6 +128,7 @@ export default function Details() { registerOptions={{ required: true, deps: "lastName", + onBlur: updateDetailsStore, }} /> {guestIsGoingToJoin ? null : ( )} - + -
- -
) diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Details/RoomOne/Signup/index.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Details/RoomOne/Signup/index.tsx index 47f438935..220c1bdc3 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Details/RoomOne/Signup/index.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Details/RoomOne/Signup/index.tsx @@ -1,7 +1,7 @@ "use client" import { useEffect, useState } from "react" -import { useWatch } from "react-hook-form" +import { type RegisterOptions, useWatch } from "react-hook-form" import { useIntl } from "react-intl" import DateSelect from "@/components/TempDesignSystem/Form/Date" @@ -10,7 +10,13 @@ import Caption from "@/components/TempDesignSystem/Text/Caption" import styles from "./signup.module.css" -export default function Signup({ name }: { name: string }) { +export default function Signup({ + name, + registerOptions, +}: { + name: string + registerOptions?: RegisterOptions +}) { const intl = useIntl() const [isJoinChecked, setIsJoinChecked] = useState(false) @@ -30,7 +36,7 @@ export default function Signup({ name }: { name: string }) { label={intl.formatMessage({ defaultMessage: "Zip code", })} - registerOptions={{ required: true }} + registerOptions={{ required: true, ...registerOptions }} />
@@ -42,7 +48,10 @@ export default function Signup({ name }: { name: string }) {
- +
) : ( @@ -52,6 +61,7 @@ export default function Signup({ name }: { name: string }) { })} name="membershipNo" type="tel" + registerOptions={registerOptions} /> ) } diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Details/RoomOne/index.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Details/RoomOne/index.tsx index 6ae788128..88d1ebd0b 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Details/RoomOne/index.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Details/RoomOne/index.tsx @@ -1,13 +1,9 @@ "use client" import { zodResolver } from "@hookform/resolvers/zod" -import { useCallback } from "react" +import { useCallback, useEffect } from "react" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" -import { Button } from "@scandic-hotels/design-system/Button" - -import { useEnterDetailsStore } from "@/stores/enter-details" - import SpecialRequests from "@/components/HotelReservation/EnterDetails/Details/SpecialRequests" import CountrySelect from "@/components/TempDesignSystem/Form/Country" import Input from "@/components/TempDesignSystem/Form/Input" @@ -30,24 +26,14 @@ const formID = "enter-details" export default function Details({ user }: DetailsProps) { const intl = useIntl() - const { canProceedToPayment, lastRoom, isMultiRoom } = useEnterDetailsStore( - (state) => ({ - canProceedToPayment: state.canProceedToPayment, - lastRoom: state.lastRoom, - isMultiRoom: state.rooms.length > 1, - }) - ) const { - actions: { updateDetails }, - idx, + actions: { updateDetails, setIncomplete }, room, roomNr, } = useRoomContext() const initialData = room.guest const memberRate = "member" in room.roomRate ? room.roomRate.member : null - const isPaymentNext = idx === lastRoom - const showContinueButton = isMultiRoom || !user const methods = useForm({ criteriaMode: "all", @@ -78,6 +64,16 @@ export default function Details({ user }: DetailsProps) { [updateDetails] ) + const updateDetailsStore = useCallback(() => { + if (methods.formState.isValid) { + methods.handleSubmit(onSubmit)() + } else { + setIncomplete() + } + }, [methods, onSubmit, setIncomplete]) + + useEffect(updateDetailsStore, [methods.formState.isValid, updateDetailsStore]) + return (
{user ? null : (
- +
)} - + - {showContinueButton ? ( -
- -
- ) : null}
) diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Details/SpecialRequests/index.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Details/SpecialRequests/index.tsx index 73a782b08..b70d6e0d6 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Details/SpecialRequests/index.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Details/SpecialRequests/index.tsx @@ -6,7 +6,13 @@ import TextArea from "@/components/TempDesignSystem/Form/TextArea" import styles from "./specialRequests.module.css" -export default function SpecialRequests() { +import type { RegisterOptions } from "react-hook-form" + +export default function SpecialRequests({ + registerOptions, +}: { + registerOptions?: RegisterOptions +}) { const intl = useIntl() return ( @@ -63,6 +69,7 @@ export default function SpecialRequests() { "Is there anything else you would like us to know before your arrival?", })} name="specialRequest.comment" + registerOptions={registerOptions} /> diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx index a7c9dd182..15431a792 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx @@ -33,6 +33,7 @@ import Title from "@/components/TempDesignSystem/Text/Title" import { useAvailablePaymentOptions } from "@/hooks/booking/useAvailablePaymentOptions" import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus" import useLang from "@/hooks/useLang" +import useStickyPosition from "@/hooks/useStickyPosition" import { trackPaymentEvent } from "@/utils/tracking" import { trackEvent } from "@/utils/tracking/base" import { trackGlaSaveCardAttempt } from "@/utils/tracking/myStay" @@ -71,6 +72,7 @@ export default function PaymentClient({ const lang = useLang() const intl = useIntl() const searchParams = useSearchParams() + const { getTopOffset } = useStickyPosition({}) const [showPaymentAlert, setShowPaymentAlert] = useState(false) @@ -80,8 +82,6 @@ export default function PaymentClient({ totalPrice: state.totalPrice, })) - const allRoomsComplete = rooms.every((r) => r.isComplete) - const bookingMustBeGuaranteed = rooms.some(({ room }, idx) => { if (idx === 0 && isUserLoggedIn && room.memberMustBeGuaranteed) { return true @@ -279,6 +279,30 @@ export default function PaymentClient({ const handleSubmit = useCallback( (data: PaymentFormData) => { + const firstIncompleteRoomIndex = rooms.findIndex( + (room) => !room.isComplete + ) + + // If any room is not complete/valid, scroll to it + if (firstIncompleteRoomIndex !== -1) { + const roomElement = document.getElementById( + `room-${firstIncompleteRoomIndex + 1}` + ) + + if (!roomElement) { + return + } + const roomElementTop = + roomElement.getBoundingClientRect().top + window.scrollY + + window.scrollTo({ + top: roomElementTop - getTopOffset() - 20, + behavior: "smooth", + }) + + return + } + const paymentMethod = getPaymentMethod(data.paymentMethod) const savedCreditCard = savedCreditCards?.find( @@ -423,6 +447,7 @@ export default function PaymentClient({ hasOnlyFlexRates, bookingMustBeGuaranteed, isUserLoggedIn, + getTopOffset, ] ) @@ -446,9 +471,7 @@ export default function PaymentClient({ }) return ( -
+
{hasOnlyFlexRates && bookingMustBeGuaranteed @@ -573,9 +596,7 @@ export default function PaymentClient({ theme="base" size="small" type="submit" - disabled={ - !methods.formState.isValid || methods.formState.isSubmitting - } + disabled={methods.formState.isSubmitting} > {intl.formatMessage({ defaultMessage: "Complete booking", diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Room/Multiroom.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Room/Multiroom.tsx index 514f587c5..559ef27e1 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Room/Multiroom.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Room/Multiroom.tsx @@ -19,8 +19,8 @@ import { StepEnum } from "@/types/enums/step" export default function Multiroom() { const intl = useIntl() - const { idx, room, roomNr, steps } = useRoomContext() - const { breakfastPackages, rooms } = useEnterDetailsStore((state) => ({ + const { room, roomNr } = useRoomContext() + const { breakfastPackages } = useEnterDetailsStore((state) => ({ breakfastPackages: state.breakfastPackages, rooms: state.rooms, })) @@ -28,22 +28,6 @@ export default function Multiroom() { const showBreakfastStep = !room.breakfastIncluded && !!breakfastPackages.length - const arePreviousRoomsValid = rooms.slice(0, idx).every((r) => r.isComplete) - - const isBreakfastStepValid = showBreakfastStep - ? steps[StepEnum.breakfast]?.isValid - : true - - const isBreakfastDisabled = !( - arePreviousRoomsValid && steps[StepEnum.selectBed].isValid - ) - - const isDetailsDisabled = !( - arePreviousRoomsValid && - steps[StepEnum.selectBed].isValid && - isBreakfastStepValid - ) - const hasChildWithExtraBed = room.childrenInRoom?.some( (child) => Number(child.bed) === ChildBedMapEnum.IN_EXTRA_BED ) @@ -55,7 +39,7 @@ export default function Multiroom() { ) return ( - <section> + <section id={`room-${roomNr}`}> <Header> <Title level="h2" as="h4"> {intl.formatMessage( @@ -77,7 +61,6 @@ export default function Multiroom() { label={intl.formatMessage({ defaultMessage: "Request bedtype" })} additionalInfo={bedTypeInfoText} step={StepEnum.selectBed} - disabled={!arePreviousRoomsValid} > <BedType /> </Section> @@ -92,7 +75,6 @@ export default function Multiroom() { defaultMessage: "Select breakfast options", })} step={StepEnum.breakfast} - disabled={isBreakfastDisabled} > <Breakfast /> </Section> @@ -106,7 +88,6 @@ export default function Multiroom() { label={intl.formatMessage({ defaultMessage: "Enter your details", })} - disabled={isDetailsDisabled} > <Details /> </Section> diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Room/One.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Room/One.tsx index 529ce3dce..da09a992f 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Room/One.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Room/One.tsx @@ -20,7 +20,7 @@ import type { SafeUser } from "@/types/user" export default function RoomOne({ user }: { user: SafeUser }) { const intl = useIntl() - const { room, steps } = useRoomContext() + const { room } = useRoomContext() const { breakfastPackages, isMultiroom } = useEnterDetailsStore((state) => ({ breakfastPackages: state.breakfastPackages, isMultiroom: state.rooms.length > 1, @@ -43,7 +43,7 @@ export default function RoomOne({ user }: { user: SafeUser }) { !room.breakfastIncluded && !!breakfastPackages.length return ( - <section> + <section id="room-1"> {isMultiroom ? ( <Header> <Title level="h2" as="h4"> @@ -81,7 +81,6 @@ export default function RoomOne({ user }: { user: SafeUser }) { defaultMessage: "Select breakfast options", })} step={StepEnum.breakfast} - disabled={!steps[StepEnum.selectBed].isValid} > <Breakfast /> </Section> @@ -95,12 +94,6 @@ export default function RoomOne({ user }: { user: SafeUser }) { label={intl.formatMessage({ defaultMessage: "Enter your details", })} - disabled={ - !( - steps[StepEnum.selectBed].isValid && - steps[StepEnum.breakfast]?.isValid !== false - ) - } > <Details user={user} /> </Section> diff --git a/apps/scandic-web/stores/enter-details/index.ts b/apps/scandic-web/stores/enter-details/index.ts index 871c93e23..ff9d869e2 100644 --- a/apps/scandic-web/stores/enter-details/index.ts +++ b/apps/scandic-web/stores/enter-details/index.ts @@ -167,6 +167,13 @@ export function createDetailsStore( return { actions: { + setIncomplete() { + return set( + produce((state: DetailsState) => { + state.rooms[idx].isComplete = false + }) + ) + }, updateBedType(bedType) { return set( produce((state: DetailsState) => { diff --git a/apps/scandic-web/types/contexts/details/room.ts b/apps/scandic-web/types/contexts/details/room.ts index 5222b387c..f0cb273fe 100644 --- a/apps/scandic-web/types/contexts/details/room.ts +++ b/apps/scandic-web/types/contexts/details/room.ts @@ -5,6 +5,7 @@ import type { RoomState } from "@/types/stores/enter-details" export interface RoomContextValue { actions: { + setIncomplete: () => void updateBedType: (data: BedTypeSchema) => void updateBreakfast: (data: BreakfastPackage | false) => void updateJoin: (join: boolean) => void diff --git a/apps/scandic-web/types/stores/enter-details.ts b/apps/scandic-web/types/stores/enter-details.ts index 500bc6920..5fdd4e7fe 100644 --- a/apps/scandic-web/types/stores/enter-details.ts +++ b/apps/scandic-web/types/stores/enter-details.ts @@ -60,6 +60,7 @@ export interface Room extends InitialRoomData { export interface RoomState { actions: { + setIncomplete: () => void updateBedType: (data: BedTypeSchema) => void updateBreakfast: (data: BreakfastPackage | false) => void updateJoin: (join: boolean) => void