150 lines
12 KiB
JavaScript
150 lines
12 KiB
JavaScript
import React from 'react'
|
|
import { useForm, usePage } from '@inertiajs/react'
|
|
import StudioLayout from '../../Layouts/StudioLayout'
|
|
import NovaSelect from '../../components/ui/NovaSelect'
|
|
|
|
export default function StudioGroupChallengeEditor() {
|
|
const { props } = usePage()
|
|
const challenge = props.challenge || null
|
|
const outcomeArtworkOptions = Array.isArray(challenge?.artworks) ? challenge.artworks : []
|
|
const form = useForm({
|
|
title: challenge?.title || '',
|
|
summary: challenge?.summary || '',
|
|
description: challenge?.description || '',
|
|
visibility: challenge?.visibility || props.visibilityOptions?.[0]?.value || 'public',
|
|
participation_scope: challenge?.participation_scope || props.participationScopeOptions?.[0]?.value || 'group_only',
|
|
status: challenge?.status || props.statusOptions?.[0]?.value || 'draft',
|
|
start_at: challenge?.start_at ? challenge.start_at.slice(0, 16) : '',
|
|
end_at: challenge?.end_at ? challenge.end_at.slice(0, 16) : '',
|
|
rules_text: challenge?.rules_text || '',
|
|
submission_instructions: challenge?.submission_instructions || '',
|
|
judging_mode: challenge?.judging_mode || '',
|
|
linked_collection_id: challenge?.linked_collection?.id || '',
|
|
linked_project_id: challenge?.linked_project?.id || '',
|
|
featured_artwork_id: challenge?.featured_artwork?.id || '',
|
|
outcomes: Array.isArray(challenge?.outcomes) ? challenge.outcomes.map((outcome) => ({
|
|
artwork_id: outcome.artwork_id || '',
|
|
outcome_type: outcome.outcome_type || props.outcomeTypeOptions?.[0]?.value || 'winner',
|
|
position: outcome.position || '',
|
|
sort_order: outcome.sort_order ?? 0,
|
|
title_override: outcome.title_override || '',
|
|
note: outcome.note || '',
|
|
})) : [],
|
|
cover_file: null,
|
|
})
|
|
const attachForm = useForm({ artwork_id: '' })
|
|
|
|
const submit = (event) => {
|
|
event.preventDefault()
|
|
const options = { forceFormData: true, preserveScroll: true }
|
|
if (props.updateUrl) {
|
|
form.post(props.updateUrl, { ...options, _method: 'patch' })
|
|
return
|
|
}
|
|
form.post(props.storeUrl, options)
|
|
}
|
|
|
|
const updateOutcome = (index, key, value) => {
|
|
const next = [...(form.data.outcomes || [])]
|
|
next[index] = { ...next[index], [key]: value }
|
|
form.setData('outcomes', next)
|
|
}
|
|
|
|
const addOutcome = () => {
|
|
form.setData('outcomes', [
|
|
...(form.data.outcomes || []),
|
|
{
|
|
artwork_id: outcomeArtworkOptions[0]?.id || '',
|
|
outcome_type: props.outcomeTypeOptions?.[0]?.value || 'winner',
|
|
position: '',
|
|
sort_order: form.data.outcomes?.length || 0,
|
|
title_override: '',
|
|
note: '',
|
|
},
|
|
])
|
|
}
|
|
|
|
const removeOutcome = (index) => {
|
|
form.setData('outcomes', (form.data.outcomes || []).filter((_, currentIndex) => currentIndex !== index))
|
|
}
|
|
|
|
return (
|
|
<StudioLayout title={props.title} subtitle={props.description}>
|
|
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
|
|
<form onSubmit={submit} className="rounded-[28px] border border-white/10 bg-white/[0.03] p-6">
|
|
<div className="grid gap-4">
|
|
<input value={form.data.title} onChange={(event) => form.setData('title', event.target.value)} placeholder="Challenge title" className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
|
|
<textarea value={form.data.summary} onChange={(event) => form.setData('summary', event.target.value)} placeholder="Short summary" rows={3} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
|
|
<textarea value={form.data.description} onChange={(event) => form.setData('description', event.target.value)} placeholder="Challenge description" rows={8} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
|
|
<div className="grid gap-4 md:grid-cols-3">
|
|
<NovaSelect value={form.data.visibility} onChange={(val) => form.setData('visibility', val)} options={props.visibilityOptions || []} searchable={false} />
|
|
<NovaSelect value={form.data.participation_scope} onChange={(val) => form.setData('participation_scope', val)} options={props.participationScopeOptions || []} searchable={false} />
|
|
<NovaSelect value={form.data.status} onChange={(val) => form.setData('status', val)} options={props.statusOptions || []} searchable={false} />
|
|
</div>
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<input type="datetime-local" value={form.data.start_at} onChange={(event) => form.setData('start_at', event.target.value)} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
|
|
<input type="datetime-local" value={form.data.end_at} onChange={(event) => form.setData('end_at', event.target.value)} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
|
|
</div>
|
|
<textarea value={form.data.rules_text} onChange={(event) => form.setData('rules_text', event.target.value)} placeholder="Rules" rows={4} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
|
|
<textarea value={form.data.submission_instructions} onChange={(event) => form.setData('submission_instructions', event.target.value)} placeholder="Submission instructions" rows={4} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
|
|
<NovaSelect value={form.data.judging_mode || ''} onChange={(val) => form.setData('judging_mode', val)} placeholder="No judging mode" options={(props.judgingModeOptions || []).map((o) => ({ value: o.value, label: o.label }))} searchable={false} />
|
|
<div className="grid gap-4 md:grid-cols-3">
|
|
<NovaSelect value={String(form.data.linked_collection_id || '')} onChange={(val) => form.setData('linked_collection_id', val)} placeholder="No linked collection" options={(props.collectionOptions || []).map((o) => ({ value: String(o.id), label: o.title }))} />
|
|
<NovaSelect value={String(form.data.linked_project_id || '')} onChange={(val) => form.setData('linked_project_id', val)} placeholder="No linked project" options={(props.projectOptions || []).map((o) => ({ value: String(o.id), label: o.title }))} />
|
|
<div className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm leading-6 text-slate-300">
|
|
Featured result rendering now comes from structured outcomes. Attach entries first, then assign winners and finalists below.
|
|
</div>
|
|
</div>
|
|
<input type="file" accept="image/*" onChange={(event) => form.setData('cover_file', event.target.files?.[0] || null)} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
|
|
|
|
{props.updateUrl ? (
|
|
<div className="rounded-[28px] border border-white/10 bg-black/20 p-5">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-white">Challenge outcomes</h2>
|
|
<p className="mt-2 text-sm leading-6 text-slate-400">Choose from attached challenge entries only. These outcomes now drive public winners, finalists, and linked-world reward automation.</p>
|
|
</div>
|
|
<button type="button" onClick={addOutcome} className="rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-sm font-semibold text-white">Add outcome</button>
|
|
</div>
|
|
|
|
{outcomeArtworkOptions.length === 0 ? (
|
|
<div className="mt-4 rounded-2xl border border-dashed border-white/10 bg-black/20 p-4 text-sm leading-6 text-slate-400">Attach challenge entries first. Outcomes are intentionally limited to artworks already entered into this challenge.</div>
|
|
) : null}
|
|
|
|
<div className="mt-4 space-y-4">
|
|
{(form.data.outcomes || []).map((outcome, index) => (
|
|
<div key={`${outcome.artwork_id || 'new'}-${index}`} className="rounded-[24px] border border-white/10 bg-white/[0.03] p-4">
|
|
<div className="grid gap-3 md:grid-cols-3">
|
|
<NovaSelect value={String(outcome.artwork_id || '')} onChange={(val) => updateOutcome(index, 'artwork_id', val)} placeholder="Choose challenge entry" options={outcomeArtworkOptions.map((o) => ({ value: String(o.id), label: o.title }))} />
|
|
<NovaSelect value={outcome.outcome_type} onChange={(val) => updateOutcome(index, 'outcome_type', val)} options={props.outcomeTypeOptions || []} searchable={false} />
|
|
<button type="button" onClick={() => removeOutcome(index)} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-slate-300">Remove</button>
|
|
</div>
|
|
<div className="mt-3 grid gap-3 md:grid-cols-3">
|
|
<input value={outcome.position} onChange={(event) => updateOutcome(index, 'position', event.target.value)} placeholder="Position" className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
|
|
<input value={outcome.sort_order} onChange={(event) => updateOutcome(index, 'sort_order', event.target.value)} placeholder="Sort order" className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
|
|
<input value={outcome.title_override} onChange={(event) => updateOutcome(index, 'title_override', event.target.value)} placeholder="Optional label override" className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
|
|
</div>
|
|
<textarea value={outcome.note} onChange={(event) => updateOutcome(index, 'note', event.target.value)} placeholder="Optional editorial note" rows={3} className="mt-3 w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
<button type="submit" className="mt-6 rounded-full border border-white/10 bg-white/[0.05] px-5 py-2.5 text-sm font-semibold text-white">Save challenge</button>
|
|
</form>
|
|
|
|
<div className="space-y-6">
|
|
{props.publishUrl ? <form onSubmit={(event) => { event.preventDefault(); form.post(props.publishUrl, { preserveScroll: true }) }} className="rounded-[28px] border border-white/10 bg-white/[0.03] p-6"><button type="submit" className="rounded-full border border-white/10 bg-white/[0.05] px-5 py-2.5 text-sm font-semibold text-white">Publish challenge</button></form> : null}
|
|
{props.attachArtworkUrl ? (
|
|
<form onSubmit={(event) => { event.preventDefault(); attachForm.post(props.attachArtworkUrl, { preserveScroll: true }) }} className="rounded-[28px] border border-white/10 bg-white/[0.03] p-6">
|
|
<h2 className="text-lg font-semibold text-white">Attach artwork</h2>
|
|
<NovaSelect value={String(attachForm.data.artwork_id || '')} onChange={(val) => attachForm.setData('artwork_id', val)} placeholder="Choose artwork" options={(props.artworkOptions || []).map((o) => ({ value: String(o.id), label: o.title }))} />
|
|
<button type="submit" className="mt-4 rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-sm font-semibold text-white">Attach</button>
|
|
</form>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</StudioLayout>
|
|
)
|
|
} |