Refacto UX

This commit is contained in:
2025-12-23 09:40:14 +01:00
parent e567a2c5ab
commit 7ce2b70a8c
2 changed files with 343 additions and 401 deletions

View File

@@ -2,12 +2,12 @@
import { useState, useMemo } from "react" import { useState, useMemo } from "react"
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { getTracks, getStats } from "@/lib/api" import { getTracks } from "@/lib/api"
import type { FilterParams, Track } from "@/lib/types" import type { FilterParams, Track } from "@/lib/types"
import FilterPanel from "@/components/FilterPanel" import FilterPanel from "@/components/FilterPanel"
import AudioPlayer from "@/components/AudioPlayer" import AudioPlayer from "@/components/AudioPlayer"
// Helper function to format Discogs genre labels (e.g., "Pop---Ballad" -> ["Pop", "Ballad"]) // Helper function to format Discogs genre labels
function formatGenre(genre: string): { category: string; subgenre: string } { function formatGenre(genre: string): { category: string; subgenre: string } {
const parts = genre.split('---') const parts = genre.split('---')
return { return {
@@ -24,21 +24,17 @@ function extractFilterOptions(tracks: Track[]) {
const keys = new Set<string>() const keys = new Set<string>()
tracks.forEach(track => { tracks.forEach(track => {
// Extract genre category (before "---")
const genreCategory = formatGenre(track.classification.genre.primary).category const genreCategory = formatGenre(track.classification.genre.primary).category
genres.add(genreCategory) genres.add(genreCategory)
// Extract primary mood
if (track.classification.mood.primary) { if (track.classification.mood.primary) {
moods.add(track.classification.mood.primary) moods.add(track.classification.mood.primary)
} }
// Extract instruments
track.classification.instruments?.forEach(instrument => { track.classification.instruments?.forEach(instrument => {
instruments.add(instrument) instruments.add(instrument)
}) })
// Extract key
if (track.features.key) { if (track.features.key) {
keys.add(track.features.key) keys.add(track.features.key)
} }
@@ -56,21 +52,27 @@ export default function Home() {
const [filters, setFilters] = useState<FilterParams>({}) const [filters, setFilters] = useState<FilterParams>({})
const [page, setPage] = useState(0) const [page, setPage] = useState(0)
const [currentTrack, setCurrentTrack] = useState<Track | null>(null) const [currentTrack, setCurrentTrack] = useState<Track | null>(null)
const limit = 50 const [searchQuery, setSearchQuery] = useState("")
const limit = 25
const { data: tracksData, isLoading: isLoadingTracks } = useQuery({ const { data: tracksData, isLoading: isLoadingTracks } = useQuery({
queryKey: ['tracks', filters, page], queryKey: ['tracks', filters, page],
queryFn: () => getTracks({ ...filters, skip: page * limit, limit }), queryFn: () => getTracks({ ...filters, skip: page * limit, limit }),
}) })
const { data: stats } = useQuery({ // Filter tracks by search query on client side
queryKey: ['stats'], const filteredTracks = useMemo(() => {
queryFn: getStats, if (!tracksData?.tracks) return []
}) if (!searchQuery.trim()) return tracksData.tracks
const query = searchQuery.toLowerCase()
return tracksData.tracks.filter(track =>
track.filename.toLowerCase().includes(query) ||
track.metadata?.title?.toLowerCase().includes(query) ||
track.metadata?.artist?.toLowerCase().includes(query)
)
}, [tracksData?.tracks, searchQuery])
// Extract filter options from displayed tracks
// For better UX, we use the current page's tracks to populate filters
// TODO: Add a dedicated API endpoint to get all unique filter values
const filterOptions = useMemo(() => { const filterOptions = useMemo(() => {
if (!tracksData?.tracks || tracksData.tracks.length === 0) { if (!tracksData?.tracks || tracksData.tracks.length === 0) {
return { genres: [], moods: [], instruments: [], keys: [] } return { genres: [], moods: [], instruments: [], keys: [] }
@@ -78,212 +80,205 @@ export default function Home() {
return extractFilterOptions(tracksData.tracks) return extractFilterOptions(tracksData.tracks)
}, [tracksData]) }, [tracksData])
const totalPages = tracksData ? Math.ceil(tracksData.total / limit) : 0
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex flex-col">
{/* Header */} {/* Header */}
<header className="bg-white border-b"> <header className="bg-white border-b border-slate-200 shadow-sm sticky top-0 z-40">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4"> <div className="px-6 py-4">
<h1 className="text-3xl font-bold text-gray-900">Audio Classifier</h1> <div className="flex items-center justify-between">
<p className="text-gray-600">Intelligent music library management</p> <div>
<h1 className="text-2xl font-bold text-slate-800">Bibliothèque Musicale</h1>
<p className="text-sm text-slate-600">Gestion intelligente de votre collection audio</p>
</div>
{/* Search bar */}
<div className="flex-1 max-w-md ml-8">
<div className="relative">
<input
type="text"
placeholder="Rechercher un titre, artiste..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full px-4 py-2 pl-10 bg-slate-50 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
/>
<svg className="absolute left-3 top-2.5 w-5 h-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
</div>
<div className="ml-6 text-sm text-slate-600">
{tracksData?.total || 0} piste{(tracksData?.total || 0) > 1 ? 's' : ''}
</div>
</div>
</div> </div>
</header> </header>
{/* Main Content */} {/* Main content with sidebar */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 pb-32"> <div className="flex-1 flex overflow-hidden">
{/* Stats */} {/* Sidebar */}
{stats && ( <aside className="w-72 bg-white border-r border-slate-200 overflow-y-auto">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8"> <div className="p-6">
<div className="bg-white p-4 rounded-lg shadow"> <h2 className="text-lg font-semibold text-slate-800 mb-4">Filtres</h2>
<p className="text-gray-600 text-sm">Total Tracks</p>
<p className="text-2xl font-bold">{stats.total_tracks}</p>
</div>
<div className="bg-white p-4 rounded-lg shadow">
<p className="text-gray-600 text-sm">Avg BPM</p>
<p className="text-2xl font-bold">{stats.average_bpm}</p>
</div>
<div className="bg-white p-4 rounded-lg shadow">
<p className="text-gray-600 text-sm">Total Hours</p>
<p className="text-2xl font-bold">{stats.total_duration_hours}h</p>
</div>
<div className="bg-white p-4 rounded-lg shadow">
<p className="text-gray-600 text-sm">Genres</p>
<p className="text-2xl font-bold">{stats.genres.length}</p>
</div>
</div>
)}
{/* Filter Panel */}
<FilterPanel <FilterPanel
filters={filters} filters={filters}
onFiltersChange={(newFilters) => { onFiltersChange={(newFilters) => {
setFilters(newFilters) setFilters(newFilters)
setPage(0) // Reset to first page when filters change setPage(0)
}} }}
availableGenres={filterOptions.genres} availableGenres={filterOptions.genres}
availableMoods={filterOptions.moods} availableMoods={filterOptions.moods}
availableInstruments={filterOptions.instruments} availableInstruments={filterOptions.instruments}
availableKeys={filterOptions.keys} availableKeys={filterOptions.keys}
/> />
{/* Tracks List */}
<div className="bg-white rounded-lg shadow">
<div className="p-4 border-b">
<h2 className="text-xl font-semibold">Music Library</h2>
<p className="text-gray-600 text-sm">
{tracksData?.total || 0} tracks total
</p>
</div> </div>
</aside>
{/* Tracks list */}
<main className="flex-1 overflow-y-auto pb-32">
<div className="p-6">
{isLoadingTracks ? ( {isLoadingTracks ? (
<div className="p-8 text-center text-gray-600">Loading...</div> <div className="flex items-center justify-center h-64">
) : tracksData?.tracks.length === 0 ? ( <div className="text-slate-600">Chargement...</div>
<div className="p-8 text-center text-gray-600"> </div>
No tracks found. Start by analyzing your audio library! ) : filteredTracks.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64 text-slate-500">
<svg className="w-16 h-16 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3" />
</svg>
<p className="text-lg font-medium">Aucune piste trouvée</p>
<p className="text-sm mt-2">Essayez de modifier vos filtres ou votre recherche</p>
</div> </div>
) : ( ) : (
<div className="divide-y"> <>
{tracksData?.tracks.map((track) => ( <div className="grid grid-cols-1 gap-3">
<div key={track.id} className="p-4 hover:bg-gray-50"> {filteredTracks.map((track) => (
<div className="flex justify-between items-start"> <div
<div className="flex-1"> key={track.id}
<h3 className="font-medium text-gray-900">{track.filename}</h3> className={`bg-white rounded-lg p-4 border transition-all hover:shadow-md ${
currentTrack?.id === track.id
? 'border-orange-500 shadow-md'
: 'border-slate-200 hover:border-slate-300'
}`}
>
<div className="flex items-center gap-4">
{/* Play button */}
<button
onClick={() => setCurrentTrack(track)}
className="flex-shrink-0 w-12 h-12 flex items-center justify-center bg-orange-500 hover:bg-orange-600 rounded-full transition-colors shadow-sm"
>
{currentTrack?.id === track.id ? (
<svg className="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z"/>
</svg>
) : (
<svg className="w-5 h-5 text-white ml-0.5" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z"/>
</svg>
)}
</button>
{/* Primary metadata */} {/* Track info */}
<div className="mt-1 flex flex-wrap gap-2"> <div className="flex-1 min-w-0">
<h3 className="font-semibold text-slate-800 truncate text-base">
{track.metadata?.title || track.filename}
</h3>
{track.metadata?.artist && (
<p className="text-sm text-slate-600 truncate">{track.metadata.artist}</p>
)}
<div className="flex flex-wrap gap-2 mt-2">
{/* Genre */}
{(() => { {(() => {
const genre = formatGenre(track.classification.genre.primary) const genre = formatGenre(track.classification.genre.primary)
return ( return (
<> <>
<span className="inline-flex items-center px-2 py-1 rounded text-xs bg-blue-100 text-blue-800 font-semibold"> <span className="inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-orange-100 text-orange-800">
{genre.category} {genre.category}
</span> </span>
{genre.subgenre && ( {genre.subgenre && (
<span className="inline-flex items-center px-2 py-1 rounded text-xs bg-blue-50 text-blue-700"> <span className="inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-orange-50 text-orange-700">
{genre.subgenre} {genre.subgenre}
</span> </span>
)} )}
</> </>
) )
})()} })()}
<span className="inline-flex items-center px-2 py-1 rounded text-xs bg-purple-100 text-purple-800">
{/* Mood */}
<span className="inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-purple-100 text-purple-800">
{track.classification.mood.primary} {track.classification.mood.primary}
</span> </span>
<span className="text-xs text-gray-500">
{Math.round(track.features.tempo_bpm)} BPM {/* Key & BPM */}
</span> <span className="inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-slate-100 text-slate-700">
<span className="text-xs text-gray-500">
{track.features.key} {track.features.key}
</span> </span>
<span className="text-xs text-gray-500"> <span className="inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-slate-100 text-slate-700">
{Math.round(track.features.tempo_bpm)} BPM
</span>
{/* Duration */}
<span className="inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-slate-100 text-slate-700">
{Math.floor(track.duration_seconds / 60)}:{String(Math.floor(track.duration_seconds % 60)).padStart(2, '0')} {Math.floor(track.duration_seconds / 60)}:{String(Math.floor(track.duration_seconds % 60)).padStart(2, '0')}
</span> </span>
</div> </div>
{/* Secondary genres */}
{track.classification.genre.secondary && track.classification.genre.secondary.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
<span className="text-xs text-gray-400">Also:</span>
{track.classification.genre.secondary.slice(0, 3).map((g, i) => {
const genre = formatGenre(g)
return (
<span key={i} className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-blue-50 text-blue-600">
{genre.category}{genre.subgenre && `${genre.subgenre}`}
</span>
)
})}
</div>
)}
{/* Secondary moods */}
{track.classification.mood.secondary && track.classification.mood.secondary.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
<span className="text-xs text-gray-400">Moods:</span>
{track.classification.mood.secondary.map((mood, i) => (
<span key={i} className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-purple-50 text-purple-600">
{mood}
</span>
))}
</div>
)}
{/* Instruments */} {/* Instruments */}
{track.classification.instruments && track.classification.instruments.length > 0 && ( {track.classification.instruments && track.classification.instruments.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1.5 mt-2">
<span className="text-xs text-gray-400">Instruments:</span> <span className="text-xs text-slate-500">Instruments:</span>
{track.classification.instruments.slice(0, 6).map((instrument, i) => ( {track.classification.instruments.slice(0, 5).map((instrument, i) => (
<span key={i} className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-green-50 text-green-700"> <span key={i} className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-emerald-50 text-emerald-700">
{instrument} {instrument}
</span> </span>
))} ))}
{track.classification.instruments.length > 6 && ( {track.classification.instruments.length > 5 && (
<span className="text-xs text-gray-400">+{track.classification.instruments.length - 6} more</span> <span className="text-xs text-slate-400">+{track.classification.instruments.length - 5}</span>
)} )}
</div> </div>
)} )}
</div> </div>
<div className="ml-4 flex gap-2">
<button
onClick={() => setCurrentTrack(track)}
className="px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700"
>
{currentTrack?.id === track.id ? '▶ Playing' : 'Play'}
</button>
<a
href={`${process.env.NEXT_PUBLIC_API_URL}/api/audio/download/${track.id}`}
download
className="px-3 py-1 text-sm bg-gray-600 text-white rounded hover:bg-gray-700"
>
Download
</a>
</div>
</div> </div>
</div> </div>
))} ))}
</div> </div>
)}
{/* Pagination */} {/* Pagination */}
{tracksData && tracksData.total > limit && ( {totalPages > 1 && (
<div className="p-4 border-t flex justify-between items-center"> <div className="mt-8 flex items-center justify-between">
<button <button
onClick={() => setPage(p => Math.max(0, p - 1))} onClick={() => setPage(p => Math.max(0, p - 1))}
disabled={page === 0} disabled={page === 0}
className="px-4 py-2 bg-gray-200 rounded disabled:opacity-50" className="px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
> >
Previous Précédent
</button> </button>
<span className="text-sm text-gray-600">
Page {page + 1} of {Math.ceil(tracksData.total / limit)} <div className="flex items-center gap-2">
<span className="text-sm text-slate-600">
Page <span className="font-semibold">{page + 1}</span> sur <span className="font-semibold">{totalPages}</span>
</span> </span>
</div>
<button <button
onClick={() => setPage(p => p + 1)} onClick={() => setPage(p => p + 1)}
disabled={(page + 1) * limit >= tracksData.total} disabled={(page + 1) >= totalPages}
className="px-4 py-2 bg-gray-200 rounded disabled:opacity-50" className="px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
> >
Next Suivant
</button> </button>
</div> </div>
)} )}
</div> </>
)}
{/* Instructions */}
<div className="mt-8 bg-blue-50 border border-blue-200 rounded-lg p-6">
<h3 className="font-semibold text-blue-900 mb-2">Getting Started</h3>
<ol className="list-decimal list-inside space-y-1 text-blue-800 text-sm">
<li>Make sure the backend is running (<code>docker-compose up</code>)</li>
<li>Use the API to analyze your audio library:
<pre className="mt-2 bg-blue-100 p-2 rounded text-xs">
{`curl -X POST http://localhost:8000/api/analyze/folder \\
-H "Content-Type: application/json" \\
-d '{"path": "/audio/your_music", "recursive": true}'`}
</pre>
</li>
<li>Refresh this page to see your analyzed tracks</li>
</ol>
</div> </div>
</main> </main>
</div>
{/* Fixed Audio Player at bottom - always visible */} {/* Fixed Audio Player at bottom */}
<div className="fixed bottom-0 left-0 right-0 z-50"> <div className="fixed bottom-0 left-0 right-0 z-50">
<AudioPlayer track={currentTrack} /> <AudioPlayer track={currentTrack} />
</div> </div>

View File

@@ -22,7 +22,6 @@ export default function FilterPanel({
}: FilterPanelProps) { }: FilterPanelProps) {
const [localFilters, setLocalFilters] = useState<FilterParams>(filters) const [localFilters, setLocalFilters] = useState<FilterParams>(filters)
// Update local filters when parent changes
useEffect(() => { useEffect(() => {
setLocalFilters(filters) setLocalFilters(filters)
}, [filters]) }, [filters])
@@ -39,34 +38,34 @@ export default function FilterPanel({
onFiltersChange(emptyFilters) onFiltersChange(emptyFilters)
} }
const hasActiveFilters = Object.keys(localFilters).length > 0 const hasActiveFilters = Object.keys(localFilters).filter(key =>
localFilters[key as keyof FilterParams] !== undefined &&
localFilters[key as keyof FilterParams] !== ""
).length > 0
return ( return (
<div className="bg-white rounded-lg shadow p-4 mb-6"> <div className="space-y-6">
<div className="flex justify-between items-center mb-4"> {/* Clear all button */}
<h2 className="text-lg font-semibold text-gray-900">Filters</h2>
{hasActiveFilters && ( {hasActiveFilters && (
<button <button
onClick={clearFilters} onClick={clearFilters}
className="text-sm text-blue-600 hover:text-blue-800" 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"
> >
Clear all Effacer tous les filtres
</button> </button>
)} )}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Genre Filter */} {/* Genre Filter */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-semibold text-slate-700 mb-2">
Genre Genre
</label> </label>
<select <select
value={localFilters.genre || ""} value={localFilters.genre || ""}
onChange={(e) => handleFilterChange("genre", e.target.value || undefined)} onChange={(e) => handleFilterChange("genre", e.target.value || undefined)}
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" 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="">All Genres</option> <option value="">Tous les genres</option>
{availableGenres.map((genre) => ( {availableGenres.map((genre) => (
<option key={genre} value={genre}> <option key={genre} value={genre}>
{genre} {genre}
@@ -77,15 +76,15 @@ export default function FilterPanel({
{/* Mood Filter */} {/* Mood Filter */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-semibold text-slate-700 mb-2">
Mood Ambiance
</label> </label>
<select <select
value={localFilters.mood || ""} value={localFilters.mood || ""}
onChange={(e) => handleFilterChange("mood", e.target.value || undefined)} onChange={(e) => handleFilterChange("mood", e.target.value || undefined)}
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" 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="">All Moods</option> <option value="">Toutes les ambiances</option>
{availableMoods.map((mood) => ( {availableMoods.map((mood) => (
<option key={mood} value={mood}> <option key={mood} value={mood}>
{mood} {mood}
@@ -96,15 +95,15 @@ export default function FilterPanel({
{/* Instrument Filter */} {/* Instrument Filter */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-semibold text-slate-700 mb-2">
Instrument Instrument
</label> </label>
<select <select
value={localFilters.instrument || ""} value={localFilters.instrument || ""}
onChange={(e) => handleFilterChange("instrument", e.target.value || undefined)} onChange={(e) => handleFilterChange("instrument", e.target.value || undefined)}
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" 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="">All Instruments</option> <option value="">Tous les instruments</option>
{availableInstruments.map((instrument) => ( {availableInstruments.map((instrument) => (
<option key={instrument} value={instrument}> <option key={instrument} value={instrument}>
{instrument} {instrument}
@@ -115,15 +114,15 @@ export default function FilterPanel({
{/* Key Filter */} {/* Key Filter */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-semibold text-slate-700 mb-2">
Key Tonalité
</label> </label>
<select <select
value={localFilters.key || ""} value={localFilters.key || ""}
onChange={(e) => handleFilterChange("key", e.target.value || undefined)} onChange={(e) => handleFilterChange("key", e.target.value || undefined)}
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" 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="">All Keys</option> <option value="">Toutes les tonalités</option>
{availableKeys.map((key) => ( {availableKeys.map((key) => (
<option key={key} value={key}> <option key={key} value={key}>
{key} {key}
@@ -134,114 +133,62 @@ export default function FilterPanel({
{/* Tempo Range Filter */} {/* Tempo Range Filter */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-semibold text-slate-700 mb-2">
Tempo Tempo
</label> </label>
<select <select
value={localFilters.tempo_range || ""} value={localFilters.tempo_range || ""}
onChange={(e) => handleFilterChange("tempo_range", e.target.value || undefined)} onChange={(e) => handleFilterChange("tempo_range", e.target.value || undefined)}
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" 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="">All Tempos</option> <option value="">Tous les tempos</option>
<option value="slow">Slow (&lt;100 BPM)</option> <option value="slow">Lent (&lt; 100 BPM)</option>
<option value="medium">Medium (100-140 BPM)</option> <option value="medium">Moyen (100-140 BPM)</option>
<option value="fast">Fast (&gt;140 BPM)</option> <option value="fast">Rapide (&gt; 140 BPM)</option>
</select> </select>
</div> </div>
{/* Sort By */} {/* Active filters summary */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Sort By
</label>
<select
value={localFilters.sort_by || "analyzed_at"}
onChange={(e) => handleFilterChange("sort_by", e.target.value as any)}
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="analyzed_at">Date Added</option>
<option value="filename">Filename</option>
<option value="tempo_bpm">BPM</option>
<option value="duration_seconds">Duration</option>
<option value="energy">Energy</option>
</select>
</div>
{/* Sort Order */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Order
</label>
<select
value={localFilters.sort_desc ? "desc" : "asc"}
onChange={(e) => handleFilterChange("sort_desc", e.target.value === "desc")}
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
</div>
</div>
{/* Active Filters Display */}
{hasActiveFilters && ( {hasActiveFilters && (
<div className="mt-4 flex flex-wrap gap-2"> <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 && ( {localFilters.genre && (
<span className="inline-flex items-center px-2 py-1 rounded text-xs bg-blue-100 text-blue-800"> <div className="flex items-center justify-between text-xs">
Genre: {localFilters.genre} <span className="text-slate-600">Genre:</span>
<button <span className="font-medium text-slate-800">{localFilters.genre}</span>
onClick={() => handleFilterChange("genre", undefined)} </div>
className="ml-1 hover:text-blue-900"
>
×
</button>
</span>
)} )}
{localFilters.mood && ( {localFilters.mood && (
<span className="inline-flex items-center px-2 py-1 rounded text-xs bg-purple-100 text-purple-800"> <div className="flex items-center justify-between text-xs">
Mood: {localFilters.mood} <span className="text-slate-600">Ambiance:</span>
<button <span className="font-medium text-slate-800">{localFilters.mood}</span>
onClick={() => handleFilterChange("mood", undefined)} </div>
className="ml-1 hover:text-purple-900"
>
×
</button>
</span>
)} )}
{localFilters.instrument && ( {localFilters.instrument && (
<span className="inline-flex items-center px-2 py-1 rounded text-xs bg-green-100 text-green-800"> <div className="flex items-center justify-between text-xs">
Instrument: {localFilters.instrument} <span className="text-slate-600">Instrument:</span>
<button <span className="font-medium text-slate-800">{localFilters.instrument}</span>
onClick={() => handleFilterChange("instrument", undefined)} </div>
className="ml-1 hover:text-green-900"
>
×
</button>
</span>
)} )}
{localFilters.key && ( {localFilters.key && (
<span className="inline-flex items-center px-2 py-1 rounded text-xs bg-yellow-100 text-yellow-800"> <div className="flex items-center justify-between text-xs">
Key: {localFilters.key} <span className="text-slate-600">Tonalité:</span>
<button <span className="font-medium text-slate-800">{localFilters.key}</span>
onClick={() => handleFilterChange("key", undefined)} </div>
className="ml-1 hover:text-yellow-900"
>
×
</button>
</span>
)} )}
{localFilters.tempo_range && ( {localFilters.tempo_range && (
<span className="inline-flex items-center px-2 py-1 rounded text-xs bg-orange-100 text-orange-800"> <div className="flex items-center justify-between text-xs">
Tempo: {localFilters.tempo_range} <span className="text-slate-600">Tempo:</span>
<button <span className="font-medium text-slate-800">
onClick={() => handleFilterChange("tempo_range", undefined)} {localFilters.tempo_range === 'slow' && 'Lent'}
className="ml-1 hover:text-orange-900" {localFilters.tempo_range === 'medium' && 'Moyen'}
> {localFilters.tempo_range === 'fast' && 'Rapide'}
×
</button>
</span> </span>
</div>
)} )}
</div> </div>
</div>
)} )}
</div> </div>
) )