2025-12-17 13:07:35 +00:00
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
2026-01-07 18:39:21 +00:00
|
|
|
import { getTrack } from '../services/trackService';
|
|
|
|
|
import { TrackServiceError as TrackUploadError } from '../errors/trackErrors';
|
2025-12-17 13:07:35 +00:00
|
|
|
import { usePlayerStore } from '@/features/player/store/playerStore';
|
|
|
|
|
import type { Track as PlayerTrack } from '@/features/player/types';
|
2026-01-15 18:26:53 +00:00
|
|
|
import toast from '@/utils/toast';
|
2026-01-25 11:33:46 +00:00
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
|
import { Card } from '@/components/ui/card';
|
2025-12-17 13:07:35 +00:00
|
|
|
import { LoadingSpinner } from '@/components/ui/loading-spinner';
|
2026-01-11 16:05:23 +00:00
|
|
|
import { ErrorDisplay } from '@/components/ui/ErrorDisplay';
|
2025-12-24 11:57:49 +00:00
|
|
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
2026-01-13 18:47:57 +00:00
|
|
|
import {
|
2026-01-26 13:12:17 +00:00
|
|
|
Play, Pause, ArrowLeft, Share2, Plus, BarChart3, Music, Heart, Clock
|
2026-01-13 18:47:57 +00:00
|
|
|
} from 'lucide-react';
|
2025-12-17 13:07:35 +00:00
|
|
|
import type { Track } from '../types/track';
|
2025-12-24 11:57:49 +00:00
|
|
|
import { CommentSection } from '../components/CommentSection';
|
|
|
|
|
import { ShareDialog } from '../components/ShareDialog';
|
|
|
|
|
import { TrackHistory } from '../components/TrackHistory';
|
|
|
|
|
import { TrackStatsDisplay } from '../components/TrackStatsDisplay';
|
|
|
|
|
|
2025-12-17 13:07:35 +00:00
|
|
|
function formatDuration(seconds: number): string {
|
|
|
|
|
const minutes = Math.floor(seconds / 60);
|
|
|
|
|
const secs = Math.floor(seconds % 60);
|
|
|
|
|
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function TrackDetailPage() {
|
|
|
|
|
const { id } = useParams<{ id: string }>();
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
const [track, setTrack] = useState<Track | null>(null);
|
|
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
2026-01-11 16:05:23 +00:00
|
|
|
const [error, setError] = useState<Error | null>(null);
|
2025-12-24 11:57:49 +00:00
|
|
|
const [isShareDialogOpen, setIsShareDialogOpen] = useState(false);
|
2025-12-17 13:07:35 +00:00
|
|
|
|
|
|
|
|
const { play, pause, currentTrack, isPlaying, addToQueue } = usePlayerStore();
|
|
|
|
|
|
2026-01-11 16:05:23 +00:00
|
|
|
const loadTrack = async () => {
|
2025-12-17 13:07:35 +00:00
|
|
|
if (!id) {
|
2026-01-11 16:05:23 +00:00
|
|
|
setError(new Error('Track ID is required'));
|
2025-12-17 13:07:35 +00:00
|
|
|
setIsLoading(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-01-11 16:05:23 +00:00
|
|
|
try {
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
const loadedTrack = await getTrack(id);
|
|
|
|
|
setTrack(loadedTrack);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
const errorMessage =
|
2026-01-26 13:12:17 +00:00
|
|
|
err instanceof TrackUploadError ? err.message : err instanceof Error ? err.message : 'Failed to load track';
|
2026-01-11 16:05:23 +00:00
|
|
|
setError(new Error(errorMessage));
|
|
|
|
|
} finally {
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-12-17 13:07:35 +00:00
|
|
|
|
2026-01-26 13:12:17 +00:00
|
|
|
useEffect(() => { loadTrack(); }, [id]);
|
2025-12-17 13:07:35 +00:00
|
|
|
|
|
|
|
|
const mapToPlayerTrack = (t: Track): PlayerTrack => ({
|
|
|
|
|
id: t.id,
|
|
|
|
|
title: t.title,
|
|
|
|
|
artist: t.artist,
|
|
|
|
|
album: t.album,
|
|
|
|
|
duration: t.duration,
|
|
|
|
|
url: t.stream_manifest_url || t.file_path,
|
|
|
|
|
cover: t.cover_art_path,
|
|
|
|
|
genre: t.genre,
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-26 13:12:17 +00:00
|
|
|
const handlePlay = () => { if (track) play(mapToPlayerTrack(track)); };
|
|
|
|
|
const handlePause = () => { pause(); };
|
|
|
|
|
const handleAddToQueue = () => { if (track) { addToQueue([mapToPlayerTrack(track)]); toast.success("Added to queue"); } };
|
|
|
|
|
const handleShare = () => { setIsShareDialogOpen(true); };
|
2025-12-17 13:07:35 +00:00
|
|
|
|
|
|
|
|
const isCurrentTrack = currentTrack?.id === track?.id;
|
|
|
|
|
const isCurrentlyPlaying = isCurrentTrack && isPlaying;
|
|
|
|
|
|
|
|
|
|
if (isLoading) {
|
2026-01-26 13:12:17 +00:00
|
|
|
return <div className="flex items-center justify-center min-h-screen"><LoadingSpinner /></div>;
|
2025-12-17 13:07:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (error || !track) {
|
|
|
|
|
return (
|
2026-01-26 13:12:17 +00:00
|
|
|
<div className="container mx-auto px-4 py-8 flex items-center justify-center min-h-screen">
|
2026-01-11 16:05:23 +00:00
|
|
|
<ErrorDisplay
|
2026-01-26 13:12:17 +00:00
|
|
|
error={error || new Error('Track not found')}
|
2026-01-11 16:05:23 +00:00
|
|
|
variant="card"
|
|
|
|
|
severity="error"
|
|
|
|
|
onRetry={loadTrack}
|
2026-01-26 13:12:17 +00:00
|
|
|
actions={[{ label: 'Go Back', onClick: () => navigate(-1), variant: 'outline' }]}
|
2026-01-11 16:05:23 +00:00
|
|
|
/>
|
2025-12-17 13:07:35 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2026-01-26 13:12:17 +00:00
|
|
|
<div className="min-h-screen pb-24 relative overflow-hidden bg-background">
|
2025-12-17 13:07:35 +00:00
|
|
|
|
2026-01-26 13:12:17 +00:00
|
|
|
{/* 1. Immersive Blur Background */}
|
|
|
|
|
<div className="absolute inset-0 h-[60vh] overflow-hidden pointer-events-none">
|
|
|
|
|
<div className="absolute inset-x-0 -top-40 h-[150%] opacity-20 blur-[120px] scale-110"
|
|
|
|
|
style={{ background: track.cover_art_path ? `url(${track.cover_art_path}) center/cover` : 'var(--primary)' }}
|
|
|
|
|
/>
|
|
|
|
|
<div className="absolute inset-0 bg-gradient-to-b from-background/40 via-background/80 to-background" />
|
|
|
|
|
</div>
|
2025-12-17 13:07:35 +00:00
|
|
|
|
2026-01-26 13:12:17 +00:00
|
|
|
<div className="container mx-auto px-4 relative z-10 pt-8">
|
|
|
|
|
<Button onClick={() => navigate(-1)} variant="ghost" className="mb-6 rounded-full hover:bg-white/10">
|
|
|
|
|
<ArrowLeft className="h-4 w-4 mr-2" /> Back
|
|
|
|
|
</Button>
|
2025-12-17 13:07:35 +00:00
|
|
|
|
2026-01-26 13:12:17 +00:00
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-start">
|
|
|
|
|
|
|
|
|
|
{/* Left Column: Cover & Main Actions */}
|
|
|
|
|
<div className="lg:col-span-4 sticky top-24 space-y-8">
|
|
|
|
|
<div className="relative aspect-square rounded-3xl overflow-hidden shadow-[0_20px_50px_rgba(0,0,0,0.5)] border border-white/10 group">
|
|
|
|
|
{track.cover_art_path ? (
|
|
|
|
|
<img src={track.cover_art_path} alt={track.title} className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105" />
|
2026-01-07 09:32:53 +00:00
|
|
|
) : (
|
2026-01-26 13:12:17 +00:00
|
|
|
<div className="w-full h-full bg-gradient-to-br from-gray-800 to-black flex items-center justify-center">
|
|
|
|
|
<Music className="h-24 w-24 text-white/20" />
|
|
|
|
|
</div>
|
2026-01-07 09:32:53 +00:00
|
|
|
)}
|
2026-01-26 13:12:17 +00:00
|
|
|
|
|
|
|
|
{/* Vinyl Shine Effect */}
|
|
|
|
|
<div className="absolute inset-0 bg-gradient-to-tr from-white/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
|
2026-01-07 09:32:53 +00:00
|
|
|
</div>
|
2025-12-17 13:07:35 +00:00
|
|
|
|
2026-01-26 13:12:17 +00:00
|
|
|
<Card variant="glass" className="p-4 border-white/5 bg-black/30 backdrop-blur-xl">
|
|
|
|
|
<div className="flex gap-3">
|
|
|
|
|
{isCurrentlyPlaying ? (
|
|
|
|
|
<Button onClick={handlePause} className="flex-1 h-12 bg-primary text-primary-foreground font-bold shadow-glow-cyan hover:scale-[1.02] transition-transform">
|
|
|
|
|
<Pause className="h-5 w-5 mr-2 fill-current" /> Pause
|
|
|
|
|
</Button>
|
|
|
|
|
) : (
|
|
|
|
|
<Button onClick={handlePlay} className="flex-1 h-12 bg-primary text-primary-foreground font-bold shadow-glow-cyan hover:scale-[1.02] transition-transform">
|
|
|
|
|
<Play className="h-5 w-5 mr-2 fill-current" /> Play
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
<Button onClick={handleAddToQueue} variant="secondary" className="h-12 w-12 p-0 rounded-xl" title="Add to Queue">
|
|
|
|
|
<Plus className="h-5 w-5" />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button onClick={handleShare} variant="secondary" className="h-12 w-12 p-0 rounded-xl" title="Share">
|
|
|
|
|
<Share2 className="h-5 w-5" />
|
|
|
|
|
</Button>
|
2026-01-07 09:32:53 +00:00
|
|
|
</div>
|
2026-01-26 13:12:17 +00:00
|
|
|
</Card>
|
2025-12-17 13:07:35 +00:00
|
|
|
|
2026-01-26 13:12:17 +00:00
|
|
|
{/* Quick Stats Grid */}
|
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
<Card variant="glass" className="p-4 flex flex-col items-center justify-center bg-black/20 text-center hover:bg-white/5 transition-colors">
|
|
|
|
|
<Heart className="w-5 h-5 text-magenta-500 mb-1" />
|
|
|
|
|
<span className="text-xl font-bold text-white">{track.like_count}</span>
|
|
|
|
|
<span className="text-xs text-muted-foreground uppercase tracking-wider">Likes</span>
|
|
|
|
|
</Card>
|
|
|
|
|
<Card variant="glass" className="p-4 flex flex-col items-center justify-center bg-black/20 text-center hover:bg-white/5 transition-colors">
|
|
|
|
|
<Play className="w-5 h-5 text-cyan-500 mb-1" />
|
|
|
|
|
<span className="text-xl font-bold text-white">{track.play_count}</span>
|
|
|
|
|
<span className="text-xs text-muted-foreground uppercase tracking-wider">Plays</span>
|
|
|
|
|
</Card>
|
2026-01-07 09:32:53 +00:00
|
|
|
</div>
|
2026-01-26 13:12:17 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Right Column: Details & Content */}
|
|
|
|
|
<div className="lg:col-span-8 space-y-8">
|
|
|
|
|
|
|
|
|
|
{/* Header Info */}
|
|
|
|
|
<div>
|
|
|
|
|
<h1 className="text-4xl md:text-6xl font-display font-bold text-white mb-2 leading-tight">{track.title}</h1>
|
|
|
|
|
<div className="flex flex-wrap items-center gap-4 text-lg md:text-xl text-muted-foreground">
|
|
|
|
|
<span className="text-primary font-medium">{track.artist}</span>
|
|
|
|
|
{track.album && <span>• {track.album}</span>}
|
|
|
|
|
{track.year && <span>• {track.year}</span>}
|
|
|
|
|
</div>
|
2025-12-24 11:57:49 +00:00
|
|
|
|
2026-01-26 13:12:17 +00:00
|
|
|
<div className="flex flex-wrap gap-3 mt-6">
|
|
|
|
|
{track.genre && (
|
|
|
|
|
<span className="px-3 py-1 rounded-full bg-white/5 border border-white/10 text-sm">{track.genre}</span>
|
|
|
|
|
)}
|
|
|
|
|
<span className="px-3 py-1 rounded-full bg-white/5 border border-white/10 text-sm flex items-center gap-1">
|
|
|
|
|
<Clock className="w-3 h-3" /> {formatDuration(track.duration)}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-24 11:57:49 +00:00
|
|
|
|
2026-01-26 13:12:17 +00:00
|
|
|
{/* Waveform */}
|
|
|
|
|
{track.waveform_path && (
|
|
|
|
|
<div className="relative h-24 w-full bg-black/20 rounded-xl border border-white/5 overflow-hidden group">
|
|
|
|
|
<div className="absolute inset-0 bg-gradient-to-r from-cyan-500/10 to-magenta-500/10 opacity-50" />
|
|
|
|
|
<img src={track.waveform_path} alt="Waveform" className="w-full h-full object-cover opacity-60 group-hover:opacity-80 transition-opacity mix-blend-screen" />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-12-24 11:57:49 +00:00
|
|
|
|
2026-01-26 13:12:17 +00:00
|
|
|
{/* Tabs & Content */}
|
|
|
|
|
<Tabs defaultValue="comments" className="w-full">
|
|
|
|
|
<TabsList className="bg-transparent border-b border-white/10 w-full justify-start h-auto p-0 gap-8 mb-6 rounded-none">
|
|
|
|
|
<TabsTrigger value="comments" className="rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent data-[state=active]:border-primary data-[state=active]:text-primary py-3 px-0 text-lg font-display bg-transparent">
|
|
|
|
|
Discussion <span className="ml-2 text-xs bg-white/10 px-2 py-0.5 rounded-full text-muted-foreground align-middle">24</span>
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
<TabsTrigger value="analytics" className="rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent data-[state=active]:border-primary data-[state=active]:text-primary py-3 px-0 text-lg font-display bg-transparent">
|
|
|
|
|
Analytics
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
<TabsTrigger value="history" className="rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent data-[state=active]:border-primary data-[state=active]:text-primary py-3 px-0 text-lg font-display bg-transparent">
|
|
|
|
|
History
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
</TabsList>
|
|
|
|
|
|
|
|
|
|
<TabsContent value="comments" className="animate-fade-in">
|
|
|
|
|
<CommentSection trackId={track.id} />
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
<TabsContent value="analytics" className="animate-fade-in">
|
|
|
|
|
<Card variant="glass" className="p-6">
|
|
|
|
|
<div className="flex items-center gap-3 mb-6">
|
|
|
|
|
<BarChart3 className="w-5 h-5 text-primary" />
|
|
|
|
|
<h3 className="text-xl font-bold">Performance Data</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<TrackStatsDisplay trackId={parseInt(track.id, 10) || 0} variant="horizontal" showLabels={true} />
|
|
|
|
|
</Card>
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
<TabsContent value="history" className="animate-fade-in">
|
|
|
|
|
<Card variant="glass" className="p-6">
|
|
|
|
|
<h3 className="text-xl font-bold mb-6">Version History</h3>
|
|
|
|
|
<TrackHistory trackId={track.id} limit={20} />
|
|
|
|
|
</Card>
|
|
|
|
|
</TabsContent>
|
|
|
|
|
</Tabs>
|
|
|
|
|
|
|
|
|
|
</div>
|
2025-12-17 13:07:35 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-24 11:57:49 +00:00
|
|
|
|
2026-01-26 13:12:17 +00:00
|
|
|
{track && <ShareDialog open={isShareDialogOpen} onClose={() => setIsShareDialogOpen(false)} trackId={track.id} trackTitle={track.title} />}
|
2025-12-17 13:07:35 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|