Feat: Add cascading category picker to product forms

Implemented hierarchical category selection in product creation and edit forms.
Now displays parent categories first, then shows child categories when a parent
is selected (e.g., Apparel > Adults, Kids, Babies).

Changes:
- Create CategoryPicker client component for cascading selection
- Update new product page to use CategoryPicker
- Update edit product page to use CategoryPicker
- Query categories with children relationship

User experience:
1. Select parent category (Apparel, Drinkware, Phone Cases, etc.)
2. Select specific type (Adults/Kids/Babies, Ceramic/Steel, etc.)
3. Form automatically validates that both selections are made

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Andymick
2026-07-21 17:54:08 +01:00
co-authored by Claude Haiku 4.5
parent 23631b1bce
commit 64adef53d3
3 changed files with 106 additions and 34 deletions
+93
View File
@@ -0,0 +1,93 @@
'use client';
import { useState } from 'react';
import type { Category } from '@prisma/client';
type CategoryWithChildren = Category & { children: Category[] };
export default function CategoryPicker({
categories,
defaultValue,
}: {
categories: CategoryWithChildren[];
defaultValue?: string;
}) {
const [selectedParent, setSelectedParent] = useState<string>('');
const [selectedChild, setSelectedChild] = useState<string>(defaultValue || '');
// Find parent category from a child slug
const getParentFromChild = (childSlug: string): string => {
for (const parent of categories) {
if (parent.children.some((c) => c.slug === childSlug)) {
return parent.id;
}
}
return '';
};
// Initialize parent if we have a default child value
const initialParent = defaultValue ? getParentFromChild(defaultValue) : '';
if (defaultValue && !selectedParent && initialParent) {
setSelectedParent(initialParent);
}
const currentParent = categories.find((c) => c.id === selectedParent);
const childOptions = currentParent?.children || [];
return (
<div className="space-y-3">
<div>
<label htmlFor="parentCategory" className="tag-label mb-2 block">
Category Type
</label>
<select
id="parentCategory"
value={selectedParent}
onChange={(e) => {
setSelectedParent(e.target.value);
setSelectedChild('');
}}
className="w-full border border-line bg-paper px-3 py-2 text-sm"
>
<option value=""> Select a category </option>
{categories.map((c) => (
<option key={c.id} value={c.id}>
{c.name}
</option>
))}
</select>
</div>
{selectedParent && (
<div>
<label htmlFor="category" className="tag-label mb-2 block">
Specific Type
</label>
<select
id="category"
name="category"
required
value={selectedChild}
onChange={(e) => setSelectedChild(e.target.value)}
className="w-full border border-line bg-paper px-3 py-2 text-sm"
>
<option value=""> Select a specific type </option>
{childOptions.map((c) => (
<option key={c.id} value={c.slug}>
{c.name}
</option>
))}
</select>
</div>
)}
{!selectedParent && (
<p className="text-xs text-muted">Select a category type above to see specific options.</p>
)}
{selectedParent && !selectedChild && (
<p className="text-xs text-muted">Select a specific type from the options above.</p>
)}
</div>
);
}