veza/apps/web/src/features/tracks/components/TrackSearch.tsx

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>
);
}