Stepper improvement
This commit is contained in:
@@ -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,26 +306,47 @@ 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) => (
|
||||
<StepperItem
|
||||
key={step.value}
|
||||
value={step.value}
|
||||
completed={index < currentStep - 1}
|
||||
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">
|
||||
{index + 1}
|
||||
</StepperIndicator>
|
||||
{!mobile && (
|
||||
<StepperTitle className="whitespace-nowrap text-xs font-semibold text-inherit">
|
||||
{step.label}
|
||||
</StepperTitle>
|
||||
)}
|
||||
</StepperTrigger>
|
||||
<StepperSeparator className="mx-3 h-px min-w-6 flex-1 bg-white/10 data-[state=completed]:bg-emerald-500/80" />
|
||||
</StepperItem>
|
||||
))}
|
||||
{steps.map((step, index) => {
|
||||
const isCompleted = completedSteps[step.value];
|
||||
const isActive = step.value === activeStep;
|
||||
|
||||
return (
|
||||
<StepperItem
|
||||
key={step.value}
|
||||
value={step.value}
|
||||
completed={isCompleted}
|
||||
className="flex min-w-0 flex-1 items-center last:flex-none"
|
||||
>
|
||||
<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 && (
|
||||
<StepperTitle className="whitespace-nowrap text-xs font-semibold text-inherit">
|
||||
{step.label}
|
||||
</StepperTitle>
|
||||
)}
|
||||
</StepperTrigger>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user