Added Stepper component

This commit is contained in:
Jose Selesan
2026-05-19 09:21:21 -03:00
parent c1d910708a
commit e905052cc1
3 changed files with 1310 additions and 40 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,15 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import {
Stepper,
StepperIndicator,
StepperItem,
StepperList,
StepperSeparator,
StepperTitle,
StepperTrigger,
} from '@/components/ui/stepper';
import { useIsMobile } from '@/hooks/use-mobile'; import { useIsMobile } from '@/hooks/use-mobile';
import { ApiClientError, apiClient } from '@/lib/api-client'; import { ApiClientError, apiClient } from '@/lib/api-client';
import { useTheme } from '@/lib/theme'; import { useTheme } from '@/lib/theme';
@@ -265,49 +274,43 @@ function PlayzerBrand({ compact = false }: { compact?: boolean }) {
} }
function ProgressSteps({ currentStep, mobile = false }: { currentStep: number; mobile?: boolean }) { function ProgressSteps({ currentStep, mobile = false }: { currentStep: number; mobile?: boolean }) {
const labels = ['Elegi cancha', 'Selecciona horario', 'Tus datos', 'Confirmacion']; const steps = [
{ value: 'court', label: 'Elegi cancha' },
{ value: 'time', label: 'Selecciona horario' },
{ value: 'details', label: 'Tus datos' },
{ value: 'confirm', label: 'Confirmacion' },
];
const activeStep = steps[Math.max(0, Math.min(currentStep - 1, steps.length - 1))]?.value;
return ( return (
<div <Stepper
className={ value={activeStep}
mobile nonInteractive
? 'mx-auto grid w-full max-w-[230px] grid-cols-[1fr_1fr_1fr_1fr] items-center' className={mobile ? 'mx-auto w-full max-w-[240px]' : 'max-w-[760px]'}
: 'flex items-center gap-5'
}
> >
{labels.map((label, index) => { <StepperList className="items-center gap-0">
const step = index + 1; {steps.map((step, index) => (
const isActive = step <= currentStep; <StepperItem
key={step.value}
return ( value={step.value}
<div key={label} className="flex min-w-0 items-center"> completed={index < currentStep - 1}
<div className="flex min-w-0 flex-1 items-center last:flex-none"
className={`flex size-7 shrink-0 items-center justify-center rounded-full text-xs font-semibold ${ >
isActive <StepperTrigger className="gap-2 rounded-full p-0 text-white/60 data-[state=active]:text-white data-[state=completed]:text-white">
? 'bg-emerald-500 text-white shadow-lg shadow-emerald-500/25' <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">
: 'bg-white/10 text-white/65' {index + 1}
}`} </StepperIndicator>
> {!mobile && (
{step} <StepperTitle className="whitespace-nowrap text-xs font-semibold text-inherit">
</div> {step.label}
{!mobile && ( </StepperTitle>
<span )}
className={`ml-3 text-xs ${isActive ? 'font-semibold text-white' : 'text-white/65'}`} </StepperTrigger>
> <StepperSeparator className="mx-3 h-px min-w-6 flex-1 bg-white/10 data-[state=completed]:bg-emerald-500/80" />
{label} </StepperItem>
</span> ))}
)} </StepperList>
{index < labels.length - 1 && ( </Stepper>
<div
className={`mx-2 h-px flex-1 ${isActive ? 'bg-emerald-500/70' : 'bg-white/10'} ${
mobile ? 'min-w-9' : 'w-8'
}`}
/>
)}
</div>
);
})}
</div>
); );
} }

View File

@@ -0,0 +1,61 @@
import * as React from 'react';
type PossibleRef<T> = React.Ref<T> | undefined;
/**
* Set a given ref to a given value
* This utility takes care of different types of refs: callback refs and RefObject(s)
*/
function setRef<T>(ref: PossibleRef<T>, value: T) {
if (typeof ref === 'function') {
return ref(value);
}
if (ref !== null && ref !== undefined) {
ref.current = value;
}
}
/**
* A utility to compose multiple refs together
* Accepts callback refs and RefObject(s)
*/
function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
return (node) => {
let hasCleanup = false;
const cleanups = refs.map((ref) => {
const cleanup = setRef(ref, node);
if (!hasCleanup && typeof cleanup === 'function') {
hasCleanup = true;
}
return cleanup;
});
// React <19 will log an error to the console if a callback ref returns a
// value. We don't use ref cleanups internally so this will only happen if a
// user's ref callback returns a value, which we only expect if they are
// using the cleanup functionality added in React 19.
if (hasCleanup) {
return () => {
for (let i = 0; i < cleanups.length; i++) {
const cleanup = cleanups[i];
if (typeof cleanup === 'function') {
cleanup();
} else {
setRef(refs[i], null);
}
}
};
}
};
}
/**
* A custom hook that composes multiple refs
* Accepts callback refs and RefObject(s)
*/
function useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
return React.useCallback(composeRefs(...refs), refs);
}
export { composeRefs, useComposedRefs };