Files
SkinbaseNova/resources/js/components/upload/CategorySelector.jsx

108 lines
4.3 KiB
JavaScript

import React from 'react'
/**
* CategorySelector
*
* Reusable pill-based category + subcategory selector.
* Renders root categories as pills; when a root with children is selected,
* subcategory pills appear in an animated block below.
*
* @param {object} props
* @param {Array} props.categories Flat list of root-category objects { id, name, children[] }
* @param {string} props.rootCategoryId Currently selected root id
* @param {string} props.subCategoryId Currently selected sub id
* @param {boolean} props.hasContentType Whether a content type is selected (gate)
* @param {string} [props.error] Validation error message
* @param {function} props.onRootChange Called with (rootId: string)
* @param {function} props.onSubChange Called with (subId: string)
* @param {Array} [props.allRoots] All root options (for cross-type fallback)
* @param {function} [props.onRootChangeAll] Fallback handler with full cross-type info
*/
export default function CategorySelector({
categories = [],
rootCategoryId = '',
subCategoryId = '',
hasContentType = false,
error = '',
onRootChange,
onSubChange,
allRoots = [],
onRootChangeAll,
}) {
const rootOptions = hasContentType ? categories : allRoots
const selectedRoot = categories.find((c) => String(c.id) === String(rootCategoryId || '')) ?? null
const hasSubcategories = Boolean(
selectedRoot && Array.isArray(selectedRoot.children) && selectedRoot.children.length > 0
)
return (
<div className="space-y-3">
{!hasContentType ? (
<div className="rounded-lg bg-white/[0.025] px-3 py-3 text-sm text-white/45 ring-1 ring-white/8">
Select a content type to load categories.
</div>
) : categories.length === 0 ? (
<div className="rounded-lg bg-white/[0.025] px-3 py-3 text-sm text-white/45 ring-1 ring-white/8">
No categories available for this content type.
</div>
) : (
<div className="flex flex-wrap gap-2" role="group" aria-label="Category">
{categories.map((root) => {
const active = String(root.id) === String(rootCategoryId || '')
return (
<button
key={root.id}
type="button"
aria-pressed={active}
onClick={() => onRootChange?.(String(root.id))}
className={[
'rounded-full border px-3.5 py-1.5 text-sm transition-all',
active
? 'border-violet-500/70 bg-violet-600/25 text-white shadow-sm'
: 'border-white/10 bg-white/5 text-white/65 hover:border-violet-300/40 hover:bg-violet-400/10 hover:text-white/90',
].join(' ')}
>
{root.name}
</button>
)
})}
</div>
)}
{/* Subcategories (shown when root has children) */}
{hasSubcategories && (
<div className="rounded-xl ring-1 ring-white/8 bg-white/[0.025] p-3">
<p className="mb-2 text-[11px] uppercase tracking-wide text-white/45">
Subcategory for <span className="text-white/70">{selectedRoot.name}</span>
</p>
<div className="flex flex-wrap gap-2" role="group" aria-label="Subcategory">
{selectedRoot.children.map((sub) => {
const active = String(sub.id) === String(subCategoryId || '')
return (
<button
key={sub.id}
type="button"
aria-pressed={active}
onClick={() => onSubChange?.(String(sub.id))}
className={[
'rounded-full border px-3 py-1 text-sm transition-all',
active
? 'border-cyan-500/70 bg-cyan-600/20 text-white shadow-sm'
: 'border-white/10 bg-white/5 text-white/60 hover:border-cyan-300/40 hover:bg-cyan-400/10 hover:text-white/85',
].join(' ')}
>
{sub.name}
</button>
)
})}
</div>
</div>
)}
{error && (
<p className="mt-1 text-xs text-red-300" role="alert">{error}</p>
)}
</div>
)
}