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