Allow artworks to have one primary category and extra secondaries.
Keep artwork_category as membership, store a deterministic primary_category_id, and let upload plus studio pick up to five additional same-type categories without changing canonical URLs.
This commit is contained in:
@@ -3,6 +3,7 @@ import { usePage } from '@inertiajs/react'
|
||||
import SeoHead from '../../components/seo/SeoHead'
|
||||
import TagInput from '../../components/tags/TagInput'
|
||||
import UploadWizard from '../../components/upload/UploadWizard'
|
||||
import UploadReviewNotice from '../../components/upload/UploadReviewNotice'
|
||||
import UploadDescriptionEditor from '../../components/upload/UploadDescriptionEditor'
|
||||
import Checkbox from '../../Components/ui/Checkbox'
|
||||
import { mapUploadErrorNotice, mapUploadResultNotice } from '../../lib/uploadNotices'
|
||||
@@ -52,6 +53,7 @@ const initialState = {
|
||||
title: '',
|
||||
type: '',
|
||||
category: '',
|
||||
additionalCategories: [],
|
||||
tags: '',
|
||||
description: '',
|
||||
isMature: false,
|
||||
@@ -320,6 +322,8 @@ function useUploadMachine({ draftId = null, filesCdnUrl = '', chunkSize, chunkRe
|
||||
const res = await window.axios.post('/api/artworks', {
|
||||
title: state.metadata.title,
|
||||
category: state.metadata.category,
|
||||
primary_category_id: state.metadata.category ? Number(state.metadata.category) : null,
|
||||
secondary_category_ids: (state.metadata.additionalCategories || []).map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0),
|
||||
tags: state.metadata.tags,
|
||||
description: state.metadata.description,
|
||||
is_mature: state.metadata.isMature,
|
||||
@@ -671,6 +675,8 @@ export default function UploadPage({ draftId = null, filesCdnUrl = '', chunkSize
|
||||
contributorOptionsByGroup={props?.contributor_options_by_group && typeof props.contributor_options_by_group === 'object' ? props.contributor_options_by_group : {}}
|
||||
initialGroupSlug={props?.initial_group || ''}
|
||||
currentUserId={props?.auth?.user?.id ?? null}
|
||||
maxSecondaryCategories={Number(props?.max_secondary_categories) > 0 ? Number(props.max_secondary_categories) : 5}
|
||||
requiresUploadReview={Boolean(props?.requires_upload_review)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -841,6 +847,7 @@ export default function UploadPage({ draftId = null, filesCdnUrl = '', chunkSize
|
||||
<div className="grid gap-8 lg:grid-cols-[1.1fr,0.9fr]">
|
||||
<div className="space-y-6">
|
||||
<SeoHead seo={{ title: pageTitle, description: pageDescription, robots: 'noindex, nofollow' }} />
|
||||
<UploadReviewNotice visible={Boolean(props?.requires_upload_review)} />
|
||||
<div className="rounded-2xl border border-white/10 bg-white/5 p-6 shadow-[0_0_50px_rgba(59,130,246,0.15)]">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-3xl font-semibold tracking-tight">Upload artwork</h1>
|
||||
@@ -911,7 +918,7 @@ export default function UploadPage({ draftId = null, filesCdnUrl = '', chunkSize
|
||||
<button
|
||||
key={ct.id}
|
||||
type="button"
|
||||
onClick={() => dispatch({ type: 'SET_METADATA', payload: { type: String(ct.id), category: '' } })}
|
||||
onClick={() => dispatch({ type: 'SET_METADATA', payload: { type: String(ct.id), category: '', additionalCategories: [] } })}
|
||||
className={`group rounded-2xl border px-4 py-3 text-left transition ${active
|
||||
? 'border-emerald-400/60 bg-emerald-500/10 shadow-[0_0_25px_rgba(16,185,129,0.25)]'
|
||||
: 'border-white/10 bg-white/5 hover:border-sky-400/40 hover:bg-white/10'}`}
|
||||
@@ -981,11 +988,11 @@ export default function UploadPage({ draftId = null, filesCdnUrl = '', chunkSize
|
||||
onClick={() => {
|
||||
if (hasChildren) {
|
||||
setSelectedParentCategory(String(cat.id))
|
||||
dispatch({ type: 'SET_METADATA', payload: { category: '' } })
|
||||
dispatch({ type: 'SET_METADATA', payload: { category: '', additionalCategories: state.metadata.additionalCategories || [] } })
|
||||
return
|
||||
}
|
||||
setSelectedParentCategory(null)
|
||||
dispatch({ type: 'SET_METADATA', payload: { category: String(cat.id) } })
|
||||
dispatch({ type: 'SET_METADATA', payload: { category: String(cat.id), additionalCategories: (state.metadata.additionalCategories || []).filter((id) => String(id) !== String(cat.id)) } })
|
||||
}}
|
||||
className={`rounded-full border px-4 py-2 text-sm transition ${isSelected || isExpanded
|
||||
? 'border-purple-300/60 bg-purple-400/20 text-purple-100'
|
||||
@@ -1019,7 +1026,7 @@ export default function UploadPage({ draftId = null, filesCdnUrl = '', chunkSize
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// Select child but keep parent expanded so the panel remains visible
|
||||
dispatch({ type: 'SET_METADATA', payload: { category: String(child.id) } })
|
||||
dispatch({ type: 'SET_METADATA', payload: { category: String(child.id), additionalCategories: (state.metadata.additionalCategories || []).filter((id) => String(id) !== String(child.id)) } })
|
||||
}}
|
||||
className={`rounded-full border px-4 py-2 text-sm transition ${activeChild
|
||||
? 'border-purple-300/60 bg-purple-400/20 text-purple-100'
|
||||
@@ -1038,6 +1045,43 @@ export default function UploadPage({ draftId = null, filesCdnUrl = '', chunkSize
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedType && state.metadata.category ? (
|
||||
<div className="mt-4 rounded-2xl border border-white/10 bg-white/5 p-4 sm:p-5">
|
||||
<div className="text-sm font-semibold text-white">Additional categories</div>
|
||||
<div className="mt-1 text-xs text-white/60">Optional. Add up to 5 other relevant categories from the same type. They will not change the artwork URL or breadcrumb.</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{categoryOptions.flatMap((cat) => [cat, ...(Array.isArray(cat.children) ? cat.children : [])])
|
||||
.filter((cat, index, all) => all.findIndex((item) => String(item.id) === String(cat.id)) === index)
|
||||
.filter((cat) => String(cat.id) !== String(state.metadata.category))
|
||||
.sort((a, b) => String(a.name || '').localeCompare(String(b.name || ''), undefined, { numeric: true, sensitivity: 'base' }))
|
||||
.map((cat) => {
|
||||
const selected = (state.metadata.additionalCategories || []).map(String).includes(String(cat.id))
|
||||
const atLimit = !selected && (state.metadata.additionalCategories || []).length >= 5
|
||||
return (
|
||||
<button
|
||||
key={`extra-${cat.id}`}
|
||||
type="button"
|
||||
disabled={atLimit}
|
||||
onClick={() => {
|
||||
const current = (state.metadata.additionalCategories || []).map(String)
|
||||
const next = selected
|
||||
? current.filter((id) => id !== String(cat.id))
|
||||
: [...current, String(cat.id)]
|
||||
dispatch({ type: 'SET_METADATA', payload: { additionalCategories: next } })
|
||||
}}
|
||||
className={`rounded-full border px-4 py-2 text-sm transition ${selected
|
||||
? 'border-sky-300/60 bg-sky-400/20 text-sky-100'
|
||||
: 'border-white/10 bg-white/5 text-white/70 hover:border-sky-300/50 hover:bg-sky-400/10 disabled:opacity-40'}`}
|
||||
aria-pressed={selected}
|
||||
>
|
||||
{cat.name}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-4">
|
||||
<span className="text-sm text-white/80">Tags</span>
|
||||
<div className="mt-2">
|
||||
|
||||
Reference in New Issue
Block a user