Remove dead admin UI code, redesign dashboard followers/following and upload experiences, and add schema audit tooling with repair migrations for forum and upload drift.
209 lines
8.2 KiB
JavaScript
209 lines
8.2 KiB
JavaScript
import React from 'react'
|
||
import { motion, useReducedMotion } from 'framer-motion'
|
||
|
||
/**
|
||
* PublishCheckBadge – a single status item for the review section
|
||
*/
|
||
function PublishCheckBadge({ label, ok }) {
|
||
return (
|
||
<span
|
||
className={[
|
||
'inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs',
|
||
ok
|
||
? 'border-emerald-300/40 bg-emerald-500/12 text-emerald-100'
|
||
: 'border-white/15 bg-white/5 text-white/55',
|
||
].join(' ')}
|
||
>
|
||
<span aria-hidden="true">{ok ? '✓' : '○'}</span>
|
||
{label}
|
||
</span>
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Step3Publish
|
||
*
|
||
* Step 3 of the upload wizard: review summary and publish action.
|
||
* Shows a compact artwork preview, metadata summary, readiness badges,
|
||
* and a summary of publish mode / schedule + visibility.
|
||
*
|
||
* Publish controls (mode/schedule picker) live in PublishPanel (sidebar).
|
||
* This step serves as the final review before the user clicks Publish.
|
||
*/
|
||
export default function Step3Publish({
|
||
headingRef,
|
||
// Asset
|
||
primaryFile,
|
||
primaryPreviewUrl,
|
||
isArchive,
|
||
screenshots,
|
||
fileMetadata,
|
||
// Metadata
|
||
metadata,
|
||
// Readiness
|
||
canPublish,
|
||
uploadReady,
|
||
// Publish options (from wizard state, for summary display only)
|
||
publishMode = 'now',
|
||
scheduledAt = null,
|
||
timezone = null,
|
||
visibility = 'public',
|
||
// Category tree (for label lookup)
|
||
allRootCategoryOptions = [],
|
||
filteredCategoryTree = [],
|
||
}) {
|
||
const prefersReducedMotion = useReducedMotion()
|
||
const quickTransition = prefersReducedMotion ? { duration: 0 } : { duration: 0.2, ease: 'easeOut' }
|
||
|
||
const hasPreview = Boolean(primaryPreviewUrl && !isArchive)
|
||
|
||
// ── Category label lookup ────────────────────────────────────────────────
|
||
const rootCategory = allRootCategoryOptions.find(
|
||
(r) => String(r.id) === String(metadata.rootCategoryId)
|
||
) ?? null
|
||
const rootLabel = rootCategory?.name ?? null
|
||
const subCategory = rootCategory?.children?.find(
|
||
(c) => String(c.id) === String(metadata.subCategoryId)
|
||
) ?? null
|
||
const subLabel = subCategory?.name ?? null
|
||
|
||
const checks = [
|
||
{ label: 'File uploaded', ok: uploadReady },
|
||
{ label: 'Scan passed', ok: uploadReady },
|
||
{ label: 'Preview ready', ok: hasPreview || (isArchive && screenshots.length > 0) },
|
||
{ label: 'Rights confirmed', ok: Boolean(metadata.rightsAccepted) },
|
||
{ label: 'Tags added', ok: Array.isArray(metadata.tags) && metadata.tags.length > 0 },
|
||
]
|
||
|
||
return (
|
||
<div className="space-y-5 rounded-[28px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))] p-6 shadow-[0_24px_90px_rgba(2,8,23,0.28)] backdrop-blur sm:p-7">
|
||
{/* Step header */}
|
||
<div className="rounded-2xl border border-white/8 bg-white/[0.04] px-5 py-4">
|
||
<h2
|
||
ref={headingRef}
|
||
tabIndex={-1}
|
||
className="text-lg font-semibold text-white focus:outline-none"
|
||
>
|
||
Review & publish
|
||
</h2>
|
||
<p className="mt-1 text-sm text-white/60">
|
||
Everything looks good? Hit <span className="text-white/85">Publish</span> to make your artwork live.
|
||
</p>
|
||
</div>
|
||
|
||
{/* Preview + summary */}
|
||
<div className="rounded-2xl ring-1 ring-white/8 bg-white/[0.025] p-5">
|
||
<div className="flex flex-col gap-4 sm:flex-row">
|
||
{/* Artwork thumbnail */}
|
||
<div className="shrink-0">
|
||
{hasPreview ? (
|
||
<div className="flex h-[140px] w-[140px] items-center justify-center overflow-hidden rounded-xl ring-1 ring-white/10 bg-black/30">
|
||
<img
|
||
src={primaryPreviewUrl}
|
||
alt="Artwork preview"
|
||
className="max-h-full max-w-full object-contain"
|
||
loading="lazy"
|
||
decoding="async"
|
||
width={140}
|
||
height={140}
|
||
/>
|
||
</div>
|
||
) : (
|
||
<div className="grid h-[140px] w-[140px] place-items-center rounded-xl ring-1 ring-white/10 bg-white/5 text-white/40">
|
||
<svg className="h-8 w-8" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden="true">
|
||
<path strokeLinecap="round" strokeLinejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||
</svg>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Summary */}
|
||
<div className="min-w-0 flex-1 space-y-2.5">
|
||
<p className="text-base font-semibold text-white leading-snug">
|
||
{metadata.title || <span className="text-white/45 italic">Untitled artwork</span>}
|
||
</p>
|
||
|
||
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-white/55">
|
||
{metadata.contentType && (
|
||
<span className="capitalize">Type: <span className="text-white/75">{metadata.contentType}</span></span>
|
||
)}
|
||
{rootLabel && (
|
||
<span>Category: <span className="text-white/75">{rootLabel}</span></span>
|
||
)}
|
||
{subLabel && (
|
||
<span>Sub: <span className="text-white/75">{subLabel}</span></span>
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-white/55">
|
||
<span>Tags: <span className="text-white/75">{(metadata.tags || []).length}</span></span>
|
||
{!isArchive && fileMetadata?.resolution && fileMetadata.resolution !== '—' && (
|
||
<span>Resolution: <span className="text-white/75">{fileMetadata.resolution}</span></span>
|
||
)}
|
||
{isArchive && (
|
||
<span>Screenshots: <span className="text-white/75">{screenshots.length}</span></span>
|
||
)}
|
||
</div>
|
||
|
||
{metadata.description && (
|
||
<p className="line-clamp-2 text-xs text-white/50">{metadata.description}</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Publish summary: visibility + schedule */}
|
||
<div className="flex flex-wrap gap-3">
|
||
<span className="inline-flex items-center gap-1.5 rounded-full border border-white/15 bg-white/6 px-2.5 py-1 text-xs text-white/60">
|
||
👁 {visibility === 'public' ? 'Public' : visibility === 'unlisted' ? 'Unlisted' : 'Private'}
|
||
</span>
|
||
{publishMode === 'schedule' && scheduledAt ? (
|
||
<span className="inline-flex items-center gap-1.5 rounded-full border border-violet-300/30 bg-violet-500/15 px-2.5 py-1 text-xs text-violet-200">
|
||
🕐 Scheduled
|
||
{timezone && (
|
||
<span className="text-violet-300/70">
|
||
{' '}·{' '}
|
||
{new Intl.DateTimeFormat('en-GB', {
|
||
timeZone: timezone,
|
||
weekday: 'short', day: 'numeric', month: 'short',
|
||
hour: '2-digit', minute: '2-digit', hour12: false,
|
||
}).format(new Date(scheduledAt))}
|
||
</span>
|
||
)}
|
||
</span>
|
||
) : (
|
||
<span className="inline-flex items-center gap-1.5 rounded-full border border-emerald-300/30 bg-emerald-500/12 px-2.5 py-1 text-xs text-emerald-200">
|
||
⚡ Publish immediately
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* Readiness badges */}
|
||
<div>
|
||
<p className="mb-2.5 text-xs uppercase tracking-wide text-white/40">Readiness checks</p>
|
||
<div className="flex flex-wrap gap-2">
|
||
{checks.map((check) => (
|
||
<PublishCheckBadge key={check.label} label={check.label} ok={check.ok} />
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Not-ready notice */}
|
||
{!canPublish && (
|
||
<motion.div
|
||
initial={prefersReducedMotion ? false : { opacity: 0, y: 4 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={quickTransition}
|
||
className="rounded-lg ring-1 ring-amber-300/25 bg-amber-500/8 px-4 py-3 text-sm text-amber-100/85"
|
||
>
|
||
{!uploadReady
|
||
? 'Waiting for upload processing to complete…'
|
||
: !metadata.rightsAccepted
|
||
? 'Please confirm rights in the Details step to enable publishing.'
|
||
: 'Complete all required fields to enable publishing.'}
|
||
</motion.div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|