veza/apps/web/src/features/tracks/components/TrackSearchResults.tsx
senke 023b8a89c6 fix: Corriger URL Swagger et finaliser implémentation DeveloperPage
- 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
2026-01-18 13:55:28 +01:00

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