Upload beautify

This commit is contained in:
2026-02-14 15:14:12 +01:00
parent e129618910
commit 79192345e3
249 changed files with 24436 additions and 1021 deletions

View File

@@ -0,0 +1,152 @@
import React from 'react'
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'
export default function UploadProgress({
title = 'Upload Artwork',
description = 'Preload → Details → Publish',
progress = 24,
status = 'Idle',
state,
processingStatus,
processingLabel = '',
isCancelling = false,
error = '',
onRetry,
onReset,
}) {
const prefersReducedMotion = useReducedMotion()
const getRecoveryHint = () => {
const text = String(error || '').toLowerCase()
if (!text) return ''
if (text.includes('network') || text.includes('timeout') || text.includes('failed to fetch')) {
return 'Your connection may be unstable. Retry now or wait a moment and try again.'
}
if (text.includes('busy') || text.includes('unavailable') || text.includes('503') || text.includes('server')) {
return 'The server looks busy right now. Waiting 2030 seconds before retrying can help.'
}
if (text.includes('validation') || text.includes('invalid') || text.includes('too large') || text.includes('format')) {
return 'Please review the file requirements, then update your selection and try again.'
}
return 'You can retry now, or reset this upload and start again with the same files.'
}
const recoveryHint = getRecoveryHint()
const resolvedStatus = (() => {
if (isCancelling) return 'Processing'
if (state === 'error') return 'Error'
if (processingStatus === 'ready') return 'Ready'
if (state === 'uploading') return 'Uploading'
if (state === 'processing' || state === 'finishing' || state === 'publishing') return 'Processing'
if (status) return status
return 'Idle'
})()
const statusTheme = {
Idle: 'border-slate-400/35 bg-slate-400/15 text-slate-200',
Uploading: 'border-sky-400/35 bg-sky-400/15 text-sky-100',
Processing: 'border-amber-400/35 bg-amber-400/15 text-amber-100',
Ready: 'border-emerald-400/35 bg-emerald-400/15 text-emerald-100',
Error: 'border-red-400/35 bg-red-400/15 text-red-100',
}
const statusColors = {
Idle: { borderColor: 'rgba(148,163,184,0.35)', backgroundColor: 'rgba(148,163,184,0.15)', color: 'rgb(226,232,240)' },
Uploading: { borderColor: 'rgba(56,189,248,0.35)', backgroundColor: 'rgba(56,189,248,0.15)', color: 'rgb(224,242,254)' },
Processing: { borderColor: 'rgba(251,191,36,0.35)', backgroundColor: 'rgba(251,191,36,0.15)', color: 'rgb(254,243,199)' },
Ready: { borderColor: 'rgba(52,211,153,0.35)', backgroundColor: 'rgba(52,211,153,0.15)', color: 'rgb(209,250,229)' },
Error: { borderColor: 'rgba(248,113,113,0.35)', backgroundColor: 'rgba(248,113,113,0.15)', color: 'rgb(254,226,226)' },
}
const quickTransition = prefersReducedMotion
? { duration: 0 }
: { duration: 0.2, ease: 'easeOut' }
const stepLabels = ['Preload', 'Details', 'Publish']
const stepIndex = progress >= 100 ? 2 : progress >= 34 ? 1 : 0
return (
<header className="rounded-xl border border-white/50 bg-gradient-to-br from-slate-900/80 to-slate-900/50 p-5 shadow-[0_12px_40px_rgba(0,0,0,0.35)] sm:p-6">
{/* Intended props: step, steps, phase, badge, progress, statusMessage */}
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<h1 className="text-2xl font-semibold tracking-tight text-white sm:text-3xl">{title}</h1>
<p className="mt-1 text-sm text-white/65">{description}</p>
</div>
<motion.span
className={`inline-flex items-center rounded-full border px-3 py-1 text-xs font-medium ${statusTheme[resolvedStatus] || statusTheme.Idle}`}
animate={statusColors[resolvedStatus] || statusColors.Idle}
transition={quickTransition}
>
{resolvedStatus}
</motion.span>
</div>
<div className="mt-4 flex items-center gap-2 overflow-x-auto">
{stepLabels.map((label, idx) => {
const active = idx <= stepIndex
return (
<div key={label} className="flex items-center gap-2">
<span className={`rounded-full border px-3 py-1 text-xs ${active ? 'border-emerald-400/40 bg-emerald-400/20 text-emerald-100' : 'border-white/15 bg-white/5 text-white/55'}`}>
{label}
</span>
{idx < stepLabels.length - 1 && <span className="text-white/30"></span>}
</div>
)
})}
</div>
<div className="mt-4">
<div className="h-2 overflow-hidden rounded-full bg-white/10">
<div
className="h-full rounded-full"
style={{
width: `${Math.max(0, Math.min(100, progress))}%`,
background: 'linear-gradient(90deg,#38bdf8,#06b6d4,#34d399)',
transition: prefersReducedMotion ? 'none' : 'width 200ms ease-out',
}}
/>
</div>
<p className="mt-2 text-right text-xs text-white/55">{Math.round(progress)}%</p>
</div>
<AnimatePresence initial={false}>
{(state === 'processing' || state === 'finishing' || state === 'publishing' || isCancelling) && (
<motion.div
key="processing-note"
initial={prefersReducedMotion ? false : { opacity: 0, y: 4 }}
animate={prefersReducedMotion ? {} : { opacity: 1, y: 0 }}
exit={prefersReducedMotion ? {} : { opacity: 0, y: -4 }}
transition={quickTransition}
className="mt-3 rounded-lg border border-cyan-300/25 bg-cyan-500/10 px-3 py-2 text-xs text-cyan-100"
>
{processingLabel || 'Analyzing content'} you can continue editing details while processing finishes.
</motion.div>
)}
</AnimatePresence>
<AnimatePresence initial={false}>
{error && (
<motion.div
key="progress-error"
initial={prefersReducedMotion ? false : { opacity: 0, y: 4 }}
animate={prefersReducedMotion ? {} : { opacity: 1, y: 0 }}
exit={prefersReducedMotion ? {} : { opacity: 0, y: -4 }}
transition={quickTransition}
className="mt-3 rounded-lg border border-rose-200/25 bg-rose-400/8 px-3 py-2"
>
<p className="text-sm font-medium text-rose-100">Something went wrong while uploading.</p>
<p className="mt-1 text-xs text-rose-100/90">You can retry safely. {error}</p>
{recoveryHint && <p className="mt-1 text-xs text-rose-100/80">{recoveryHint}</p>}
<div className="mt-2 flex flex-wrap gap-2">
<button type="button" onClick={onRetry} className="rounded-md border border-rose-200/35 bg-rose-400/10 px-2.5 py-1 text-xs text-rose-100 transition hover:bg-rose-400/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-200/75">Retry</button>
<button type="button" onClick={onReset} className="rounded-md border border-white/25 bg-white/10 px-2.5 py-1 text-xs text-white transition hover:bg-white/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/60">Reset</button>
</div>
</motion.div>
)}
</AnimatePresence>
</header>
)
}