feat: featured items — admin toggle, badge, sorted to top

- Add featured to ItemOverride so it can be set per-item in admin
- Catalog API applies the override and sorts featured items before
  non-featured (within each group, sortOrder still applies)
- ProductCard shows a teal Featured badge on the image when featured
  and not sold out
- Admin item editor has a  Featured checkbox beside Hidden

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chris 2026-04-17 14:21:33 -04:00
parent 6705293e50
commit 84ab6bef2d
4 changed files with 28 additions and 3 deletions

View File

@ -724,7 +724,8 @@ function ItemEditor({
}) {
const ov = item._override
const [hidden, setHidden] = useState(ov.hidden ?? false)
const [hidden, setHidden] = useState(ov.hidden ?? false)
const [featured, setFeatured] = useState(ov.featured ?? item.featured ?? false)
const [catOverride, setCatOverride] = useState(ov.categoryOverride ?? '')
const [catLabel, setCatLabel] = useState(ov.categoryLabelOverride ?? '')
const [sortOrder, setSortOrder] = useState(String(ov.sortOrder ?? ''))
@ -780,6 +781,7 @@ function ItemEditor({
setError('')
const patch: Partial<ItemOverride> = {
hidden,
featured,
hiddenVariationIds: hiddenVars,
hiddenModifierIds: hiddenMods,
}
@ -816,6 +818,7 @@ function ItemEditor({
const res = await fetch(`${BASE}/api/admin/items/${item.id}`, { method: 'DELETE' })
if (res.ok) {
setHidden(false)
setFeatured(item.featured ?? false)
setCatOverride('')
setCatLabel('')
setSortOrder('')
@ -864,8 +867,8 @@ function ItemEditor({
{/* Left column */}
<div className="column is-half">
{/* Hidden toggle */}
<div className="field">
{/* Visibility toggles */}
<div className="field" style={{ display: 'flex', gap: '1.5rem', flexWrap: 'wrap' }}>
<label className="checkbox" style={{ fontWeight: 600 }}>
<input
type="checkbox"
@ -875,6 +878,15 @@ function ItemEditor({
/>
Hidden from storefront
</label>
<label className="checkbox" style={{ fontWeight: 600, color: '#11b3be' }}>
<input
type="checkbox"
checked={featured}
onChange={(e) => setFeatured(e.target.checked)}
style={{ marginRight: 6, accentColor: '#11b3be' }}
/>
Featured
</label>
</div>
{/* Category */}

View File

@ -12,6 +12,7 @@ function applyOverrides(items: CatalogItem[]): CatalogItem[] {
if (!ov) return item
return {
...item,
featured: ov.featured ?? item.featured,
category: ov.categoryOverride ?? item.category,
categoryLabel: ov.categoryLabelOverride ?? item.categoryLabel,
showColors: ov.showColors != null ? ov.showColors : item.showColors,
@ -33,6 +34,8 @@ function applyOverrides(items: CatalogItem[]): CatalogItem[] {
})
.filter((item) => !(overrides[item.id]?.hidden))
.sort((a, b) => {
const featDiff = (b.featured ? 1 : 0) - (a.featured ? 1 : 0)
if (featDiff !== 0) return featDiff
const aOrder = overrides[a.id]?.sortOrder ?? 0
const bOrder = overrides[b.id]?.sortOrder ?? 0
return aOrder - bOrder

View File

@ -55,6 +55,15 @@ export default function ProductCard({ item }: Props) {
) : (
<div className="no-image">🎈</div>
)}
{item.featured && !soldOut && (
<span style={{
position: 'absolute', top: 10, left: 10,
background: '#11b3be', color: '#fff',
fontWeight: 700, fontSize: '0.72rem', letterSpacing: '0.05em',
padding: '3px 10px', borderRadius: 4,
textTransform: 'uppercase',
}}>Featured</span>
)}
{soldOut && (
<div style={{
position: 'absolute', inset: 0,

View File

@ -4,6 +4,7 @@ import { atomicWriteJSON } from './file-utils'
export interface ItemOverride {
hidden?: boolean
featured?: boolean
categoryOverride?: string
categoryLabelOverride?: string
sortOrder?: number