133 lines
13 KiB
JavaScript
133 lines
13 KiB
JavaScript
import React from 'react'
|
|
import { router, useForm, usePage } from '@inertiajs/react'
|
|
import StudioLayout from '../../Layouts/StudioLayout'
|
|
import Checkbox from '../../components/ui/Checkbox'
|
|
import DateTimePicker from '../../components/ui/DateTimePicker'
|
|
import NovaSelect from '../../components/ui/NovaSelect'
|
|
|
|
function toDateTimeInput(value) {
|
|
return value ? String(value).slice(0, 16) : ''
|
|
}
|
|
|
|
export default function StudioGroupReleaseEditor() {
|
|
const { props } = usePage()
|
|
const release = props.release || null
|
|
const form = useForm({
|
|
title: release?.title || '',
|
|
summary: release?.summary || '',
|
|
description: release?.description || '',
|
|
release_notes: release?.release_notes || '',
|
|
visibility: release?.visibility || props.visibilityOptions?.[0]?.value || 'public',
|
|
status: release?.status || props.statusOptions?.[0]?.value || 'draft',
|
|
current_stage: release?.current_stage || props.stageOptions?.[0]?.value || 'concept',
|
|
planned_release_at: toDateTimeInput(release?.planned_release_at),
|
|
lead_user_id: release?.lead?.id || '',
|
|
linked_project_id: release?.linked_project?.id || '',
|
|
linked_collection_id: release?.linked_collection?.id || '',
|
|
featured_artwork_id: release?.featured_artwork?.id || '',
|
|
is_featured: Boolean(release?.is_featured),
|
|
cover_file: null,
|
|
})
|
|
const stageForm = useForm({ current_stage: release?.current_stage || props.stageOptions?.[0]?.value || 'concept' })
|
|
const artworkAttach = useForm({ artwork_id: '' })
|
|
const contributorForm = useForm({ user_id: '', role_label: '' })
|
|
const milestoneForm = useForm({ title: '', summary: '', status: 'pending', due_date: '', owner_user_id: '', notes: '' })
|
|
|
|
const submit = (event) => {
|
|
event.preventDefault()
|
|
const options = { forceFormData: true, preserveScroll: true }
|
|
if (props.updateUrl) {
|
|
form.transform((data) => ({ ...data, _method: 'patch' })).post(props.updateUrl, options)
|
|
return
|
|
}
|
|
form.post(props.storeUrl, options)
|
|
}
|
|
|
|
return (
|
|
<StudioLayout title={props.title} subtitle={props.description}>
|
|
<form onSubmit={submit} className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
|
|
<section 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="Release 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="Release overview" rows={8} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
|
|
<textarea value={form.data.release_notes} onChange={(event) => form.setData('release_notes', event.target.value)} placeholder="Release notes" rows={7} 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.status} onChange={(val) => form.setData('status', val)} options={props.statusOptions || []} searchable={false} />
|
|
<NovaSelect value={form.data.current_stage} onChange={(val) => form.setData('current_stage', val)} options={props.stageOptions || []} searchable={false} />
|
|
</div>
|
|
<DateTimePicker value={form.data.planned_release_at} onChange={(nextValue) => form.setData('planned_release_at', nextValue)} placeholder="Planned release" clearable className="bg-black/20" />
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<NovaSelect value={String(form.data.lead_user_id || '')} onChange={(val) => form.setData('lead_user_id', val)} placeholder="No release lead" options={(props.memberOptions || []).map((o) => ({ value: String(o.id), label: o.name || o.username }))} />
|
|
<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>
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<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.featured_artwork_id || '')} onChange={(val) => form.setData('featured_artwork_id', val)} placeholder="No featured artwork" options={(props.artworkOptions || []).map((o) => ({ value: String(o.id), label: o.title }))} />
|
|
</div>
|
|
<div className="flex items-center gap-3 rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white">
|
|
<Checkbox checked={form.data.is_featured} onChange={(event) => form.setData('is_featured', event.target.checked)} label="Feature this release on the public group page" />
|
|
</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" />
|
|
</div>
|
|
<div className="mt-6 flex flex-wrap gap-3">
|
|
<button type="submit" disabled={form.processing} className="rounded-full border border-white/10 bg-white/[0.05] px-5 py-2.5 text-sm font-semibold text-white">{form.processing ? 'Saving…' : 'Save release'}</button>
|
|
{release?.url ? <a href={release.url} className="rounded-full border border-white/10 bg-black/20 px-5 py-2.5 text-sm font-semibold text-white">View public page</a> : null}
|
|
</div>
|
|
</section>
|
|
|
|
<div className="space-y-6">
|
|
{props.stageUrl ? (
|
|
<form onSubmit={(event) => { event.preventDefault(); stageForm.post(props.stageUrl, { preserveScroll: true }) }} className="rounded-[28px] border border-white/10 bg-white/[0.03] p-6">
|
|
<h2 className="text-lg font-semibold text-white">Stage</h2>
|
|
<NovaSelect value={stageForm.data.current_stage} onChange={(val) => stageForm.setData('current_stage', val)} options={props.stageOptions || []} searchable={false} />
|
|
<div className="mt-4 flex flex-wrap gap-2">
|
|
<button type="submit" className="rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-sm font-semibold text-white">Update stage</button>
|
|
{props.publishUrl ? <button type="button" onClick={() => router.post(props.publishUrl, {}, { preserveScroll: true })} className="rounded-full border border-emerald-300/20 bg-emerald-300/10 px-4 py-2 text-sm font-semibold text-emerald-100">Publish</button> : null}
|
|
</div>
|
|
</form>
|
|
) : null}
|
|
|
|
{props.attachArtworkUrl ? (
|
|
<form onSubmit={(event) => { event.preventDefault(); artworkAttach.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(artworkAttach.data.artwork_id || '')} onChange={(val) => artworkAttach.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 artwork</button>
|
|
</form>
|
|
) : null}
|
|
|
|
{props.attachContributorUrl ? (
|
|
<form onSubmit={(event) => { event.preventDefault(); contributorForm.post(props.attachContributorUrl, { preserveScroll: true }) }} className="rounded-[28px] border border-white/10 bg-white/[0.03] p-6">
|
|
<h2 className="text-lg font-semibold text-white">Contributor credit</h2>
|
|
<div className="mt-4 space-y-3">
|
|
<NovaSelect value={String(contributorForm.data.user_id || '')} onChange={(val) => contributorForm.setData('user_id', val)} placeholder="Choose contributor" options={(props.memberOptions || []).map((o) => ({ value: String(o.id), label: o.name || o.username }))} />
|
|
<input value={contributorForm.data.role_label} onChange={(event) => contributorForm.setData('role_label', event.target.value)} placeholder="Role label" className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
|
|
<button type="submit" className="rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-sm font-semibold text-white">Attach contributor</button>
|
|
</div>
|
|
{Array.isArray(release?.contributors) && release.contributors.length > 0 ? <div className="mt-6 space-y-3">{release.contributors.map((contributor) => <div key={contributor.id} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white"><div className="font-semibold">{contributor.name || contributor.username}</div><div className="mt-1 text-xs uppercase tracking-[0.16em] text-slate-400">{contributor.role_label || 'Contributor'}</div></div>)}</div> : null}
|
|
</form>
|
|
) : null}
|
|
|
|
{props.storeMilestoneUrl ? (
|
|
<form onSubmit={(event) => { event.preventDefault(); milestoneForm.post(props.storeMilestoneUrl, { preserveScroll: true, onSuccess: () => milestoneForm.reset('title', 'summary', 'due_date', 'owner_user_id', 'notes') }) }} className="rounded-[28px] border border-white/10 bg-white/[0.03] p-6">
|
|
<h2 className="text-lg font-semibold text-white">Milestones</h2>
|
|
<div className="mt-4 space-y-3">
|
|
<input value={milestoneForm.data.title} onChange={(event) => milestoneForm.setData('title', event.target.value)} placeholder="Milestone title" className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
|
|
<textarea value={milestoneForm.data.summary} onChange={(event) => milestoneForm.setData('summary', event.target.value)} placeholder="Summary" rows={3} className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
|
|
<div className="grid gap-3 md:grid-cols-2">
|
|
<NovaSelect value={milestoneForm.data.status} onChange={(val) => milestoneForm.setData('status', val)} searchable={false} options={['pending', 'active', 'blocked', 'completed', 'cancelled'].map((s) => ({ value: s, label: s }))} />
|
|
<DateTimePicker value={milestoneForm.data.due_date} onChange={(nextValue) => milestoneForm.setData('due_date', nextValue)} mode="date" placeholder="Due date" clearable className="bg-black/20" />
|
|
</div>
|
|
<NovaSelect value={String(milestoneForm.data.owner_user_id || '')} onChange={(val) => milestoneForm.setData('owner_user_id', val)} placeholder="No owner" options={(props.memberOptions || []).map((o) => ({ value: String(o.id), label: o.name || o.username }))} />
|
|
<textarea value={milestoneForm.data.notes} onChange={(event) => milestoneForm.setData('notes', event.target.value)} placeholder="Notes" rows={3} className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
|
|
<button type="submit" className="rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-sm font-semibold text-white">Add milestone</button>
|
|
</div>
|
|
{Array.isArray(release?.milestones) && release.milestones.length > 0 ? <div className="mt-6 space-y-3">{release.milestones.map((milestone) => <div key={milestone.id} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-4"><div className="flex items-center justify-between gap-3"><div><div className="font-semibold text-white">{milestone.title}</div><div className="mt-1 text-xs text-slate-500">{milestone.owner?.name || milestone.owner?.username || 'No owner'}{milestone.due_date ? ` • due ${milestone.due_date}` : ''}</div></div><button type="button" onClick={() => router.patch(props.updateMilestonePattern.replace('__MILESTONE__', String(milestone.id)), { title: milestone.title, summary: milestone.summary || '', status: milestone.status === 'completed' ? 'active' : 'completed', due_date: milestone.due_date || '', owner_user_id: milestone.owner?.id || '', notes: milestone.notes || '' }, { preserveScroll: true })} className="rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-xs font-semibold text-white">Mark {milestone.status === 'completed' ? 'active' : 'complete'}</button></div>{milestone.summary ? <p className="mt-2 text-sm text-slate-400">{milestone.summary}</p> : null}</div>)}</div> : null}
|
|
</form>
|
|
) : null}
|
|
</div>
|
|
</form>
|
|
</StudioLayout>
|
|
)
|
|
} |