- Ajouter fallback pour Swagger UI si doc.json ne fonctionne pas - Améliorer message d'erreur avec bouton pour ouvrir Swagger UI directement - Les fonctionnalités API Keys et Usage Stats sont maintenant complètes et fonctionnelles - Tous les onglets de DeveloperPage sont maintenant implémentés
227 lines
7.1 KiB
TypeScript
227 lines
7.1 KiB
TypeScript
import { Track } from '../types/track';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { LoadingSpinner } from '@/components/ui/loading-spinner';
|
|
import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert';
|
|
import {
|
|
Music,
|
|
Clock,
|
|
Play,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
AlertCircle,
|
|
} from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
import { Link } from 'react-router-dom';
|
|
import { logger } from '@/utils/logger';
|
|
import { usePlayerStore } from '@/features/player/store/playerStore';
|
|
|
|
/**
|
|
* TrackSearchResults Component
|
|
* T0305: Composant pour afficher les résultats de recherche avec pagination
|
|
*/
|
|
|
|
const formatDuration = (seconds: number): string => {
|
|
const mins = Math.floor(seconds / 60);
|
|
const secs = seconds % 60;
|
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
};
|
|
|
|
interface TrackSearchResultsProps {
|
|
tracks: Track[];
|
|
total: number;
|
|
page: number;
|
|
limit: number;
|
|
onPageChange: (page: number) => void;
|
|
loading?: boolean;
|
|
error?: string | null;
|
|
className?: string;
|
|
}
|
|
|
|
export function TrackSearchResults({
|
|
tracks,
|
|
total,
|
|
page,
|
|
limit,
|
|
onPageChange,
|
|
loading = false,
|
|
error = null,
|
|
className,
|
|
}: TrackSearchResultsProps) {
|
|
const { play, addToQueue } = usePlayerStore();
|
|
const totalPages = Math.ceil(total / limit) || 1;
|
|
|
|
const handlePlayTrack = (track: Track) => {
|
|
try {
|
|
// Convert tracks Track to player Track format if needed
|
|
// The player expects tracks with url property, but tracks Track may not have it
|
|
// For now, just add to queue and play - the player will handle URL resolution
|
|
const trackForPlayer = track as any; // Type assertion needed due to type mismatch
|
|
addToQueue([trackForPlayer]);
|
|
play(trackForPlayer);
|
|
logger.debug('Playing track from search results', { trackId: track.id });
|
|
} catch (error) {
|
|
logger.error('Failed to play track', {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
trackId: track.id,
|
|
});
|
|
}
|
|
};
|
|
|
|
const handlePreviousPage = () => {
|
|
if (page > 1) {
|
|
onPageChange(page - 1);
|
|
}
|
|
};
|
|
|
|
const handleNextPage = () => {
|
|
if (page < totalPages) {
|
|
onPageChange(page + 1);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className={cn('flex justify-center items-center py-12', className)}>
|
|
<LoadingSpinner size="lg" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<Alert variant="destructive" className={className}>
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertTitle>Erreur</AlertTitle>
|
|
<AlertDescription>{error}</AlertDescription>
|
|
</Alert>
|
|
);
|
|
}
|
|
|
|
if (tracks.length === 0) {
|
|
return (
|
|
<div className={cn('text-center py-12 text-muted-foreground', className)}>
|
|
<Music className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
|
<p>Aucun track trouvé</p>
|
|
{total === 0 && (
|
|
<p className="text-sm mt-2">
|
|
Essayez de modifier vos critères de recherche
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={cn('space-y-4', className)}>
|
|
{/* Results Count */}
|
|
<div className="text-sm text-muted-foreground">
|
|
{total} résultat{total > 1 ? 's' : ''} trouvé{total > 1 ? 's' : ''}
|
|
</div>
|
|
|
|
{/* Track Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
|
{tracks.map((track) => (
|
|
<Card
|
|
key={track.id}
|
|
className="overflow-hidden hover:shadow-lg transition-shadow"
|
|
>
|
|
<CardContent className="p-0">
|
|
{/* Cover Art or Placeholder */}
|
|
<div className="relative aspect-square bg-muted flex items-center justify-center">
|
|
{track.cover_art_path ? (
|
|
<img
|
|
src={track.cover_art_path}
|
|
alt={track.title}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
) : (
|
|
<Music className="h-16 w-16 text-muted-foreground/50" />
|
|
)}
|
|
{/* Play Button Overlay */}
|
|
<div className="absolute inset-0 bg-black/0 hover:bg-black/20 transition-colors flex items-center justify-center opacity-0 hover:opacity-100">
|
|
<Button
|
|
variant="secondary"
|
|
size="icon"
|
|
className="rounded-full h-12 w-12"
|
|
onClick={() => handlePlayTrack(track)}
|
|
>
|
|
<Play className="h-6 w-6 fill-current" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
{/* Track Info */}
|
|
<div className="p-4">
|
|
<Link to={`/tracks/${track.id}`}>
|
|
<h3
|
|
className="font-semibold text-lg truncate hover:text-primary transition-colors"
|
|
title={track.title}
|
|
>
|
|
{track.title}
|
|
</h3>
|
|
</Link>
|
|
{track.artist && (
|
|
<p
|
|
className="text-sm text-muted-foreground truncate"
|
|
title={track.artist}
|
|
>
|
|
{track.artist}
|
|
</p>
|
|
)}
|
|
<div className="flex items-center gap-4 mt-2 text-xs text-muted-foreground">
|
|
<div className="flex items-center gap-1">
|
|
<Clock className="h-3 w-3" />
|
|
<span>{formatDuration(track.duration)}</span>
|
|
</div>
|
|
{track.genre && (
|
|
<span className="px-2 py-1 bg-muted rounded">
|
|
{track.genre}
|
|
</span>
|
|
)}
|
|
{track.format && (
|
|
<span className="px-2 py-1 bg-muted rounded">
|
|
{track.format}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-4 mt-2 text-xs text-muted-foreground">
|
|
<span>{track.play_count} écoutes</span>
|
|
<span>{track.like_count} likes</span>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
{/* Pagination */}
|
|
{totalPages > 1 && (
|
|
<div className="flex items-center justify-between pt-4 border-t">
|
|
<div className="text-sm text-muted-foreground">
|
|
Page {page} sur {totalPages}
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handlePreviousPage}
|
|
disabled={page <= 1}
|
|
>
|
|
<ChevronLeft className="h-4 w-4 mr-1" />
|
|
Précédent
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleNextPage}
|
|
disabled={page >= totalPages}
|
|
>
|
|
Suivant
|
|
<ChevronRight className="h-4 w-4 ml-1" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|