Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
145 lines
4.9 KiB
TypeScript
145 lines
4.9 KiB
TypeScript
'use client'
|
||
|
||
import { cn } from '@/lib/cn'
|
||
import { WIZARD_STEPS } from '@/components/events/create/types'
|
||
import { texts, format } from '@/texts'
|
||
|
||
interface WizardStepperProps {
|
||
currentStep: number
|
||
className?: string
|
||
}
|
||
|
||
function StepCircle({ step, isActive, isDone }: { step: number; isActive: boolean; isDone: boolean }) {
|
||
return (
|
||
<div
|
||
className={cn(
|
||
'relative z-10 flex size-10 shrink-0 items-center justify-center rounded-full border-2 text-sm font-bold transition-all duration-300',
|
||
isActive && 'border-primary bg-primary text-white shadow-md shadow-primary/25 scale-110',
|
||
isDone && !isActive && 'border-fifth bg-fifth text-white',
|
||
!isActive && !isDone && 'border-default-300 bg-white text-default-400'
|
||
)}
|
||
>
|
||
{isDone && !isActive ? (
|
||
<svg
|
||
aria-hidden
|
||
className="size-5"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
strokeWidth={2.5}
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
d="M5 13l4 4L19 7"
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
/>
|
||
</svg>
|
||
) : (
|
||
step
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default function WizardStepper({ currentStep, className }: WizardStepperProps) {
|
||
const current = WIZARD_STEPS.find((step) => step.key === currentStep)
|
||
const progress = Math.round((currentStep / WIZARD_STEPS.length) * 100)
|
||
|
||
return (
|
||
<nav
|
||
aria-label={texts.events.wizardStepsAria}
|
||
className={cn('w-full', className)}
|
||
>
|
||
<div className="sm:hidden">
|
||
<div className="mb-2 flex items-center justify-between gap-3 text-xs">
|
||
<span className="font-bold text-consumer-text">
|
||
مرحله {currentStep} از {WIZARD_STEPS.length} — {current?.label}
|
||
</span>
|
||
<span className="text-secondary-20">{progress}٪</span>
|
||
</div>
|
||
<div
|
||
aria-label={format(texts.events.wizardProgressAria, { progress })}
|
||
aria-valuemax={100}
|
||
aria-valuemin={0}
|
||
aria-valuenow={progress}
|
||
className="h-2 overflow-hidden rounded-full bg-default-200"
|
||
role="progressbar"
|
||
>
|
||
<div
|
||
className="h-full rounded-full bg-primary transition-[width] duration-300"
|
||
style={{ width: `${progress}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<ol className="hidden items-start justify-between gap-2 sm:flex">
|
||
{WIZARD_STEPS.map((step, index) => {
|
||
const isActive = step.key === currentStep
|
||
const isDone = step.key < currentStep
|
||
const isLast = index === WIZARD_STEPS.length - 1
|
||
|
||
return (
|
||
<li
|
||
key={step.key}
|
||
className={cn('flex flex-1 min-w-0', isLast && 'flex-none max-w-[4.5rem] md:max-w-none')}
|
||
>
|
||
<div className="flex flex-col items-center w-full min-w-0">
|
||
<div className="flex items-center w-full">
|
||
{/* connector before (RTL: appears on the right side visually for non-first) */}
|
||
{index > 0 ? (
|
||
<div
|
||
aria-hidden
|
||
className={cn(
|
||
'h-1 flex-1 min-w-2 transition-colors duration-300',
|
||
step.key <= currentStep ? 'bg-fifth' : 'bg-default-200'
|
||
)}
|
||
/>
|
||
) : (
|
||
<div className="flex-1 min-w-2" />
|
||
)}
|
||
|
||
<StepCircle
|
||
isActive={isActive}
|
||
isDone={isDone}
|
||
step={step.key}
|
||
/>
|
||
|
||
{!isLast ? (
|
||
<div
|
||
aria-hidden
|
||
className={cn(
|
||
'h-1 flex-1 min-w-2 transition-colors duration-300',
|
||
step.key < currentStep ? 'bg-fifth' : 'bg-default-200'
|
||
)}
|
||
/>
|
||
) : (
|
||
<div className="flex-1 min-w-2" />
|
||
)}
|
||
</div>
|
||
|
||
<div className="mt-3 text-center px-1 w-full min-w-0">
|
||
<p
|
||
className={cn(
|
||
'text-smtext-xs font-medium leading-tight truncate',
|
||
isActive && 'text-primary font-semibold',
|
||
isDone && !isActive && 'text-fifth',
|
||
!isActive && !isDone && 'text-default-400'
|
||
)}
|
||
>
|
||
{step.label}
|
||
</p>
|
||
{isActive ? (
|
||
<span className="inline-block mt-1 text-[10px] text-primary/80 bg-primary-50 px-2 py-1 rounded-full">
|
||
{texts.events.wizardInProgress}
|
||
</span>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
</li>
|
||
)
|
||
})}
|
||
</ol>
|
||
</nav>
|
||
)
|
||
}
|