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:
parent
6705293e50
commit
84ab6bef2d
@ -725,6 +725,7 @@ function ItemEditor({
|
||||
const ov = item._override
|
||||
|
||||
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 */}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -4,6 +4,7 @@ import { atomicWriteJSON } from './file-utils'
|
||||
|
||||
export interface ItemOverride {
|
||||
hidden?: boolean
|
||||
featured?: boolean
|
||||
categoryOverride?: string
|
||||
categoryLabelOverride?: string
|
||||
sortOrder?: number
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user