Files
SkinbaseNova/resources/js/components/worlds/WorldSubmissionSelector.jsx
2026-04-18 17:02:56 +02:00

164 lines
8.0 KiB
JavaScript

import React from 'react'
function statusTone(item) {
if (item?.is_featured) {
return 'border-amber-300/30 bg-amber-400/10 text-amber-100'
}
switch (item?.status) {
case 'live':
return 'border-emerald-300/30 bg-emerald-400/10 text-emerald-100'
case 'removed':
return 'border-orange-300/30 bg-orange-400/10 text-orange-100'
case 'blocked':
return 'border-rose-300/30 bg-rose-400/10 text-rose-100'
case 'pending':
return 'border-sky-300/30 bg-sky-400/10 text-sky-100'
default:
return 'border-white/10 bg-white/[0.04] text-slate-300'
}
}
function modeTone(mode) {
switch (mode) {
case 'manual_approval':
return 'border-sky-300/30 bg-sky-400/10 text-sky-100'
case 'auto_add':
return 'border-emerald-300/30 bg-emerald-400/10 text-emerald-100'
default:
return 'border-white/10 bg-white/[0.04] text-slate-300'
}
}
function dateBadgeLabel(item) {
const timeframe = String(item?.timeframe_label || '').trim()
const submissionWindow = String(item?.submission_window_label || '').trim()
if (timeframe && submissionWindow) {
return timeframe === submissionWindow ? timeframe : `${submissionWindow}${timeframe}`
}
return submissionWindow || timeframe || ''
}
export default function WorldSubmissionSelector({
title = 'Add to Worlds',
description = 'Attach this artwork to active worlds while keeping community participation separate from curated editorial relations.',
options = [],
emptyMessage = 'No worlds are currently open for creator participation.',
onToggle,
onNoteChange,
className = '',
}) {
const items = Array.isArray(options) ? options : []
return (
<section className={`rounded-[28px] border border-white/10 bg-white/[0.03] p-5 ${className}`.trim()}>
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div>
<h2 className="text-xl font-semibold text-white">{title}</h2>
<p className="mt-1 max-w-3xl text-sm leading-6 text-slate-400">{description}</p>
</div>
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-slate-500">{items.filter((item) => item.selected).length} selected</div>
</div>
{items.length === 0 ? (
<div className="mt-5 rounded-[24px] border border-dashed border-white/10 bg-white/[0.02] p-5 text-sm text-slate-400">{emptyMessage}</div>
) : (
<div className="mt-5 grid gap-4 xl:grid-cols-2">
{items.map((item) => {
const checked = Boolean(item.selected)
const locked = Boolean(item.selection_locked)
const combinedDateLabel = dateBadgeLabel(item)
return (
<div key={item.id} className={`overflow-hidden rounded-[24px] border ${checked ? 'border-sky-300/25 bg-sky-400/[0.07]' : 'border-white/10 bg-black/20'}`}>
<button
type="button"
onClick={() => !locked && onToggle?.(item.id)}
disabled={locked}
className="w-full p-4 text-left disabled:cursor-not-allowed disabled:opacity-100"
>
<div className="grid gap-4 md:grid-cols-[auto_minmax(0,1fr)_auto] md:items-start">
<div className="h-24 w-24 shrink-0 overflow-hidden rounded-[20px] border border-white/10 bg-slate-950/80">
{item.cover_url ? (
<img src={item.cover_url} alt="" className="h-full w-full object-cover" />
) : (
<div className="flex h-full items-center justify-center bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.16),_transparent_48%),linear-gradient(135deg,_rgba(15,23,42,0.96),_rgba(2,6,23,0.96))] text-slate-500">
<i className="fa-solid fa-globe text-lg" />
</div>
)}
</div>
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<h3 className="text-base font-semibold text-white">{item.title}</h3>
{item.status_label ? (
<span className={`rounded-full border px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.14em] ${statusTone(item)}`}>
{item.status_label}
</span>
) : null}
{item.participation_mode_label ? (
<span className={`rounded-full border px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.14em] ${modeTone(item.participation_mode)}`}>
{item.participation_mode_label}
</span>
) : null}
</div>
{item.tagline ? <p className="mt-1 text-xs uppercase tracking-[0.14em] text-slate-500">{item.tagline}</p> : null}
</div>
<span className={`inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full border text-xs md:mt-0.5 ${checked ? 'border-sky-300/40 bg-sky-400/20 text-sky-100' : 'border-white/10 bg-white/[0.04] text-slate-500'}`}>
{checked ? '✓' : ''}
</span>
{item.summary ? <p className="text-sm leading-6 text-slate-300 md:col-span-3">{item.summary}</p> : null}
<div className="flex flex-wrap gap-2 text-xs text-slate-300 md:col-span-3">
{combinedDateLabel ? <span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1">{combinedDateLabel}</span> : null}
{item.can_resubmit ? <span className="rounded-full border border-amber-300/25 bg-amber-400/10 px-3 py-1 text-amber-100">Can re-add</span> : null}
</div>
</div>
</button>
<div className="border-t border-white/10 px-4 py-4">
{item.submission_guidelines ? (
<div className="rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm leading-6 text-slate-300">
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">World guidelines</div>
<div className="mt-2">{item.submission_guidelines}</div>
</div>
) : null}
{item.selection_locked_reason ? (
<div className="mt-3 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-400">{item.selection_locked_reason}</div>
) : null}
{item.reviewer_note ? (
<div className="mt-3 rounded-2xl border border-amber-300/20 bg-amber-400/10 px-4 py-3 text-sm text-amber-50">
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-amber-100/80">Moderator note</div>
<div className="mt-2 leading-6">{item.reviewer_note}</div>
</div>
) : null}
{checked && item.submission_note_enabled ? (
<label className="mt-3 grid gap-2 text-sm text-slate-300">
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Creator note</span>
<textarea
rows={4}
value={item.note || ''}
onChange={(event) => onNoteChange?.(item.id, event.target.value)}
disabled={locked}
placeholder="Optional note for world moderators: fit, context, challenge angle, or why this artwork belongs here."
className="nova-scrollbar rounded-[20px] border border-white/10 bg-black/20 px-4 py-3 text-white outline-none disabled:cursor-not-allowed disabled:opacity-60"
/>
</label>
) : null}
</div>
</div>
)
})}
</div>
)}
</section>
)
}