484 lines
24 KiB
JavaScript
484 lines
24 KiB
JavaScript
import React, { useEffect, useMemo, useState } from 'react'
|
|
import UploadSidebar from '../UploadSidebar'
|
|
import { getContentTypeValue, getContentTypeVisualKey } from '../../../lib/uploadUtils'
|
|
|
|
/**
|
|
* Step2Details
|
|
*
|
|
* Step 2 of the upload wizard: artwork metadata.
|
|
* Shows uploaded-asset summary, content type selector,
|
|
* category/subcategory selectors, tags, description, and rights.
|
|
*/
|
|
export default function Step2Details({
|
|
headingRef,
|
|
// Asset summary
|
|
primaryFile,
|
|
primaryPreviewUrl,
|
|
isArchive,
|
|
fileMetadata,
|
|
screenshots,
|
|
// Content type + category
|
|
contentTypes,
|
|
metadata,
|
|
metadataErrors,
|
|
filteredCategoryTree,
|
|
allRootCategoryOptions,
|
|
requiresSubCategory,
|
|
onContentTypeChange,
|
|
onRootCategoryChange,
|
|
onSubCategoryChange,
|
|
// Sidebar (title / tags / description / rights)
|
|
suggestedTags,
|
|
onChangeTitle,
|
|
onChangeTags,
|
|
onChangeDescription,
|
|
onToggleMature,
|
|
onToggleRights,
|
|
}) {
|
|
const [isContentTypeChooserOpen, setIsContentTypeChooserOpen] = useState(() => !metadata.contentType)
|
|
const [isCategoryChooserOpen, setIsCategoryChooserOpen] = useState(() => !metadata.rootCategoryId)
|
|
const [isSubCategoryChooserOpen, setIsSubCategoryChooserOpen] = useState(() => !metadata.subCategoryId)
|
|
const [categorySearch, setCategorySearch] = useState('')
|
|
const [subCategorySearch, setSubCategorySearch] = useState('')
|
|
|
|
const contentTypeOptions = useMemo(
|
|
() => (Array.isArray(contentTypes) ? contentTypes : []).map((item) => {
|
|
const normalizedName = String(item?.name || '').trim().toLowerCase()
|
|
const normalizedSlug = String(item?.slug || '').trim().toLowerCase()
|
|
|
|
if (normalizedName === 'other' || normalizedSlug === 'other') {
|
|
return {
|
|
...item,
|
|
name: 'Others',
|
|
}
|
|
}
|
|
|
|
return item
|
|
}),
|
|
[contentTypes]
|
|
)
|
|
|
|
const selectedContentType = useMemo(
|
|
() => contentTypeOptions.find((item) => String(getContentTypeValue(item)) === String(metadata.contentType || '')) ?? null,
|
|
[contentTypeOptions, metadata.contentType]
|
|
)
|
|
|
|
const selectedRoot = useMemo(
|
|
() => filteredCategoryTree.find((item) => String(item.id) === String(metadata.rootCategoryId || '')) ?? null,
|
|
[filteredCategoryTree, metadata.rootCategoryId]
|
|
)
|
|
|
|
const subCategories = selectedRoot?.children || []
|
|
const selectedSubCategory = useMemo(
|
|
() => subCategories.find((item) => String(item.id) === String(metadata.subCategoryId || '')) ?? null,
|
|
[subCategories, metadata.subCategoryId]
|
|
)
|
|
|
|
const sortedFilteredCategories = useMemo(() => {
|
|
const sorted = [...filteredCategoryTree].sort((a, b) => a.name.localeCompare(b.name))
|
|
const q = categorySearch.trim().toLowerCase()
|
|
return q ? sorted.filter((c) => c.name.toLowerCase().includes(q)) : sorted
|
|
}, [filteredCategoryTree, categorySearch])
|
|
|
|
const sortedFilteredSubCategories = useMemo(() => {
|
|
const sorted = [...subCategories].sort((a, b) => a.name.localeCompare(b.name))
|
|
const q = subCategorySearch.trim().toLowerCase()
|
|
return q ? sorted.filter((s) => s.name.toLowerCase().includes(q)) : sorted
|
|
}, [subCategories, subCategorySearch])
|
|
|
|
useEffect(() => {
|
|
if (!metadata.contentType) {
|
|
setIsContentTypeChooserOpen(true)
|
|
}
|
|
}, [metadata.contentType])
|
|
|
|
useEffect(() => {
|
|
if (!metadata.rootCategoryId) {
|
|
setIsCategoryChooserOpen(true)
|
|
}
|
|
}, [metadata.rootCategoryId])
|
|
|
|
useEffect(() => {
|
|
if (!metadata.subCategoryId) {
|
|
setIsSubCategoryChooserOpen(true)
|
|
}
|
|
}, [metadata.subCategoryId])
|
|
|
|
return (
|
|
<div className="space-y-5 rounded-[28px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))] p-6 shadow-[0_24px_90px_rgba(2,8,23,0.28)] backdrop-blur sm:p-7">
|
|
{/* Step header */}
|
|
<div className="rounded-2xl border border-white/8 bg-white/[0.04] px-5 py-4">
|
|
<h2
|
|
ref={headingRef}
|
|
tabIndex={-1}
|
|
className="text-lg font-semibold text-white focus:outline-none"
|
|
>
|
|
Artwork details
|
|
</h2>
|
|
<p className="mt-1 text-sm text-white/60">
|
|
Complete required metadata and rights confirmation before publishing.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Uploaded asset summary */}
|
|
<div className="rounded-2xl ring-1 ring-white/8 bg-white/[0.025] p-5">
|
|
<p className="mb-3 text-[11px] uppercase tracking-wide text-white/45">Uploaded asset</p>
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
|
{/* Thumbnail / Archive icon */}
|
|
{primaryPreviewUrl && !isArchive ? (
|
|
<div className="flex h-[120px] w-[120px] items-center justify-center overflow-hidden rounded-xl ring-1 ring-white/10 bg-black/30 shrink-0">
|
|
<img
|
|
src={primaryPreviewUrl}
|
|
alt="Uploaded artwork thumbnail"
|
|
className="max-h-full max-w-full object-contain"
|
|
loading="lazy"
|
|
decoding="async"
|
|
width={120}
|
|
height={120}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="grid h-[120px] w-[120px] place-items-center rounded-xl ring-1 ring-white/10 bg-white/5 shrink-0">
|
|
<svg className="h-8 w-8 text-white/30" 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>
|
|
)}
|
|
|
|
{/* File metadata */}
|
|
<div className="min-w-0 space-y-1">
|
|
<p className="truncate text-sm font-medium text-white">
|
|
{primaryFile?.name || 'Primary file'}
|
|
</p>
|
|
<p className="text-xs text-white/50">
|
|
{isArchive
|
|
? `Archive · ${screenshots.length} screenshot${screenshots.length !== 1 ? 's' : ''}`
|
|
: fileMetadata.resolution !== '—'
|
|
? `${fileMetadata.resolution} · ${fileMetadata.size}`
|
|
: fileMetadata.size}
|
|
</p>
|
|
<span className={`inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] ${isArchive ? 'border-amber-400/40 bg-amber-400/10 text-amber-200' : 'border-sky-400/40 bg-sky-400/10 text-sky-200'}`}>
|
|
{isArchive ? 'Archive' : 'Image'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<section className="rounded-2xl border border-white/10 bg-[radial-gradient(circle_at_top_left,_rgba(14,165,233,0.08),_rgba(15,23,36,0.92)_52%)] p-5 sm:p-6">
|
|
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
|
<div>
|
|
<h3 className="text-sm font-semibold text-white">Content type</h3>
|
|
<p className="mt-1 text-xs text-white/55">Choose the main content family first.</p>
|
|
</div>
|
|
<span className="rounded-full border border-sky-400/35 bg-sky-400/10 px-2.5 py-0.5 text-[11px] uppercase tracking-widest text-sky-300">
|
|
Step 2a
|
|
</span>
|
|
</div>
|
|
|
|
{contentTypeOptions.length === 0 && (
|
|
<div className="rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-4 text-sm text-slate-400">
|
|
No content types are available right now.
|
|
</div>
|
|
)}
|
|
|
|
{selectedContentType && !isContentTypeChooserOpen && (
|
|
<div className="rounded-2xl border border-emerald-400/25 bg-emerald-400/[0.08] p-4">
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<div>
|
|
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-emerald-200/80">Selected content type</div>
|
|
<div className="mt-1 text-lg font-semibold text-white">{selectedContentType.name}</div>
|
|
<div className="mt-1 text-sm text-slate-400">
|
|
{filteredCategoryTree.length > 0
|
|
? `Continue by choosing one of the ${filteredCategoryTree.length} matching categories below.`
|
|
: 'This content type does not have categories yet.'}
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsContentTypeChooserOpen(true)}
|
|
className="inline-flex items-center justify-center rounded-xl border border-white/10 bg-white/[0.05] px-3 py-2 text-sm font-medium text-slate-200 transition hover:border-white/20 hover:bg-white/[0.08] hover:text-white"
|
|
>
|
|
Change
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{(!selectedContentType || isContentTypeChooserOpen) && (
|
|
<div className="grid gap-3 lg:grid-cols-2">
|
|
{contentTypeOptions.map((ct) => {
|
|
const typeValue = String(getContentTypeValue(ct))
|
|
const isActive = typeValue === String(metadata.contentType || '')
|
|
const visualKey = getContentTypeVisualKey(ct)
|
|
const categoryCount = Array.isArray(ct.categories) ? ct.categories.length : 0
|
|
|
|
return (
|
|
<button
|
|
key={typeValue || ct.name}
|
|
type="button"
|
|
onClick={() => {
|
|
setIsContentTypeChooserOpen(false)
|
|
setIsCategoryChooserOpen(true)
|
|
onContentTypeChange(typeValue)
|
|
}}
|
|
className={[
|
|
'group flex w-full items-center gap-3 rounded-2xl border px-3 py-3 text-left transition-all',
|
|
isActive
|
|
? 'border-emerald-400/40 bg-emerald-400/10 shadow-[0_0_0_1px_rgba(52,211,153,0.18)]'
|
|
: 'border-white/10 bg-white/[0.03] hover:border-white/20 hover:bg-white/[0.06]',
|
|
].join(' ')}
|
|
aria-pressed={isActive}
|
|
>
|
|
<div className={`flex h-12 w-12 items-center justify-center rounded-2xl border ${isActive ? 'border-emerald-400/30 bg-emerald-400/10' : 'border-white/10 bg-white/[0.04]'}`}>
|
|
<img
|
|
src={`/gfx/mascot_${visualKey}.webp`}
|
|
alt=""
|
|
className="h-8 w-8 object-contain"
|
|
onError={(e) => { e.currentTarget.style.display = 'none' }}
|
|
/>
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<div className={`text-sm font-semibold ${isActive ? 'text-emerald-200' : 'text-white'}`}>{ct.name}</div>
|
|
<div className="mt-1 text-[11px] text-slate-500">{categoryCount} {categoryCount === 1 ? 'category' : 'categories'}</div>
|
|
</div>
|
|
<div className={`text-xs ${isActive ? 'text-emerald-300' : 'text-slate-500 group-hover:text-slate-300'}`}>
|
|
{isActive ? 'Selected' : 'Open'}
|
|
</div>
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{metadataErrors.contentType && <p className="mt-3 text-xs text-red-300">{metadataErrors.contentType}</p>}
|
|
</section>
|
|
|
|
<section className="rounded-2xl border border-white/10 bg-[radial-gradient(circle_at_top_left,_rgba(168,85,247,0.08),_rgba(15,23,36,0.88)_55%)] p-5 sm:p-6">
|
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
|
<div>
|
|
<h4 className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">Category path</h4>
|
|
<p className="mt-1 text-sm text-slate-400">Choose the main branch first, then refine with a subcategory when needed.</p>
|
|
</div>
|
|
<span className="rounded-full border border-violet-400/35 bg-violet-400/10 px-2.5 py-0.5 text-[11px] uppercase tracking-widest text-violet-300">
|
|
Step 2b
|
|
</span>
|
|
</div>
|
|
|
|
{!selectedContentType && (
|
|
<div className="mt-5 rounded-2xl border border-dashed border-white/12 bg-white/[0.02] px-5 py-10 text-center">
|
|
<div className="text-sm font-medium text-white">Select a content type first</div>
|
|
<p className="mt-2 text-sm text-slate-500">Once you choose the content type, the matching category tree will appear here.</p>
|
|
</div>
|
|
)}
|
|
|
|
{selectedContentType && (
|
|
<div className="mt-5 space-y-5">
|
|
<div className="flex items-center gap-2 text-sm text-slate-400">
|
|
<span className="rounded-full border border-emerald-400/20 bg-emerald-400/10 px-2.5 py-1 text-emerald-200">{selectedContentType.name}</span>
|
|
<span>contains {filteredCategoryTree.length} top-level {filteredCategoryTree.length === 1 ? 'category' : 'categories'}</span>
|
|
</div>
|
|
|
|
{selectedRoot && !isCategoryChooserOpen && (
|
|
<div className="rounded-2xl border border-purple-400/25 bg-purple-400/[0.08] p-4">
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<div>
|
|
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-purple-200/80">Selected category</div>
|
|
<div className="mt-1 text-lg font-semibold text-white">{selectedRoot.name}</div>
|
|
<div className="mt-1 text-sm text-slate-400">
|
|
{subCategories.length > 0
|
|
? `Next step: choose one of the ${subCategories.length} subcategories below.`
|
|
: 'This category is complete. No subcategory is required.'}
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => { setCategorySearch(''); setIsCategoryChooserOpen(true) }}
|
|
className="inline-flex items-center justify-center rounded-xl border border-white/10 bg-white/[0.05] px-3 py-2 text-sm font-medium text-slate-200 transition hover:border-white/20 hover:bg-white/[0.08] hover:text-white"
|
|
>
|
|
Change
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{(!selectedRoot || isCategoryChooserOpen) && (
|
|
<div className="space-y-3">
|
|
<div className="relative">
|
|
<svg className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-500" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fillRule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clipRule="evenodd" /></svg>
|
|
<input
|
|
type="search"
|
|
value={categorySearch}
|
|
onChange={(e) => setCategorySearch(e.target.value)}
|
|
placeholder="Search categories…"
|
|
className="w-full rounded-xl border border-white/10 bg-white/[0.04] py-2.5 pl-9 pr-4 text-sm text-white placeholder:text-slate-500 focus:border-purple-400/40 focus:outline-none focus:ring-1 focus:ring-purple-400/30"
|
|
/>
|
|
</div>
|
|
{sortedFilteredCategories.length === 0 && (
|
|
<p className="py-4 text-center text-sm text-slate-500">No categories match “{categorySearch}”</p>
|
|
)}
|
|
<div className="grid gap-3 lg:grid-cols-2">
|
|
{sortedFilteredCategories.map((cat) => {
|
|
const isActive = String(metadata.rootCategoryId || '') === String(cat.id)
|
|
const childCount = cat.children?.length || 0
|
|
|
|
return (
|
|
<button
|
|
key={cat.id}
|
|
type="button"
|
|
onClick={() => {
|
|
setIsCategoryChooserOpen(false)
|
|
onRootCategoryChange(String(cat.id))
|
|
}}
|
|
className={[
|
|
'rounded-2xl border px-4 py-4 text-left transition-all',
|
|
isActive
|
|
? 'border-purple-400/40 bg-purple-400/12 shadow-[0_0_0_1px_rgba(192,132,252,0.15)]'
|
|
: 'border-white/10 bg-white/[0.03] hover:border-white/20 hover:bg-white/[0.05]',
|
|
].join(' ')}
|
|
>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div>
|
|
<div className={`text-sm font-semibold ${isActive ? 'text-purple-200' : 'text-white'}`}>{cat.name}</div>
|
|
<div className="mt-1 text-[11px] text-slate-500">{childCount > 0 ? `${childCount} subcategories available` : 'Standalone category'}</div>
|
|
</div>
|
|
<span className={`rounded-full px-2 py-1 text-[11px] ${isActive ? 'bg-purple-300/15 text-purple-200' : 'bg-white/[0.05] text-slate-500'}`}>
|
|
{isActive ? 'Selected' : 'Choose'}
|
|
</span>
|
|
</div>
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{selectedRoot && subCategories.length > 0 && (
|
|
<div className="rounded-2xl border border-cyan-400/15 bg-cyan-400/[0.05] p-4">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div>
|
|
<h5 className="text-[11px] font-semibold uppercase tracking-[0.18em] text-cyan-200/80">Subcategories</h5>
|
|
<p className="mt-1 text-sm text-slate-400">Refine <span className="text-white">{selectedRoot.name}</span> with one more level.</p>
|
|
</div>
|
|
<span className="rounded-full border border-cyan-400/20 bg-cyan-400/10 px-2 py-1 text-[11px] text-cyan-200">{subCategories.length}</span>
|
|
</div>
|
|
|
|
{!metadata.subCategoryId && requiresSubCategory && (
|
|
<div className="mt-4 rounded-xl border border-amber-400/20 bg-amber-400/10 px-3 py-2 text-sm text-amber-100">
|
|
Subcategory still needs to be selected.
|
|
</div>
|
|
)}
|
|
|
|
{selectedSubCategory && !isSubCategoryChooserOpen && (
|
|
<div className="mt-4 rounded-2xl border border-cyan-400/25 bg-cyan-400/[0.09] p-4">
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<div>
|
|
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-cyan-200/80">Selected subcategory</div>
|
|
<div className="mt-1 text-lg font-semibold text-white">{selectedSubCategory.name}</div>
|
|
<div className="mt-1 text-sm text-slate-300">
|
|
Final category path: <span className="text-white">{selectedRoot.name}</span> / <span className="text-cyan-100">{selectedSubCategory.name}</span>
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => { setSubCategorySearch(''); setIsSubCategoryChooserOpen(true) }}
|
|
className="inline-flex items-center justify-center rounded-xl border border-white/10 bg-white/[0.05] px-3 py-2 text-sm font-medium text-slate-200 transition hover:border-white/20 hover:bg-white/[0.08] hover:text-white"
|
|
>
|
|
Change
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{(!selectedSubCategory || isSubCategoryChooserOpen) && (
|
|
<div className="mt-4 space-y-3">
|
|
<div className="relative">
|
|
<svg className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-500" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fillRule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clipRule="evenodd" /></svg>
|
|
<input
|
|
type="search"
|
|
value={subCategorySearch}
|
|
onChange={(e) => setSubCategorySearch(e.target.value)}
|
|
placeholder="Search subcategories…"
|
|
className="w-full rounded-xl border border-white/10 bg-white/[0.04] py-2.5 pl-9 pr-4 text-sm text-white placeholder:text-slate-500 focus:border-cyan-400/40 focus:outline-none focus:ring-1 focus:ring-cyan-400/30"
|
|
/>
|
|
</div>
|
|
{sortedFilteredSubCategories.length === 0 && (
|
|
<p className="py-4 text-center text-sm text-slate-500">No subcategories match “{subCategorySearch}”</p>
|
|
)}
|
|
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
|
{sortedFilteredSubCategories.map((sub) => {
|
|
const isActive = String(metadata.subCategoryId || '') === String(sub.id)
|
|
return (
|
|
<button
|
|
key={sub.id}
|
|
type="button"
|
|
onClick={() => {
|
|
setIsSubCategoryChooserOpen(false)
|
|
onSubCategoryChange(String(sub.id))
|
|
}}
|
|
className={[
|
|
'group rounded-2xl border px-4 py-3 text-left transition-all',
|
|
isActive
|
|
? 'border-cyan-400/40 bg-cyan-400/[0.13] shadow-[0_0_0_1px_rgba(34,211,238,0.14)]'
|
|
: 'border-white/10 bg-white/[0.04] hover:border-white/20 hover:bg-white/[0.06]',
|
|
].join(' ')}
|
|
>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<div className={[
|
|
'text-sm font-semibold transition-colors',
|
|
isActive ? 'text-cyan-100' : 'text-slate-100 group-hover:text-white',
|
|
].join(' ')}>
|
|
{sub.name}
|
|
</div>
|
|
<div className={[
|
|
'mt-1 text-xs',
|
|
isActive ? 'text-cyan-200/80' : 'text-slate-500 group-hover:text-slate-300',
|
|
].join(' ')}>
|
|
Subcategory option
|
|
</div>
|
|
</div>
|
|
<span className={[
|
|
'shrink-0 rounded-full px-2.5 py-1 text-[11px] font-medium',
|
|
isActive
|
|
? 'bg-cyan-300/15 text-cyan-100'
|
|
: 'bg-white/[0.05] text-slate-500 group-hover:text-slate-300',
|
|
].join(' ')}>
|
|
{isActive ? 'Selected' : 'Choose'}
|
|
</span>
|
|
</div>
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{selectedRoot && subCategories.length === 0 && (
|
|
<div className="rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-400">
|
|
<span className="font-medium text-white">{selectedRoot.name}</span> does not have subcategories. Selecting it is enough.
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{metadataErrors.category && <p className="mt-4 text-xs text-red-300">{metadataErrors.category}</p>}
|
|
</section>
|
|
|
|
{/* Title, tags, description, rights */}
|
|
<UploadSidebar
|
|
showHeader={false}
|
|
metadata={metadata}
|
|
suggestedTags={suggestedTags}
|
|
errors={metadataErrors}
|
|
onChangeTitle={onChangeTitle}
|
|
onChangeTags={onChangeTags}
|
|
onChangeDescription={onChangeDescription}
|
|
onToggleMature={onToggleMature}
|
|
onToggleRights={onToggleRights}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|