nsfwapp/frontend/src/components/ui/ProgressBar.tsx
2026-01-13 14:00:05 +01:00

109 lines
3.3 KiB
TypeScript

'use client'
import * as React from 'react'
type ProgressBarProps = {
label?: React.ReactNode
value?: number | null // 0..100
indeterminate?: boolean // wenn true -> “läuft…” ohne Prozent
showPercent?: boolean // zeigt “xx%” (nur determinate)
rightLabel?: React.ReactNode // optionaler Text unter der Bar (z.B. 3/10)
steps?: string[] // optional: Step-Labels
currentStep?: number // 0-basiert
size?: 'sm' | 'md'
className?: string
}
export default function ProgressBar({
label,
value = 0,
indeterminate = false,
showPercent = false,
rightLabel,
steps,
currentStep,
size = 'md',
className,
}: ProgressBarProps) {
const clamped = Math.max(0, Math.min(100, Number(value) || 0))
const h = size === 'sm' ? 'h-1.5' : 'h-2'
const hasSteps = Array.isArray(steps) && steps.length > 0
const stepCount = hasSteps ? steps!.length : 0
const stepAlign = (i: number) => {
if (i === 0) return 'text-left'
if (i === stepCount - 1) return 'text-right'
return 'text-center'
}
const isActiveStep = (i: number) => {
if (typeof currentStep !== 'number' || !Number.isFinite(currentStep)) return false
return i <= currentStep
}
const showPct = showPercent && !indeterminate
return (
<div className={className}>
{/* ✅ Label + Prozent jetzt ÜBER der Bar */}
{(label || showPct) ? (
<div className="flex items-center justify-between gap-2">
{label ? (
<p className="flex-1 min-w-0 truncate text-xs font-medium text-gray-900 dark:text-white">
{label}
</p>
) : (
<span className="flex-1" />
)}
{showPct ? (
<span className="shrink-0 text-xs font-medium text-gray-700 dark:text-gray-300">
{Math.round(clamped)}%
</span>
) : null}
</div>
) : null}
<div aria-hidden="true" className={(label || showPct) ? 'mt-2' : ''}>
<div className="overflow-hidden rounded-full bg-gray-200 dark:bg-white/10">
{indeterminate ? (
<div className={`${h} w-full rounded-full bg-indigo-600/70 dark:bg-indigo-500/70 animate-pulse`} />
) : (
<div
className={`${h} rounded-full bg-indigo-600 dark:bg-indigo-500 transition-[width] duration-200`}
style={{ width: `${clamped}%` }}
/>
)}
</div>
{/* ✅ rightLabel bleibt unter der Bar (links), Prozent ist jetzt oben */}
{rightLabel ? (
<div className="mt-2 text-xs text-gray-600 dark:text-gray-400">
{rightLabel}
</div>
) : null}
{hasSteps ? (
<div
className="mt-3 hidden text-sm font-medium text-gray-600 sm:grid dark:text-gray-400"
style={{ gridTemplateColumns: `repeat(${stepCount}, minmax(0, 1fr))` }}
>
{steps!.map((s, i) => (
<div
key={`${i}-${s}`}
className={[
stepAlign(i),
isActiveStep(i) ? 'text-indigo-600 dark:text-indigo-400' : '',
].join(' ')}
>
{s}
</div>
))}
</div>
) : null}
</div>
</div>
)
}