Merged in feat/SW-1889 (pull request #1670)

Feat/SW-1889

* fix: remove download invoice from confirmation page

* feat: remove EnterDetails Accordions


Approved-by: Simon.Emanuelsson
This commit is contained in:
Arvid Norlin
2025-03-31 13:14:11 +00:00
parent 93aafe5525
commit 5cff2e5f36
22 changed files with 205 additions and 513 deletions

View File

@@ -11,7 +11,7 @@ import useLang from "@/hooks/useLang"
import AddToCalendar from "../../AddToCalendar" import AddToCalendar from "../../AddToCalendar"
import AddToCalendarButton from "./Actions/AddToCalendarButton" import AddToCalendarButton from "./Actions/AddToCalendarButton"
import DownloadInvoice from "./Actions/DownloadInvoice" // import DownloadInvoice from "./Actions/DownloadInvoice"
import { generateDateTime } from "./Actions/helpers" import { generateDateTime } from "./Actions/helpers"
import ManageBooking from "./Actions/ManageBooking" import ManageBooking from "./Actions/ManageBooking"
@@ -24,7 +24,7 @@ import type { BookingConfirmationHeaderProps } from "@/types/components/hotelRes
export default function Header({ export default function Header({
booking, booking,
hotel, hotel,
mainRef, // mainRef,
refId, refId,
}: BookingConfirmationHeaderProps) { }: BookingConfirmationHeaderProps) {
const intl = useIntl() const intl = useIntl()
@@ -91,7 +91,8 @@ export default function Header({
renderButton={(onPress) => <AddToCalendarButton onPress={onPress} />} renderButton={(onPress) => <AddToCalendarButton onPress={onPress} />}
/> />
<ManageBooking bookingUrl={bookingUrlPath} /> <ManageBooking bookingUrl={bookingUrlPath} />
<DownloadInvoice mainRef={mainRef} /> {/* Download Invoice will be added later (currently available on My Stay) */}
{/* <DownloadInvoice mainRef={mainRef} /> */}
</div> </div>
</header> </header>
) )

View File

@@ -58,8 +58,7 @@ export default function BedType() {
return return
} }
const subscription = methods.watch(() => methods.handleSubmit(onSubmit)()) methods.watch(() => methods.handleSubmit(onSubmit)())
return () => subscription.unsubscribe()
}, [methods, onSubmit]) }, [methods, onSubmit])
return ( return (

View File

@@ -58,8 +58,7 @@ export default function Breakfast() {
if (methods.formState.isSubmitting) { if (methods.formState.isSubmitting) {
return return
} }
const subscription = methods.watch(() => methods.handleSubmit(onSubmit)()) methods.watch(() => methods.handleSubmit(onSubmit)())
return () => subscription.unsubscribe()
}, [methods, onSubmit]) }, [methods, onSubmit])
return ( return (

View File

@@ -24,23 +24,20 @@ const formID = "enter-details"
export default function Details() { export default function Details() {
const intl = useIntl() const intl = useIntl()
const { activeRoom, canProceedToPayment, lastRoom } = useEnterDetailsStore( const { canProceedToPayment, lastRoom } = useEnterDetailsStore((state) => ({
(state) => ({
activeRoom: state.activeRoom,
canProceedToPayment: state.canProceedToPayment, canProceedToPayment: state.canProceedToPayment,
lastRoom: state.lastRoom, lastRoom: state.lastRoom,
}) }))
)
const { const {
actions: { updateDetails }, actions: { updateDetails },
idx,
room, room,
roomNr, roomNr,
} = useRoomContext() } = useRoomContext()
const initialData = room.guest const initialData = room.guest
const isPaymentNext = activeRoom === lastRoom const isPaymentNext = idx === lastRoom
const methods = useForm<MultiroomDetailsSchema>({ const methods = useForm<MultiroomDetailsSchema>({
criteriaMode: "all", criteriaMode: "all",
mode: "all", mode: "all",

View File

@@ -31,22 +31,20 @@ export default function Details({ user }: DetailsProps) {
const intl = useIntl() const intl = useIntl()
const [isMemberPriceModalOpen, setIsMemberPriceModalOpen] = useState(false) const [isMemberPriceModalOpen, setIsMemberPriceModalOpen] = useState(false)
const { activeRoom, canProceedToPayment, lastRoom } = useEnterDetailsStore( const { canProceedToPayment, lastRoom } = useEnterDetailsStore((state) => ({
(state) => ({
activeRoom: state.activeRoom,
canProceedToPayment: state.canProceedToPayment, canProceedToPayment: state.canProceedToPayment,
lastRoom: state.lastRoom, lastRoom: state.lastRoom,
}) }))
)
const { const {
actions: { updateDetails }, actions: { updateDetails },
idx,
room, room,
roomNr, roomNr,
} = useRoomContext() } = useRoomContext()
const initialData = room.guest const initialData = room.guest
const memberRate = "member" in room.roomRate ? room.roomRate.member : null
const isPaymentNext = activeRoom === lastRoom const memberRate = "member" in room.roomRate ? room.roomRate.member : null
const isPaymentNext = idx === lastRoom
const methods = useForm<DetailsSchema>({ const methods = useForm<DetailsSchema>({
criteriaMode: "all", criteriaMode: "all",

View File

@@ -67,14 +67,14 @@ export default function PaymentClient({
const [showPaymentAlert, setShowPaymentAlert] = useState(false) const [showPaymentAlert, setShowPaymentAlert] = useState(false)
const { booking, canProceedToPayment, rooms, totalPrice } = const { booking, rooms, totalPrice } = useEnterDetailsStore((state) => ({
useEnterDetailsStore((state) => ({
booking: state.booking, booking: state.booking,
canProceedToPayment: state.canProceedToPayment,
rooms: state.rooms, rooms: state.rooms,
totalPrice: state.totalPrice, totalPrice: state.totalPrice,
})) }))
const allRoomsComplete = rooms.every((r) => r.isComplete)
const bookingMustBeGuaranteed = rooms.some(({ room }, idx) => { const bookingMustBeGuaranteed = rooms.some(({ room }, idx) => {
if (idx === 0 && isUserLoggedIn && room.memberMustBeGuaranteed) { if (idx === 0 && isUserLoggedIn && room.memberMustBeGuaranteed) {
return true return true
@@ -390,7 +390,7 @@ export default function PaymentClient({
return ( return (
<section <section
className={`${styles.paymentSection} ${canProceedToPayment ? "" : styles.disabled}`} className={`${styles.paymentSection} ${allRoomsComplete ? "" : styles.disabled}`}
> >
<header> <header>
<Title level="h2" as="h4"> <Title level="h2" as="h4">

View File

@@ -7,7 +7,7 @@ import BedType from "@/components/HotelReservation/EnterDetails/BedType"
import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast" import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast"
import Details from "@/components/HotelReservation/EnterDetails/Details/Multiroom" import Details from "@/components/HotelReservation/EnterDetails/Details/Multiroom"
import Header from "@/components/HotelReservation/EnterDetails/Room/Header" import Header from "@/components/HotelReservation/EnterDetails/Room/Header"
import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion" import Section from "@/components/HotelReservation/EnterDetails/Section"
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom" import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
import Title from "@/components/TempDesignSystem/Text/Title" import Title from "@/components/TempDesignSystem/Text/Title"
import { useRoomContext } from "@/contexts/Details/Room" import { useRoomContext } from "@/contexts/Details/Room"
@@ -16,12 +16,31 @@ import { StepEnum } from "@/types/enums/step"
export default function Multiroom() { export default function Multiroom() {
const intl = useIntl() const intl = useIntl()
const { room, roomNr } = useRoomContext() const { idx, room, roomNr, steps } = useRoomContext()
const breakfastPackages = useEnterDetailsStore( const { breakfastPackages, rooms } = useEnterDetailsStore((state) => ({
(state) => state.breakfastPackages breakfastPackages: state.breakfastPackages,
) rooms: state.rooms,
}))
const showBreakfastStep = const showBreakfastStep =
!room.breakfastIncluded && !!breakfastPackages?.length !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
)
return ( return (
<section> <section>
<Header> <Header>
@@ -38,34 +57,37 @@ export default function Multiroom() {
<SelectedRoom /> <SelectedRoom />
{room.bedTypes ? ( {room.bedTypes ? (
<SectionAccordion <Section
header={intl.formatMessage({ id: "Select bed" })} header={intl.formatMessage({ id: "Select bed" })}
label={intl.formatMessage({ id: "Request bedtype" })} label={intl.formatMessage({ id: "Request bedtype" })}
step={StepEnum.selectBed} step={StepEnum.selectBed}
disabled={!arePreviousRoomsValid}
> >
<BedType /> <BedType />
</SectionAccordion> </Section>
) : null} ) : null}
{showBreakfastStep ? ( {showBreakfastStep ? (
<SectionAccordion <Section
header={intl.formatMessage({ id: "Food options" })} header={intl.formatMessage({ id: "Food options" })}
label={intl.formatMessage({ label={intl.formatMessage({
id: "Select breakfast options", id: "Select breakfast options",
})} })}
step={StepEnum.breakfast} step={StepEnum.breakfast}
disabled={isBreakfastDisabled}
> >
<Breakfast /> <Breakfast />
</SectionAccordion> </Section>
) : null} ) : null}
<SectionAccordion <Section
header={intl.formatMessage({ id: "Details" })} header={intl.formatMessage({ id: "Details" })}
step={StepEnum.details} step={StepEnum.details}
label={intl.formatMessage({ id: "Enter your details" })} label={intl.formatMessage({ id: "Enter your details" })}
disabled={isDetailsDisabled}
> >
<Details /> <Details />
</SectionAccordion> </Section>
</section> </section>
) )
} }

View File

@@ -7,7 +7,7 @@ import BedType from "@/components/HotelReservation/EnterDetails/BedType"
import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast" import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast"
import Details from "@/components/HotelReservation/EnterDetails/Details/RoomOne" import Details from "@/components/HotelReservation/EnterDetails/Details/RoomOne"
import Header from "@/components/HotelReservation/EnterDetails/Room/Header" import Header from "@/components/HotelReservation/EnterDetails/Room/Header"
import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion" import Section from "@/components/HotelReservation/EnterDetails/Section"
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom" import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
import Title from "@/components/TempDesignSystem/Text/Title" import Title from "@/components/TempDesignSystem/Text/Title"
import { useRoomContext } from "@/contexts/Details/Room" import { useRoomContext } from "@/contexts/Details/Room"
@@ -17,13 +17,12 @@ import type { SafeUser } from "@/types/user"
export default function RoomOne({ user }: { user: SafeUser }) { export default function RoomOne({ user }: { user: SafeUser }) {
const intl = useIntl() const intl = useIntl()
const { room } = useRoomContext() const { room, steps } = useRoomContext()
const { breakfastPackages, rooms } = useEnterDetailsStore((state) => ({ const { breakfastPackages, isMultiroom } = useEnterDetailsStore((state) => ({
breakfastPackages: state.breakfastPackages, breakfastPackages: state.breakfastPackages,
rooms: state.rooms, isMultiroom: state.rooms.length > 1,
})) }))
const isMultiroom = rooms.length > 1
const showBreakfastStep = const showBreakfastStep =
!room.breakfastIncluded && !!breakfastPackages?.length !room.breakfastIncluded && !!breakfastPackages?.length
return ( return (
@@ -44,34 +43,41 @@ export default function RoomOne({ user }: { user: SafeUser }) {
<SelectedRoom /> <SelectedRoom />
{room.bedTypes ? ( {room.bedTypes ? (
<SectionAccordion <Section
header={intl.formatMessage({ id: "Select bed" })} header={intl.formatMessage({ id: "Select bed" })}
label={intl.formatMessage({ id: "Request bedtype" })} label={intl.formatMessage({ id: "Request bedtype" })}
step={StepEnum.selectBed} step={StepEnum.selectBed}
> >
<BedType /> <BedType />
</SectionAccordion> </Section>
) : null} ) : null}
{showBreakfastStep ? ( {showBreakfastStep ? (
<SectionAccordion <Section
header={intl.formatMessage({ id: "Food options" })} header={intl.formatMessage({ id: "Food options" })}
label={intl.formatMessage({ label={intl.formatMessage({
id: "Select breakfast options", id: "Select breakfast options",
})} })}
step={StepEnum.breakfast} step={StepEnum.breakfast}
disabled={!steps[StepEnum.selectBed].isValid}
> >
<Breakfast /> <Breakfast />
</SectionAccordion> </Section>
) : null} ) : null}
<SectionAccordion <Section
header={intl.formatMessage({ id: "Details" })} header={intl.formatMessage({ id: "Details" })}
step={StepEnum.details} step={StepEnum.details}
label={intl.formatMessage({ id: "Enter your details" })} label={intl.formatMessage({ id: "Enter your details" })}
disabled={
!(
steps[StepEnum.selectBed].isValid &&
steps[StepEnum.breakfast]?.isValid !== false
)
}
> >
<Details user={user} /> <Details user={user} />
</SectionAccordion> </Section>
</section> </section>
) )
} }

View File

@@ -0,0 +1,69 @@
"use client"
import { useEffect, useState } from "react"
import { useIntl } from "react-intl"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { useRoomContext } from "@/contexts/Details/Room"
import styles from "./section.module.css"
import type { SectionProps } from "@/types/components/hotelReservation/enterDetails/section"
import { StepEnum } from "@/types/enums/step"
export default function Section({
children,
header,
label,
step,
disabled,
}: React.PropsWithChildren<SectionProps>) {
const intl = useIntl()
const {
room: { bedType, breakfast },
} = useRoomContext()
const [title, setTitle] = useState(label)
const noBreakfastTitle = intl.formatMessage({ id: "No breakfast" })
const breakfastTitle = intl.formatMessage({ id: "Breakfast buffet" })
useEffect(() => {
if (step === StepEnum.selectBed && bedType) {
setTitle(bedType.description)
}
// If breakfast step, check if an option has been selected
if (step === StepEnum.breakfast && breakfast !== undefined) {
if (breakfast === false) {
setTitle(noBreakfastTitle)
} else {
setTitle(breakfastTitle)
}
}
}, [bedType, breakfast, setTitle, step, breakfastTitle, noBreakfastTitle])
return (
<div
className={`${styles.accordion} ${disabled ? styles.disabled : ""}`}
data-step={step}
>
<header className={styles.header}>
<Footnote
className={styles.title}
asChild
textTransform="uppercase"
type="label"
>
<h2>{header}</h2>
</Footnote>
<Subtitle className={styles.selection} type="two">
{title}
</Subtitle>
</header>
<div className={styles.content}>
<div className={styles.contentWrapper}>{children}</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,45 @@
.accordion {
--header-height: 2.4em;
--circle-height: 24px;
gap: var(--Spacing-x3);
width: 100%;
padding-top: var(--Spacing-x3);
display: grid;
grid-template-areas: "header" "content";
grid-template-rows: var(--header-height) 1fr;
column-gap: var(--Spacing-x-one-and-half);
}
.header {
grid-area: header;
}
.title {
grid-area: title;
text-align: start;
}
.selection {
grid-area: selection;
}
.contentWrapper {
padding-bottom: var(--Spacing-x3);
}
.content {
grid-area: content;
border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
}
.disabled {
opacity: 0.5;
pointer-events: none;
}
@media screen and (min-width: 768px) {
.accordion {
column-gap: var(--Spacing-x3);
grid-template-areas: "header" "content";
}
}

View File

@@ -1,148 +0,0 @@
"use client"
import { useEffect, useRef, useState } from "react"
import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/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"
import type { SectionAccordionProps } from "@/types/components/hotelReservation/selectRate/sectionAccordion"
import { StepEnum } from "@/types/enums/step"
export default function SectionAccordion({
children,
header,
label,
step,
}: React.PropsWithChildren<SectionAccordionProps>) {
const intl = useIntl()
const stickyPosition = useStickyPosition({})
const {
actions: { setStep },
currentStep,
isActiveRoom,
room: { bedType, breakfast, isAvailable },
steps,
} = useRoomContext()
const isStepComplete = !!(steps[step]?.isValid && isAvailable)
const [isOpen, setIsOpen] = useState(false)
const [title, setTitle] = useState(label)
const noBreakfastTitle = intl.formatMessage({ id: "No breakfast" })
const breakfastTitle = intl.formatMessage({ id: "Breakfast buffet" })
// useScrollToActiveSection(step, steps, currentStep === step)
useEffect(() => {
if (step === StepEnum.selectBed && bedType) {
setTitle(bedType.description)
}
// If breakfast step, check if an option has been selected
if (step === StepEnum.breakfast && breakfast !== undefined) {
if (breakfast === false) {
setTitle(noBreakfastTitle)
} else {
setTitle(breakfastTitle)
}
}
}, [bedType, breakfast, setTitle, step, breakfastTitle, noBreakfastTitle])
const accordionRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const shouldBeOpen = currentStep === step && isActiveRoom && isAvailable
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.
if (shouldBeOpen) {
const handleTransitionEnd = () => {
if (accordionRef.current) {
window.scrollTo({
top: accordionRef.current.offsetTop - stickyPosition.getTopOffset(),
behavior: "smooth",
})
}
accordionRef.current?.removeEventListener(
"transitionend",
handleTransitionEnd
)
}
if (accordionRef.current) {
accordionRef.current.addEventListener(
"transitionend",
handleTransitionEnd,
{ once: true }
)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentStep, isActiveRoom, isAvailable, setIsOpen, step])
function goToStep() {
setStep(step)
}
function close() {
setIsOpen(false)
goToStep()
}
const textColor =
isStepComplete || isOpen ? "uiTextHighContrast" : "baseTextDisabled"
return (
<div
className={styles.accordion}
data-section-open={isOpen}
data-step={step}
ref={accordionRef}
>
<div className={styles.iconWrapper}>
<div className={styles.circle} data-checked={isStepComplete}>
{isStepComplete ? (
<MaterialIcon icon="check" color="Icon/Inverted" size={16} />
) : null}
</div>
</div>
<header className={styles.header}>
<button
onClick={isOpen ? close : goToStep}
disabled={!isStepComplete}
className={styles.modifyButton}
>
<Footnote
className={styles.title}
asChild
textTransform="uppercase"
type="label"
color={textColor}
>
<h2>{header}</h2>
</Footnote>
<Subtitle className={styles.selection} type="two" color={textColor}>
{title}
</Subtitle>
{isStepComplete && (
<MaterialIcon
icon="keyboard_arrow_down"
className={`${styles.button} ${isOpen ? styles.buttonOpen : ""}`}
color="Icon/Interactive/Default"
/>
)}
</button>
</header>
<div className={styles.content}>
<div className={styles.contentWrapper}>{children}</div>
</div>
</div>
)
}

View File

@@ -1,127 +0,0 @@
.accordion {
--header-height: 2.4em;
--circle-height: 24px;
gap: var(--Spacing-x3);
width: 100%;
padding-top: var(--Spacing-x3);
transition: 0.3s ease-out;
display: grid;
grid-template-areas: "circle header" "content content";
grid-template-columns: auto 1fr;
grid-template-rows: var(--header-height) 0fr;
column-gap: var(--Spacing-x-one-and-half);
transform-origin: top;
}
.header {
grid-area: header;
}
.modifyButton {
display: grid;
grid-template-areas: "title button" "selection button";
cursor: pointer;
background-color: transparent;
border: none;
width: 100%;
padding: 0;
}
.modifyButton:disabled {
cursor: default;
}
.title {
grid-area: title;
text-align: start;
}
.button {
grid-area: button;
justify-self: flex-end;
transform-origin: 50% 50%;
transition: transform 0.3s;
}
.buttonOpen {
transform: rotate(180deg);
}
.selection {
grid-area: selection;
}
.iconWrapper {
position: relative;
grid-area: circle;
}
.circle {
width: var(--circle-height);
height: var(--circle-height);
border-radius: 100px;
transition: background-color 0.4s;
border: 2px solid var(--Base-Border-Inverted);
display: flex;
justify-content: center;
align-items: center;
}
.circle[data-checked="true"] {
background-color: var(--UI-Input-Controls-Fill-Selected);
}
.accordion[data-section-open="true"] .circle[data-checked="false"] {
background-color: var(--UI-Text-Placeholder);
}
.accordion[data-section-open="false"] .circle[data-checked="false"] {
background-color: var(--Base-Surface-Subtle-Hover);
}
.accordion[data-section-open="true"] {
grid-template-rows: var(--header-height) 1fr;
}
.contentWrapper {
opacity: 0;
padding-bottom: var(--Spacing-x3);
}
.accordion[data-section-open="true"] .contentWrapper {
opacity: 1;
}
.content {
overflow: hidden;
grid-area: content;
border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
transform-origin: top;
transition: opacity 0.2s linear;
}
.accordion[data-section-open="true"] .content {
overflow: visible;
}
@media screen and (min-width: 768px) {
.accordion {
column-gap: var(--Spacing-x3);
grid-template-areas: "circle header" "circle content";
}
.iconWrapper {
top: var(--Spacing-x1);
}
.accordion:not(:last-child) .iconWrapper::after {
position: absolute;
left: 12px;
bottom: calc(0px - var(--Spacing-x5));
top: var(--circle-height);
content: "";
border-left: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
}
}

View File

@@ -25,7 +25,7 @@ export default function SelectedRoom() {
const lang = useLang() const lang = useLang()
const router = useRouter() const router = useRouter()
const [isPending, startTransition] = useTransition() const [isPending, startTransition] = useTransition()
const { room, roomNr } = useRoomContext() const { room, idx } = useRoomContext()
const { hotelId, searchParamsStr } = useEnterDetailsStore((state) => ({ const { hotelId, searchParamsStr } = useEnterDetailsStore((state) => ({
hotelId: state.booking.hotelId, hotelId: state.booking.hotelId,
searchParamsStr: state.searchParamString, searchParamsStr: state.searchParamString,
@@ -33,8 +33,8 @@ export default function SelectedRoom() {
function changeRoom() { function changeRoom() {
const searchParams = new URLSearchParams(searchParamsStr) const searchParams = new URLSearchParams(searchParamsStr)
// rooms are index based, thus need for subtraction
searchParams.set("modifyRateIndex", `${roomNr - 1}`) searchParams.set("modifyRateIndex", `${idx}`)
startTransition(() => { startTransition(() => {
router.push(`${selectRate(lang)}?${searchParams.toString()}`) router.push(`${selectRate(lang)}?${searchParams.toString()}`)
}) })

View File

@@ -41,24 +41,6 @@
align-self: flex-start; align-self: flex-start;
} }
.iconWrapper {
position: relative;
}
.circle {
width: 24px;
height: 24px;
border-radius: 100px;
border: 2px solid var(--Base-Border-Inverted);
display: flex;
justify-content: center;
align-items: center;
}
.circle {
background-color: var(--UI-Input-Controls-Fill-Selected);
}
.wrapper[data-available="false"] .circle { .wrapper[data-available="false"] .circle {
background-color: var(--Base-Surface-Subtle-Hover); background-color: var(--Base-Surface-Subtle-Hover);
} }
@@ -92,14 +74,4 @@
.rate::after { .rate::after {
content: ")"; content: ")";
} }
.wrapper:not(:last-child)::after {
position: absolute;
left: 12px;
bottom: 0;
top: var(--Spacing-x7);
height: 100%;
content: "";
border-left: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
}
} }

View File

@@ -7,22 +7,21 @@ import { RoomContext } from "@/contexts/Details/Room"
import type { RoomProviderProps } from "@/types/providers/details/room" import type { RoomProviderProps } from "@/types/providers/details/room"
export default function RoomProvider({ children, idx }: RoomProviderProps) { export default function RoomProvider({ children, idx }: RoomProviderProps) {
const { actions, activeRoom, currentStep, isComplete, room, steps } = const { actions, isComplete, room, steps } = useEnterDetailsStore(
useEnterDetailsStore((state) => ({ (state) => ({
actions: state.rooms[idx].actions, actions: state.rooms[idx].actions,
activeRoom: state.activeRoom,
currentStep: state.rooms[idx].currentStep,
isComplete: state.rooms[idx].isComplete, isComplete: state.rooms[idx].isComplete,
room: state.rooms[idx].room, room: state.rooms[idx].room,
steps: state.rooms[idx].steps, steps: state.rooms[idx].steps,
})) })
)
return ( return (
<RoomContext.Provider <RoomContext.Provider
value={{ value={{
actions, actions,
currentStep, idx,
isComplete, isComplete,
isActiveRoom: activeRoom === idx,
room, room,
roomNr: idx + 1, roomNr: idx + 1,
steps, steps,

View File

@@ -147,7 +147,6 @@ export default function EnterDetailsProvider({
) )
currentRoom.isComplete = !invalidStep currentRoom.isComplete = !invalidStep
currentRoom.currentStep = invalidStep ? invalidStep.step : null
return currentRoom return currentRoom
}) })
@@ -185,18 +184,12 @@ export default function EnterDetailsProvider({
nights nights
) )
const activeRoom = filteredOutMissingRooms.findIndex(
(room) => !room.isComplete
)
writeToSessionStorage({ writeToSessionStorage({
activeRoom,
booking, booking,
rooms: filteredOutMissingRooms, rooms: filteredOutMissingRooms,
}) })
storeRef.current?.setState({ storeRef.current?.setState({
activeRoom: storedValues.activeRoom,
canProceedToPayment, canProceedToPayment,
rooms: filteredOutMissingRooms, rooms: filteredOutMissingRooms,
totalPrice, totalPrice,

View File

@@ -7,11 +7,7 @@ import type { Price } from "@/types/components/hotelReservation/price"
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import { CurrencyEnum } from "@/types/enums/currency" import { CurrencyEnum } from "@/types/enums/currency"
import { StepEnum } from "@/types/enums/step" import { StepEnum } from "@/types/enums/step"
import type { import type { PersistedState, RoomState } from "@/types/stores/enter-details"
DetailsState,
PersistedState,
RoomState,
} from "@/types/stores/enter-details"
import type { SafeUser } from "@/types/user" import type { SafeUser } from "@/types/user"
export function extractGuestFromUser(user: NonNullable<SafeUser>) { export function extractGuestFromUser(user: NonNullable<SafeUser>) {
@@ -513,54 +509,12 @@ export function findNextInvalidStep(roomState: RoomState) {
) )
} }
export const selectNextStep = (room: RoomState) => {
if (room.currentStep === null) {
throw new Error("getNextStep: currentStep is null")
}
if (!room.steps[room.currentStep]?.isValid) {
return room.currentStep
}
const stepsArray = Object.values(room.steps)
const currentIndex = stepsArray.findIndex(
(step) => step?.step === room.currentStep
)
if (currentIndex === stepsArray.length - 1) {
return null
}
const nextInvalidStep = stepsArray
.slice(currentIndex + 1)
.find((step) => !step.isValid)
return nextInvalidStep?.step ?? null
}
export const checkRoomProgress = (steps: RoomState["steps"]) => { export const checkRoomProgress = (steps: RoomState["steps"]) => {
return Object.values(steps) return Object.values(steps)
.filter(Boolean) .filter(Boolean)
.every((step) => step.isValid) .every((step) => step.isValid)
} }
export function handleStepProgression(room: RoomState, state: DetailsState) {
const isAllRoomsCompleted = state.rooms.every((r) => r.isComplete)
if (isAllRoomsCompleted) {
room.currentStep = null
state.canProceedToPayment = true
} else if (room.isComplete) {
room.currentStep = null
const nextRoomIndex = state.rooms.findIndex((r) => !r.isComplete)
state.activeRoom = nextRoomIndex
const nextRoom = state.rooms[nextRoomIndex]
const nextStep = selectNextStep(nextRoom)
nextRoom.currentStep = nextStep
} else if (selectNextStep(room)) {
room.currentStep = selectNextStep(room)
}
}
export function readFromSessionStorage(): PersistedState | undefined { export function readFromSessionStorage(): PersistedState | undefined {
if (typeof window === "undefined") { if (typeof window === "undefined") {
return undefined return undefined

View File

@@ -15,10 +15,8 @@ import {
calculateVoucherPrice, calculateVoucherPrice,
checkRoomProgress, checkRoomProgress,
extractGuestFromUser, extractGuestFromUser,
findNextInvalidStep,
getRoomPrice, getRoomPrice,
getTotalPrice, getTotalPrice,
handleStepProgression,
writeToSessionStorage, writeToSessionStorage,
} from "./helpers" } from "./helpers"
@@ -114,7 +112,6 @@ export function createDetailsStore(
}) })
return create<DetailsState>()((set) => ({ return create<DetailsState>()((set) => ({
activeRoom: 0,
booking: initialState.booking, booking: initialState.booking,
breakfastPackages, breakfastPackages,
canProceedToPayment: false, canProceedToPayment: false,
@@ -141,72 +138,8 @@ export function createDetailsStore(
delete steps[StepEnum.breakfast] delete steps[StepEnum.breakfast]
} }
const currentStep =
Object.values(steps).find((step) => !step.isValid)?.step ??
StepEnum.selectBed
return { return {
actions: { actions: {
setStep(step) {
return set(
produce((state: DetailsState) => {
const isSameRoom = idx === state.activeRoom
const room = state.rooms[idx]
if (isSameRoom) {
// Closed same accordion as was open
if (step === room.currentStep) {
if (room.isComplete) {
// Room is complete, move to next room or payment
const nextRoomIdx = state.rooms.findIndex(
(r) => !r.isComplete
)
state.activeRoom = nextRoomIdx
// Done, proceed to payment
if (nextRoomIdx === -1) {
room.currentStep = null
} else {
const nextRoom = state.rooms[nextRoomIdx]
const nextInvalidStep = findNextInvalidStep(nextRoom)
nextRoom.currentStep = nextInvalidStep
}
} else {
room.currentStep = findNextInvalidStep(room)
}
} else {
if (room.steps[step]?.isValid) {
room.currentStep = step
} else {
room.currentStep = findNextInvalidStep(room)
}
}
} else {
const arePreviousRoomsCompleted = state.rooms
.slice(0, idx)
.every((room) => room.isComplete)
if (arePreviousRoomsCompleted) {
state.activeRoom = idx
if (room.steps[step]?.isValid) {
room.currentStep = step
} else {
room.currentStep = findNextInvalidStep(room)
}
} else {
const firstIncompleteRoom = state.rooms.findIndex(
(r) => !r.isComplete
)
state.activeRoom = firstIncompleteRoom
if (firstIncompleteRoom === -1) {
// All rooms are done, proceed to payment
room.currentStep = null
} else {
const nextRoom = state.rooms[firstIncompleteRoom]
nextRoom.currentStep = findNextInvalidStep(nextRoom)
}
}
}
})
)
},
updateBedType(bedType) { updateBedType(bedType) {
return set( return set(
produce((state: DetailsState) => { produce((state: DetailsState) => {
@@ -220,10 +153,7 @@ export function createDetailsStore(
state.rooms[idx].isComplete = true state.rooms[idx].isComplete = true
} }
handleStepProgression(state.rooms[idx], state)
writeToSessionStorage({ writeToSessionStorage({
activeRoom: state.activeRoom,
booking: state.booking, booking: state.booking,
rooms: state.rooms, rooms: state.rooms,
}) })
@@ -340,10 +270,7 @@ export function createDetailsStore(
state.rooms[idx].isComplete = true state.rooms[idx].isComplete = true
} }
handleStepProgression(currentRoom, state)
writeToSessionStorage({ writeToSessionStorage({
activeRoom: state.activeRoom,
booking: state.booking, booking: state.booking,
rooms: state.rooms, rooms: state.rooms,
}) })
@@ -408,10 +335,7 @@ export function createDetailsStore(
state.rooms[idx].isComplete = true state.rooms[idx].isComplete = true
} }
handleStepProgression(state.rooms[idx], state)
writeToSessionStorage({ writeToSessionStorage({
activeRoom: state.activeRoom,
booking: state.booking, booking: state.booking,
rooms: state.rooms, rooms: state.rooms,
}) })
@@ -461,10 +385,7 @@ export function createDetailsStore(
state.rooms[idx].isComplete = true state.rooms[idx].isComplete = true
} }
handleStepProgression(state.rooms[idx], state)
writeToSessionStorage({ writeToSessionStorage({
activeRoom: state.activeRoom,
booking: state.booking, booking: state.booking,
rooms: state.rooms, rooms: state.rooms,
}) })
@@ -490,8 +411,6 @@ export function createDetailsStore(
comment: "", comment: "",
}, },
}, },
currentStep,
isComplete: false, isComplete: false,
steps, steps,
} }

View File

@@ -1,7 +1,8 @@
import type { StepEnum } from "@/types/enums/step" import type { StepEnum } from "@/types/enums/step"
export interface SectionAccordionProps { export interface SectionProps {
header: string header: string
label: string label: string
step: StepEnum step: StepEnum
disabled?: boolean
} }

View File

@@ -1,19 +1,16 @@
import type { BreakfastPackage } from "@/types/components/hotelReservation/breakfast" import type { BreakfastPackage } from "@/types/components/hotelReservation/breakfast"
import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType" import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
import type { DetailsSchema } from "@/types/components/hotelReservation/enterDetails/details" import type { DetailsSchema } from "@/types/components/hotelReservation/enterDetails/details"
import type { StepEnum } from "@/types/enums/step"
import type { RoomState } from "@/types/stores/enter-details" import type { RoomState } from "@/types/stores/enter-details"
export interface RoomContextValue { export interface RoomContextValue {
actions: { actions: {
setStep: (step: StepEnum) => void
updateBedType: (data: BedTypeSchema) => void updateBedType: (data: BedTypeSchema) => void
updateBreakfast: (data: BreakfastPackage | false) => void updateBreakfast: (data: BreakfastPackage | false) => void
updateDetails: (data: DetailsSchema) => void updateDetails: (data: DetailsSchema) => void
} }
currentStep: RoomState["currentStep"]
isComplete: RoomState["isComplete"] isComplete: RoomState["isComplete"]
isActiveRoom: boolean idx: number
room: RoomState["room"] room: RoomState["room"]
roomNr: number roomNr: number
steps: RoomState["steps"] steps: RoomState["steps"]

View File

@@ -1,3 +1,3 @@
import { createDetailsStore } from "@/stores/enter-details" import type { createDetailsStore } from "@/stores/enter-details"
export type DetailsStore = ReturnType<typeof createDetailsStore> export type DetailsStore = ReturnType<typeof createDetailsStore>

View File

@@ -59,13 +59,11 @@ export interface Room extends InitialRoomData {
export interface RoomState { export interface RoomState {
actions: { actions: {
setStep: (step: StepEnum) => void
updateBedType: (data: BedTypeSchema) => void updateBedType: (data: BedTypeSchema) => void
updateBreakfast: (data: BreakfastPackage | false) => void updateBreakfast: (data: BreakfastPackage | false) => void
updateDetails: (data: DetailsSchema) => void updateDetails: (data: DetailsSchema) => void
updateMultiroomDetails: (data: MultiroomDetailsSchema) => void updateMultiroomDetails: (data: MultiroomDetailsSchema) => void
} }
currentStep: StepEnum | null
isComplete: boolean isComplete: boolean
room: Room room: Room
steps: { steps: {
@@ -88,7 +86,6 @@ export interface DetailsState {
toggleSummaryOpen: () => void toggleSummaryOpen: () => void
updateSeachParamString: (searchParamString: string) => void updateSeachParamString: (searchParamString: string) => void
} }
activeRoom: number
booking: SelectRateSearchParams booking: SelectRateSearchParams
breakfastPackages: BreakfastPackages | null breakfastPackages: BreakfastPackages | null
canProceedToPayment: boolean canProceedToPayment: boolean
@@ -102,7 +99,6 @@ export interface DetailsState {
} }
export type PersistedState = { export type PersistedState = {
activeRoom: number
booking: SelectRateSearchParams booking: SelectRateSearchParams
rooms: RoomState[] rooms: RoomState[]
} }