Stepper improvement

This commit is contained in:
Jose Selesan
2026-05-19 09:32:26 -03:00
parent e905052cc1
commit 900e6146e7

View File

@@ -49,7 +49,7 @@ import {
Users,
Wrench,
} from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
@@ -190,6 +190,15 @@ function getStep(selectedSlot: SelectedSlot | null, isValid: boolean) {
return 4;
}
function getCompletedSteps(selectedSlot: SelectedSlot | null, isValid: boolean) {
return {
court: Boolean(selectedSlot),
time: Boolean(selectedSlot),
details: Boolean(selectedSlot && isValid),
confirm: false,
};
}
function getSelectedCourt(
courts: PublicAvailabilityCourt[],
selectedCourtId: string | undefined,
@@ -273,7 +282,15 @@ function PlayzerBrand({ compact = false }: { compact?: boolean }) {
);
}
function ProgressSteps({ currentStep, mobile = false }: { currentStep: number; mobile?: boolean }) {
function ProgressSteps({
currentStep,
completedSteps,
mobile = false,
}: {
currentStep: number;
completedSteps: Record<string, boolean>;
mobile?: boolean;
}) {
const steps = [
{ value: 'court', label: 'Elegi cancha' },
{ value: 'time', label: 'Selecciona horario' },
@@ -289,15 +306,31 @@ function ProgressSteps({ currentStep, mobile = false }: { currentStep: number; m
className={mobile ? 'mx-auto w-full max-w-[240px]' : 'max-w-[760px]'}
>
<StepperList className="items-center gap-0">
{steps.map((step, index) => (
{steps.map((step, index) => {
const isCompleted = completedSteps[step.value];
const isActive = step.value === activeStep;
return (
<StepperItem
key={step.value}
value={step.value}
completed={index < currentStep - 1}
completed={isCompleted}
className="flex min-w-0 flex-1 items-center last:flex-none"
>
<StepperTrigger className="gap-2 rounded-full p-0 text-white/60 data-[state=active]:text-white data-[state=completed]:text-white">
<StepperIndicator className="size-7 border-0 bg-white/10 text-xs font-bold text-white/62 shadow-none data-[state=active]:bg-emerald-500 data-[state=active]:text-white data-[state=completed]:bg-emerald-500 data-[state=completed]:text-white">
<StepperTrigger
className={`gap-2 rounded-full p-0 ${
isCompleted || isActive ? 'text-white' : 'text-white/48'
}`}
>
<StepperIndicator
className={`size-7 text-xs font-bold shadow-none ${
isCompleted
? 'border-0 bg-emerald-500 text-white'
: isActive
? 'border border-emerald-400 bg-white/10 text-white shadow-[0_0_0_4px_rgba(16,185,129,0.12)]'
: 'border-0 bg-white/10 text-white/62'
}`}
>
{index + 1}
</StepperIndicator>
{!mobile && (
@@ -306,9 +339,14 @@ function ProgressSteps({ currentStep, mobile = false }: { currentStep: number; m
</StepperTitle>
)}
</StepperTrigger>
<StepperSeparator className="mx-3 h-px min-w-6 flex-1 bg-white/10 data-[state=completed]:bg-emerald-500/80" />
<StepperSeparator
className={`mx-3 h-px min-w-6 flex-1 ${
isCompleted ? 'bg-emerald-500/80' : 'bg-white/10'
}`}
/>
</StepperItem>
))}
);
})}
</StepperList>
</Stepper>
);
@@ -340,10 +378,12 @@ function CourtIcon({ className = 'size-5' }: { className?: string }) {
function PublicBookingPageChrome({
children,
currentStep,
completedSteps,
mobile = false,
}: {
children: React.ReactNode;
currentStep: number;
completedSteps: Record<string, boolean>;
mobile?: boolean;
}) {
const { resolvedTheme } = useTheme();
@@ -373,7 +413,7 @@ function PublicBookingPageChrome({
</section>
<div className="mt-5">
<ProgressSteps currentStep={currentStep} mobile />
<ProgressSteps currentStep={currentStep} completedSteps={completedSteps} mobile />
</div>
{children}
@@ -415,7 +455,7 @@ function PublicBookingPageChrome({
Elegi tu cancha, selecciona el horario y completa tu reserva.
</p>
<div className="mt-7">
<ProgressSteps currentStep={currentStep} />
<ProgressSteps currentStep={currentStep} completedSteps={completedSteps} />
</div>
</section>
@@ -467,10 +507,11 @@ function PublicBookingDesktop(props: BookingShellProps) {
} = props;
const selectedCourt = getSelectedCourt(courts, selectedCourtId, selectedSlot);
const currentStep = getStep(selectedSlot, props.form.formState.isValid);
const completedSteps = getCompletedSteps(selectedSlot, props.form.formState.isValid);
const selectedDay = dayOptions.find((option) => option.value === selectedDate);
return (
<PublicBookingPageChrome currentStep={currentStep}>
<PublicBookingPageChrome currentStep={currentStep} completedSteps={completedSteps}>
<div className="mx-auto grid max-w-[1320px] grid-cols-[330px_minmax(0,1fr)] gap-5 px-10">
<aside className="space-y-4">
<Panel className="p-5">
@@ -647,10 +688,11 @@ function PublicBookingDesktop(props: BookingShellProps) {
function PublicBookingMobile(props: BookingShellProps) {
const selectedCourt = getSelectedCourt(props.courts, props.selectedCourtId, props.selectedSlot);
const currentStep = getStep(props.selectedSlot, props.form.formState.isValid);
const completedSteps = getCompletedSteps(props.selectedSlot, props.form.formState.isValid);
const selectedDay = props.dayOptions.find((option) => option.value === props.selectedDate);
return (
<PublicBookingPageChrome currentStep={currentStep} mobile>
<PublicBookingPageChrome currentStep={currentStep} completedSteps={completedSteps} mobile>
<div className="mt-5 space-y-3">
<Panel className="p-3">
<h2 className="text-base font-semibold">Selecciona tu cancha</h2>
@@ -1098,6 +1140,16 @@ function SelectedSlotCard(props: BookingShellProps & { compact?: boolean }) {
handleSubmit,
formState: { errors, isSubmitting, isValid },
} = form;
const customerNameInputRef = useRef<HTMLInputElement | null>(null);
const customerNameRegistration = register('customerName');
useEffect(() => {
if (!selectedSlot) return;
window.requestAnimationFrame(() => {
customerNameInputRef.current?.focus();
});
}, [selectedSlot]);
return (
<Panel className={compact ? 'border-emerald-500/70 p-3' : 'p-6'}>
@@ -1130,7 +1182,11 @@ function SelectedSlotCard(props: BookingShellProps & { compact?: boolean }) {
placeholder="Ej: Juan Perez"
aria-invalid={Boolean(errors.customerName)}
className="border-white/10 bg-white/[0.045] text-white placeholder:text-white/32"
{...register('customerName')}
{...customerNameRegistration}
ref={(element) => {
customerNameRegistration.ref(element);
customerNameInputRef.current = element;
}}
/>
<FieldError errors={[errors.customerName]} />
</Field>