Files
Audio-Classifier/frontend/components/FilterPanel.tsx
Benoit ed010fa60f
All checks were successful
Build and Push Docker Images / Build Backend Image (push) Successful in 14m9s
Build and Push Docker Images / Build Frontend Image (push) Successful in 7m36s
Fix #5 Fix #6 Fix #7
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
2026-01-27 22:43:32 +01:00

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 (&lt; 100 BPM)</option>
<option value="medium">Moyen (100-140 BPM)</option>
<option value="fast">Rapide (&gt; 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>
)
}