Files
SkinbaseNova/resources/js/components/upload/steps/Step3Publish.jsx
Gregor Klevze dc51d65440 feat: forum rich-text editor, emoji picker, mentions, discover nav, feed, uploads, profile
Forum:
- TipTap WYSIWYG editor with full toolbar
- @emoji-mart/react emoji picker (consistent with tweets)
- @mention autocomplete with user search API
- Fix PHP 8.4 parse errors in Blade templates
- Fix thread data display (paginator items)
- Align forum page widths to max-w-5xl

Discover:
- Extract shared _nav.blade.php partial
- Add missing nav links to for-you page
- Add Following link for authenticated users

Feed/Posts:
- Post model, controllers, policies, migrations
- Feed page components (PostComposer, FeedCard, etc)
- Post reactions, comments, saves, reports, sharing
- Scheduled publishing support
- Link preview controller

Profile:
- Profile page components (ProfileHero, ProfileTabs)
- Profile API controller

Uploads:
- Upload wizard enhancements
- Scheduled publish picker
- Studio status bar and readiness checklist
2026-03-03 09:48:31 +01:00

209 lines
8.1 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 { 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="bg-panel/80 backdrop-blur rounded-2xl shadow-xl shadow-black/40 ring-1 ring-white/10 p-6 space-y-5">
{/* Step header */}
<div className="rounded-xl bg-white/[0.04] px-4 py-3 ring-1 ring-white/8">
<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-xl ring-1 ring-white/8 bg-white/[0.025] p-4">
<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>
)
}