feat(marketplace): add BPM, key, category filters to MarketplaceHome
This commit is contained in:
parent
3d7cc141fe
commit
7ee70925e8
1 changed files with 89 additions and 4 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue