feat(marketplace): add BPM, key, category filters to MarketplaceHome

This commit is contained in:
senke 2026-02-22 14:14:20 +01:00
parent 3d7cc141fe
commit 7ee70925e8

View file

@ -49,8 +49,15 @@ export function MarketplaceHome() {
const [searchQuery, setSearchQuery] = useState('');
const [productType, setProductType] = useState<ProductType | ''>('');
const [priceRange, setPriceRange] = useState<[number, number]>([0, 1000]);
const [bpm, setBpm] = useState<number | ''>('');
const [musicalKey, setMusicalKey] = useState<string>('');
const [category, setCategory] = useState<string>('');
const [showFilters, setShowFilters] = useState(false);
const CATEGORIES = ['sample', 'beat', 'preset', 'pack'] as const;
const BPM_OPTIONS = [60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180];
const MUSICAL_KEYS = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B', 'Cm', 'C#m', 'Dm', 'D#m', 'Em', 'Fm', 'F#m', 'Gm', 'G#m', 'Am', 'A#m', 'Bm'];
const toast = useToast();
const { addItem, getItemCount } = useCartStore();
@ -65,6 +72,9 @@ export function MarketplaceHome() {
if (priceRange[0] > 0) filters.min_price = priceRange[0];
if (priceRange[1] < 1000) filters.max_price = priceRange[1];
if (searchQuery.trim()) filters.search = searchQuery.trim();
if (bpm !== '') filters.bpm = bpm;
if (musicalKey) filters.musical_key = musicalKey;
if (category) filters.category = category;
const response = await marketplaceService.fetchProducts(filters, { page, limit });
setProducts(response.products);
@ -93,7 +103,7 @@ export function MarketplaceHome() {
};
const priceRangeString = JSON.stringify(priceRange);
useEffect(() => { loadProducts(); }, [page, limit, productType, priceRangeString, searchQuery]);
useEffect(() => { loadProducts(); }, [page, limit, productType, priceRangeString, searchQuery, bpm, musicalKey, category]);
const handleAddToCart = (product: Product) => {
addItem(product);
@ -128,9 +138,12 @@ export function MarketplaceHome() {
setSearchQuery('');
setProductType('');
setPriceRange([0, 1000]);
setBpm('');
setMusicalKey('');
setCategory('');
};
const hasActiveFilters = searchQuery || productType || priceRange[0] > 0 || priceRange[1] < 1000;
const hasActiveFilters = searchQuery || productType || priceRange[0] > 0 || priceRange[1] < 1000 || bpm !== '' || musicalKey || category;
if (isLoading && products.length === 0) {
return <MarketplacePageSkeleton />;
@ -226,16 +239,46 @@ export function MarketplaceHome() {
</Badge>
</motion.div>
)}
{bpm !== '' && (
<motion.div key="bpm" initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.9 }} layout>
<Badge variant="secondary" className="gap-1.5 pl-3 pr-1.5 py-1 rounded-full">
BPM: {bpm}
<button onClick={() => setBpm('')} className="ml-1 rounded-full p-0.5 hover:bg-muted/50 transition-colors" aria-label="Remove BPM filter">
<X className="w-3 h-3" />
</button>
</Badge>
</motion.div>
)}
{musicalKey && (
<motion.div key="key" initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.9 }} layout>
<Badge variant="secondary" className="gap-1.5 pl-3 pr-1.5 py-1 rounded-full">
Key: {musicalKey}
<button onClick={() => setMusicalKey('')} className="ml-1 rounded-full p-0.5 hover:bg-muted/50 transition-colors" aria-label="Remove key filter">
<X className="w-3 h-3" />
</button>
</Badge>
</motion.div>
)}
{category && (
<motion.div key="category" initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.9 }} layout>
<Badge variant="secondary" className="gap-1.5 pl-3 pr-1.5 py-1 rounded-full capitalize">
Category: {category}
<button onClick={() => setCategory('')} className="ml-1 rounded-full p-0.5 hover:bg-muted/50 transition-colors" aria-label="Remove category filter">
<X className="w-3 h-3" />
</button>
</Badge>
</motion.div>
)}
</AnimatePresence>
</div>
)}
{/* Expanded Filters */}
{showFilters && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 pt-6 mt-4 border-t border-border animate-slide-down">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 pt-6 mt-4 border-t border-border animate-slide-down">
<div className="space-y-4">
<Label className="text-xs uppercase tracking-widest text-muted-foreground">Product Type</Label>
<div className="flex gap-2">
<div className="flex flex-wrap gap-2">
{['track', 'pack', 'service'].map(type => (
<Button
key={type}
@ -256,6 +299,48 @@ export function MarketplaceHome() {
</div>
<Slider min={0} max={1000} step={10} value={priceRange} onValueChange={(val) => setPriceRange([val[0] ?? 0, val[1] ?? 1000])} className="py-4" />
</div>
<div className="space-y-4">
<Label className="text-xs uppercase tracking-widest text-muted-foreground">BPM</Label>
<select
value={bpm === '' ? '' : String(bpm)}
onChange={(e) => setBpm(e.target.value === '' ? '' : Number(e.target.value))}
className="w-full h-10 rounded-lg border border-input bg-muted/30 px-3 text-sm text-foreground focus:ring-2 focus:ring-primary/40"
>
<option value="">Any</option>
{BPM_OPTIONS.map(v => (
<option key={v} value={v}>{v}</option>
))}
</select>
</div>
<div className="space-y-4">
<Label className="text-xs uppercase tracking-widest text-muted-foreground">Musical Key</Label>
<select
value={musicalKey}
onChange={(e) => setMusicalKey(e.target.value)}
className="w-full h-10 rounded-lg border border-input bg-muted/30 px-3 text-sm text-foreground focus:ring-2 focus:ring-primary/40"
>
<option value="">Any</option>
{MUSICAL_KEYS.map(k => (
<option key={k} value={k}>{k}</option>
))}
</select>
</div>
<div className="space-y-4 md:col-span-2 lg:col-span-4">
<Label className="text-xs uppercase tracking-widest text-muted-foreground">Category</Label>
<div className="flex flex-wrap gap-2">
{CATEGORIES.map(cat => (
<Button
key={cat}
variant={category === cat ? 'default' : 'outline'}
onClick={() => setCategory(category === cat ? '' : cat)}
size="sm"
className="capitalize rounded-full px-6"
>
{cat}
</Button>
))}
</div>
</div>
</div>
)}
</Card>