- 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>
126 lines
4.3 KiB
TypeScript
126 lines
4.3 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import type { CatalogItem } from '@/data/mock-catalog'
|
|
import ColorPicker from './ColorPicker'
|
|
import { fmt } from '@/lib/format'
|
|
|
|
interface Props {
|
|
item: CatalogItem
|
|
}
|
|
|
|
function maxColorsFor(name: string): number | null {
|
|
const n = name.toLowerCase()
|
|
if (/arch|column/.test(n)) return 4
|
|
if (/\b11["''″]|\b11[- ]?inch/.test(n)) return 1
|
|
if (/number.{0,10}sculpt|sculpt.{0,10}number/.test(n)) return 4
|
|
if (/ultimate/.test(n)) return 4
|
|
return null
|
|
}
|
|
|
|
/** Lowest stock count across tracked variations. null = nothing tracked. */
|
|
function lowestTrackedInventory(item: CatalogItem): number | null {
|
|
const tracked = item.variations.filter((v) => v.inventory !== null)
|
|
if (!tracked.length) return null
|
|
return Math.min(...tracked.map((v) => v.inventory as number))
|
|
}
|
|
|
|
const LOW_STOCK_THRESHOLD = 5
|
|
|
|
export default function ProductCard({ item }: Props) {
|
|
const [showPicker, setShowPicker] = useState(false)
|
|
// Prefer admin-set colorMax; fall back to name-based heuristic
|
|
const maxColors = item.showColors
|
|
? (item.colorMax !== null && item.colorMax !== undefined ? item.colorMax : maxColorsFor(item.name))
|
|
: null
|
|
|
|
const stock = lowestTrackedInventory(item)
|
|
const soldOut = stock !== null && stock <= 0
|
|
const lowStock = stock !== null && stock > 0 && stock <= LOW_STOCK_THRESHOLD
|
|
|
|
const priceDisplay = item.price ? `From ${fmt(item.price)}` : 'Custom quote'
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
className="product-card"
|
|
style={{ cursor: soldOut ? 'default' : 'pointer', opacity: soldOut ? 0.65 : 1 }}
|
|
onClick={() => !soldOut && setShowPicker(true)}
|
|
>
|
|
{/* Image */}
|
|
<div className="card-image" style={{ position: 'relative' }}>
|
|
{item.imageUrl ? (
|
|
// eslint-disable-next-line @next/next/no-img-element
|
|
<img src={item.imageUrl} alt={item.name} />
|
|
) : (
|
|
<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,
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
background: 'rgba(0,0,0,0.35)',
|
|
}}>
|
|
<span style={{
|
|
background: '#c0392b', color: '#fff',
|
|
fontWeight: 700, fontSize: '1rem', letterSpacing: '0.05em',
|
|
padding: '6px 18px', borderRadius: 4,
|
|
textTransform: 'uppercase',
|
|
}}>Sold Out</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="card-content">
|
|
<p className="title is-5">{item.name}</p>
|
|
<p className="subtitle is-6">{priceDisplay}</p>
|
|
{lowStock && (
|
|
<p style={{ fontSize: '0.78rem', color: '#c07000', fontWeight: 600, marginBottom: '0.35rem' }}>
|
|
Only {stock} left
|
|
</p>
|
|
)}
|
|
<p className="is-size-7">{item.description}</p>
|
|
|
|
{item.tags.length > 0 && (
|
|
<div className="card-tags">
|
|
{item.tags.slice(0, 3).map((tag) => (
|
|
<span key={tag} className="product-tag">{tag}</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer CTA */}
|
|
<footer className="card-footer" style={{ padding: '0.75rem' }}>
|
|
<button
|
|
className="button is-info is-fullwidth"
|
|
style={{ fontWeight: 'bold', fontSize: '1rem' }}
|
|
disabled={soldOut}
|
|
onClick={(e) => { e.stopPropagation(); if (!soldOut) setShowPicker(true) }}
|
|
>
|
|
{soldOut ? 'Sold Out' : 'Customize & Order'}
|
|
</button>
|
|
</footer>
|
|
</div>
|
|
|
|
{showPicker && (
|
|
<ColorPicker
|
|
product={item}
|
|
maxColors={maxColors}
|
|
onClose={() => setShowPicker(false)}
|
|
/>
|
|
)}
|
|
</>
|
|
)
|
|
}
|