1. Tri aléatoire ✅ Ajout d'un sélecteur de tri dans l'en-tête avec options "Récents" et "Aléatoire" Backend : nouveau paramètre sort_by="random" qui utilise func.random() de PostgreSQL Les résultats seront maintenant mélangés aléatoirement, permettant de découvrir tous les titres de la bibliothèque 2. Correction du bug de recherche textuelle ✅ La recherche charge maintenant jusqu'à 10 000 résultats au lieu de 25 quand une requête est active La recherche filtre ensuite sur TOUS les résultats chargés, pas seulement la page active Cela permet de rechercher dans toute la bibliothèque de résultats filtrés 3. Filtres exclusifs pour instruments ✅ Nouvelle option "Uniquement ces instruments (mode exclusif)" qui apparaît quand des instruments sont sélectionnés Backend : nouveau paramètre instruments_exclusive qui vérifie que le track a EXACTEMENT les instruments sélectionnés (pas d'autres) Frontend : checkbox dans un bandeau orange/ambre pour activer le mode exclusif Les modifications touchent : Backend : crud.py et tracks.py Frontend : types.ts, FilterPanel.tsx et page.tsx
232 lines
9.0 KiB
TypeScript
232 lines
9.0 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import type { FilterParams } from "@/lib/types"
|
|
|
|
interface FilterPanelProps {
|
|
filters: FilterParams
|
|
onFiltersChange: (filters: FilterParams) => void
|
|
availableGenres: string[]
|
|
availableMoods: string[]
|
|
availableInstruments: string[]
|
|
availableKeys: string[]
|
|
}
|
|
|
|
export default function FilterPanel({
|
|
filters,
|
|
onFiltersChange,
|
|
availableGenres,
|
|
availableMoods,
|
|
availableInstruments,
|
|
availableKeys,
|
|
}: FilterPanelProps) {
|
|
const [localFilters, setLocalFilters] = useState<FilterParams>(filters)
|
|
|
|
useEffect(() => {
|
|
setLocalFilters(filters)
|
|
}, [filters])
|
|
|
|
const handleFilterChange = (key: keyof FilterParams, value: any) => {
|
|
const newFilters = { ...localFilters, [key]: value }
|
|
setLocalFilters(newFilters)
|
|
onFiltersChange(newFilters)
|
|
}
|
|
|
|
const clearFilters = () => {
|
|
const emptyFilters: FilterParams = {}
|
|
setLocalFilters(emptyFilters)
|
|
onFiltersChange(emptyFilters)
|
|
}
|
|
|
|
const hasActiveFilters = Object.keys(localFilters).filter(key =>
|
|
localFilters[key as keyof FilterParams] !== undefined &&
|
|
localFilters[key as keyof FilterParams] !== ""
|
|
).length > 0
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Clear all button */}
|
|
{hasActiveFilters && (
|
|
<button
|
|
onClick={clearFilters}
|
|
className="w-full text-sm text-orange-600 hover:text-orange-700 font-medium py-2 px-3 bg-orange-50 rounded-lg hover:bg-orange-100 transition-colors"
|
|
>
|
|
Effacer tous les filtres
|
|
</button>
|
|
)}
|
|
|
|
{/* Genre Filter */}
|
|
<div>
|
|
<label className="block text-sm font-semibold text-slate-700 mb-2">
|
|
Genre
|
|
</label>
|
|
<select
|
|
value={localFilters.genre || ""}
|
|
onChange={(e) => handleFilterChange("genre", e.target.value || undefined)}
|
|
className="w-full px-3 py-2 bg-slate-50 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
|
>
|
|
<option value="">Tous les genres</option>
|
|
{availableGenres.map((genre) => (
|
|
<option key={genre} value={genre}>
|
|
{genre}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Mood Filter */}
|
|
<div>
|
|
<label className="block text-sm font-semibold text-slate-700 mb-2">
|
|
Ambiance
|
|
</label>
|
|
<select
|
|
value={localFilters.mood || ""}
|
|
onChange={(e) => handleFilterChange("mood", e.target.value || undefined)}
|
|
className="w-full px-3 py-2 bg-slate-50 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
|
>
|
|
<option value="">Toutes les ambiances</option>
|
|
{availableMoods.map((mood) => (
|
|
<option key={mood} value={mood}>
|
|
{mood}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Instrument Filter - Multiple Selection */}
|
|
<div>
|
|
<label className="block text-sm font-semibold text-slate-700 mb-2">
|
|
Instruments
|
|
</label>
|
|
<div className="space-y-2 max-h-48 overflow-y-auto bg-slate-50 border border-slate-300 rounded-lg p-2">
|
|
{availableInstruments.length === 0 ? (
|
|
<p className="text-xs text-slate-500 p-2">Aucun instrument disponible</p>
|
|
) : (
|
|
availableInstruments.map((instrument) => {
|
|
const isSelected = localFilters.instruments?.includes(instrument) || false
|
|
return (
|
|
<label
|
|
key={instrument}
|
|
className="flex items-center gap-2 p-2 hover:bg-slate-100 rounded cursor-pointer transition-colors"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={isSelected}
|
|
onChange={(e) => {
|
|
const currentInstruments = localFilters.instruments || []
|
|
const newInstruments = e.target.checked
|
|
? [...currentInstruments, instrument]
|
|
: currentInstruments.filter(i => i !== instrument)
|
|
handleFilterChange("instruments", newInstruments.length > 0 ? newInstruments : undefined)
|
|
}}
|
|
className="w-4 h-4 text-orange-500 border-slate-300 rounded focus:ring-2 focus:ring-orange-500 focus:ring-offset-0"
|
|
/>
|
|
<span className="text-sm text-slate-700">{instrument}</span>
|
|
</label>
|
|
)
|
|
})
|
|
)}
|
|
</div>
|
|
|
|
{/* Exclusive filter toggle */}
|
|
{localFilters.instruments && localFilters.instruments.length > 0 && (
|
|
<label className="flex items-center gap-2 mt-2 p-2 bg-amber-50 border border-amber-200 rounded-lg cursor-pointer hover:bg-amber-100 transition-colors">
|
|
<input
|
|
type="checkbox"
|
|
checked={localFilters.instruments_exclusive || false}
|
|
onChange={(e) => handleFilterChange("instruments_exclusive", e.target.checked || undefined)}
|
|
className="w-4 h-4 text-amber-600 border-amber-300 rounded focus:ring-2 focus:ring-amber-500 focus:ring-offset-0"
|
|
/>
|
|
<span className="text-xs font-medium text-amber-800">Uniquement ces instruments (mode exclusif)</span>
|
|
</label>
|
|
)}
|
|
</div>
|
|
|
|
{/* Key Filter */}
|
|
<div>
|
|
<label className="block text-sm font-semibold text-slate-700 mb-2">
|
|
Tonalité
|
|
</label>
|
|
<select
|
|
value={localFilters.key || ""}
|
|
onChange={(e) => handleFilterChange("key", e.target.value || undefined)}
|
|
className="w-full px-3 py-2 bg-slate-50 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
|
>
|
|
<option value="">Toutes les tonalités</option>
|
|
{availableKeys.map((key) => (
|
|
<option key={key} value={key}>
|
|
{key}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Tempo Range Filter */}
|
|
<div>
|
|
<label className="block text-sm font-semibold text-slate-700 mb-2">
|
|
Tempo
|
|
</label>
|
|
<select
|
|
value={localFilters.tempo_range || ""}
|
|
onChange={(e) => handleFilterChange("tempo_range", e.target.value || undefined)}
|
|
className="w-full px-3 py-2 bg-slate-50 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
|
>
|
|
<option value="">Tous les tempos</option>
|
|
<option value="slow">Lent (< 100 BPM)</option>
|
|
<option value="medium">Moyen (100-140 BPM)</option>
|
|
<option value="fast">Rapide (> 140 BPM)</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Active filters summary */}
|
|
{hasActiveFilters && (
|
|
<div className="pt-4 border-t border-slate-200">
|
|
<p className="text-xs font-semibold text-slate-700 mb-2">Filtres actifs:</p>
|
|
<div className="space-y-1">
|
|
{localFilters.genre && (
|
|
<div className="flex items-center justify-between text-xs">
|
|
<span className="text-slate-600">Genre:</span>
|
|
<span className="font-medium text-slate-800">{localFilters.genre}</span>
|
|
</div>
|
|
)}
|
|
{localFilters.mood && (
|
|
<div className="flex items-center justify-between text-xs">
|
|
<span className="text-slate-600">Ambiance:</span>
|
|
<span className="font-medium text-slate-800">{localFilters.mood}</span>
|
|
</div>
|
|
)}
|
|
{localFilters.instruments && localFilters.instruments.length > 0 && (
|
|
<div className="text-xs">
|
|
<span className="text-slate-600">Instruments:</span>
|
|
<div className="flex flex-wrap gap-1 mt-1">
|
|
{localFilters.instruments.map((instrument) => (
|
|
<span key={instrument} className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-emerald-50 text-emerald-700">
|
|
{instrument}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
{localFilters.key && (
|
|
<div className="flex items-center justify-between text-xs">
|
|
<span className="text-slate-600">Tonalité:</span>
|
|
<span className="font-medium text-slate-800">{localFilters.key}</span>
|
|
</div>
|
|
)}
|
|
{localFilters.tempo_range && (
|
|
<div className="flex items-center justify-between text-xs">
|
|
<span className="text-slate-600">Tempo:</span>
|
|
<span className="font-medium text-slate-800">
|
|
{localFilters.tempo_range === 'slow' && 'Lent'}
|
|
{localFilters.tempo_range === 'medium' && 'Moyen'}
|
|
{localFilters.tempo_range === 'fast' && 'Rapide'}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|