147 lines
4 KiB
TypeScript
147 lines
4 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
import {
|
|
searchTracks,
|
|
TrackSearchParams,
|
|
TrackSearchError,
|
|
} from '../services/trackSearchService';
|
|
import { Track } from '../types/track';
|
|
import { TrackSearchFilters } from './TrackSearchFilters';
|
|
import { TrackSearchResults } from './TrackSearchResults';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Button } from '@/components/ui/button';
|
|
import { useDebounce } from '@/hooks/useDebounce';
|
|
import { Search } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
/**
|
|
* TrackSearch Component
|
|
* T0305: Composant principal pour la recherche avancée de tracks
|
|
*/
|
|
|
|
interface TrackSearchProps {
|
|
className?: string;
|
|
initialQuery?: string;
|
|
}
|
|
|
|
export function TrackSearch({
|
|
className,
|
|
initialQuery = '',
|
|
}: TrackSearchProps) {
|
|
const [query, setQuery] = useState(initialQuery);
|
|
const [filters, setFilters] = useState<Partial<TrackSearchParams>>({});
|
|
const [tracks, setTracks] = useState<Track[]>([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [page, setPage] = useState(1);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Debounce de la query pour éviter trop de requêtes
|
|
const debouncedQuery = useDebounce(query, 500);
|
|
|
|
// Fonction pour effectuer la recherche
|
|
const performSearch = useCallback(async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const params: TrackSearchParams = {
|
|
query: debouncedQuery.trim() || undefined,
|
|
...filters,
|
|
page,
|
|
limit: 20,
|
|
};
|
|
|
|
const response = await searchTracks(params);
|
|
setTracks(response.tracks);
|
|
setTotal(response.pagination.total);
|
|
} catch (err) {
|
|
let errorMessage = 'Erreur lors de la recherche';
|
|
if (err instanceof TrackSearchError) {
|
|
errorMessage = err.message;
|
|
} else if (err instanceof Error) {
|
|
errorMessage = err.message;
|
|
}
|
|
setError(errorMessage);
|
|
setTracks([]);
|
|
setTotal(0);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [debouncedQuery, filters, page]);
|
|
|
|
// Effectuer la recherche quand les paramètres changent
|
|
useEffect(() => {
|
|
performSearch();
|
|
}, [performSearch]);
|
|
|
|
// Réinitialiser la page quand les filtres ou la query changent
|
|
useEffect(() => {
|
|
setPage(1);
|
|
}, [
|
|
debouncedQuery,
|
|
filters.genre,
|
|
filters.musicalKey,
|
|
filters.format,
|
|
filters.tags,
|
|
filters.minDuration,
|
|
filters.maxDuration,
|
|
filters.minBPM,
|
|
filters.maxBPM,
|
|
filters.minDate,
|
|
filters.maxDate,
|
|
]);
|
|
|
|
const handleSearch = () => {
|
|
setPage(1);
|
|
performSearch();
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'space-y-6 transition-opacity duration-[var(--sumi-duration-normal)]',
|
|
className,
|
|
)}
|
|
>
|
|
{/* Search Bar */}
|
|
<div className="flex gap-2">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground/90 pointer-events-none" />
|
|
<Input
|
|
type="text"
|
|
placeholder="Rechercher des tracks..."
|
|
value={query}
|
|
onChange={(e) => setQuery(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
handleSearch();
|
|
}
|
|
}}
|
|
className="pl-10 rounded-md transition-[border-color,box-shadow] duration-[var(--sumi-duration-normal)]"
|
|
/>
|
|
</div>
|
|
<Button
|
|
onClick={handleSearch}
|
|
disabled={loading}
|
|
className="rounded-md transition-transform duration-[var(--sumi-duration-normal)] active:scale-95"
|
|
>
|
|
Rechercher
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<TrackSearchFilters filters={filters} onFiltersChange={setFilters} />
|
|
|
|
{/* Results */}
|
|
<TrackSearchResults
|
|
tracks={tracks}
|
|
total={total}
|
|
page={page}
|
|
limit={20}
|
|
onPageChange={setPage}
|
|
loading={loading}
|
|
error={error}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|