219 lines
8.5 KiB
JavaScript
219 lines
8.5 KiB
JavaScript
import React from 'react'
|
||
import { motion, useReducedMotion } from 'framer-motion'
|
||
|
||
function stripHtml(value) {
|
||
return String(value || '')
|
||
.replace(/<[^>]*>/g, ' ')
|
||
.replace(/ /g, ' ')
|
||
.replace(/\s+/g, ' ')
|
||
.trim()
|
||
}
|
||
|
||
/**
|
||
* 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 descriptionPreview = stripHtml(metadata.description)
|
||
|
||
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>
|
||
<span>Audience: <span className="text-white/75">{metadata.isMature ? 'Mature' : 'General'}</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>
|
||
|
||
{descriptionPreview && (
|
||
<p className="line-clamp-2 text-xs text-white/50">{descriptionPreview}</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>
|
||
)
|
||
}
|