Files
SkinbaseNova/resources/js/components/upload/UploadProgress.jsx

142 lines
6.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 quickTransition = prefersReducedMotion
? { duration: 0 }
: { duration: 0.2, ease: 'easeOut' }
const stepLabels = ['Preload', 'Details', 'Publish']
const stepIndex = progress >= 100 ? 2 : progress >= 34 ? 1 : 0
const progressValue = Math.max(0, Math.min(100, Number(progress) || 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>
<span
className={`inline-flex items-center rounded-full border px-3 py-1 text-xs font-medium ${statusTheme[resolvedStatus] || statusTheme.Idle}`}
>
{resolvedStatus}
</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">
<motion.div
className="h-full origin-left rounded-full bg-gradient-to-r from-sky-400/90 via-cyan-300/90 to-emerald-300/90"
initial={false}
animate={{ scaleX: progressValue / 100 }}
transition={quickTransition}
/>
</div>
<p className="mt-2 text-right text-xs text-white/55">{Math.round(progressValue)}%</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>
)
}