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
@ -724,7 +724,8 @@ function ItemEditor({
|
|||||||
}) {
|
}) {
|
||||||
const ov = item._override
|
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 [catOverride, setCatOverride] = useState(ov.categoryOverride ?? '')
|
||||||
const [catLabel, setCatLabel] = useState(ov.categoryLabelOverride ?? '')
|
const [catLabel, setCatLabel] = useState(ov.categoryLabelOverride ?? '')
|
||||||
const [sortOrder, setSortOrder] = useState(String(ov.sortOrder ?? ''))
|
const [sortOrder, setSortOrder] = useState(String(ov.sortOrder ?? ''))
|
||||||
@ -780,6 +781,7 @@ function ItemEditor({
|
|||||||
setError('')
|
setError('')
|
||||||
const patch: Partial<ItemOverride> = {
|
const patch: Partial<ItemOverride> = {
|
||||||
hidden,
|
hidden,
|
||||||
|
featured,
|
||||||
hiddenVariationIds: hiddenVars,
|
hiddenVariationIds: hiddenVars,
|
||||||
hiddenModifierIds: hiddenMods,
|
hiddenModifierIds: hiddenMods,
|
||||||
}
|
}
|
||||||
@ -816,6 +818,7 @@ function ItemEditor({
|
|||||||
const res = await fetch(`${BASE}/api/admin/items/${item.id}`, { method: 'DELETE' })
|
const res = await fetch(`${BASE}/api/admin/items/${item.id}`, { method: 'DELETE' })
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setHidden(false)
|
setHidden(false)
|
||||||
|
setFeatured(item.featured ?? false)
|
||||||
setCatOverride('')
|
setCatOverride('')
|
||||||
setCatLabel('')
|
setCatLabel('')
|
||||||
setSortOrder('')
|
setSortOrder('')
|
||||||
@ -864,8 +867,8 @@ function ItemEditor({
|
|||||||
{/* Left column */}
|
{/* Left column */}
|
||||||
<div className="column is-half">
|
<div className="column is-half">
|
||||||
|
|
||||||
{/* Hidden toggle */}
|
{/* Visibility toggles */}
|
||||||
<div className="field">
|
<div className="field" style={{ display: 'flex', gap: '1.5rem', flexWrap: 'wrap' }}>
|
||||||
<label className="checkbox" style={{ fontWeight: 600 }}>
|
<label className="checkbox" style={{ fontWeight: 600 }}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@ -875,6 +878,15 @@ function ItemEditor({
|
|||||||
/>
|
/>
|
||||||
Hidden from storefront
|
Hidden from storefront
|
||||||
</label>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Category */}
|
{/* Category */}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ function applyOverrides(items: CatalogItem[]): CatalogItem[] {
|
|||||||
if (!ov) return item
|
if (!ov) return item
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
|
featured: ov.featured ?? item.featured,
|
||||||
category: ov.categoryOverride ?? item.category,
|
category: ov.categoryOverride ?? item.category,
|
||||||
categoryLabel: ov.categoryLabelOverride ?? item.categoryLabel,
|
categoryLabel: ov.categoryLabelOverride ?? item.categoryLabel,
|
||||||
showColors: ov.showColors != null ? ov.showColors : item.showColors,
|
showColors: ov.showColors != null ? ov.showColors : item.showColors,
|
||||||
@ -33,6 +34,8 @@ function applyOverrides(items: CatalogItem[]): CatalogItem[] {
|
|||||||
})
|
})
|
||||||
.filter((item) => !(overrides[item.id]?.hidden))
|
.filter((item) => !(overrides[item.id]?.hidden))
|
||||||
.sort((a, b) => {
|
.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 aOrder = overrides[a.id]?.sortOrder ?? 0
|
||||||
const bOrder = overrides[b.id]?.sortOrder ?? 0
|
const bOrder = overrides[b.id]?.sortOrder ?? 0
|
||||||
return aOrder - bOrder
|
return aOrder - bOrder
|
||||||
|
|||||||
@ -55,6 +55,15 @@ export default function ProductCard({ item }: Props) {
|
|||||||
) : (
|
) : (
|
||||||
<div className="no-image">🎈</div>
|
<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 && (
|
{soldOut && (
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'absolute', inset: 0,
|
position: 'absolute', inset: 0,
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { atomicWriteJSON } from './file-utils'
|
|||||||
|
|
||||||
export interface ItemOverride {
|
export interface ItemOverride {
|
||||||
hidden?: boolean
|
hidden?: boolean
|
||||||
|
featured?: boolean
|
||||||
categoryOverride?: string
|
categoryOverride?: string
|
||||||
categoryLabelOverride?: string
|
categoryLabelOverride?: string
|
||||||
sortOrder?: number
|
sortOrder?: number
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user