veza/apps/web/src/features/playlists/pages/PlaylistDetailPage.tsx
2026-02-07 06:09:51 +01:00

269 lines
13 KiB
TypeScript

import { useState } from 'react';
import { useParams, Link } from 'react-router-dom';
import { usePlaylist } from '../hooks/usePlaylist';
import { useQuery } from '@tanstack/react-query';
import { playlistsApi } from '@/services/api/playlists';
import { usePlaylistPermissions } from '../hooks/usePlaylistPermissions';
import { PlaylistActions } from '../components/playlist-actions';
import { PlaylistTrackList } from '../components/PlaylistTrackList';
import { AddTrackToPlaylistModal } from '../components/AddTrackToPlaylistModal';
import { CollaboratorList } from '../components/CollaboratorList';
import { PlaylistRecommendations } from '../components/PlaylistRecommendations';
import { SharePlaylistModal } from '../components/SharePlaylistModal';
import { AddCollaboratorModal } from '../components/AddCollaboratorModal';
import { Button } from '@/components/ui/button';
import { LoadingSpinner } from '@/components/ui/loading-spinner';
import { Card } from '@/components/ui/card';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { Plus, Users, Sparkles, Music, Calendar, Search, Play, Shuffle } from 'lucide-react';
import toast from '@/utils/toast';
import type { Track } from '../types';
import { Avatar } from '@/components/ui/avatar';
import { format } from 'date-fns';
import { cn } from '@/lib/utils';
import { PlaylistFollowButton } from '../components/PlaylistFollowButton';
export function PlaylistDetailPage() {
const { id } = useParams<{ id: string }>();
const [isAddTrackModalOpen, setIsAddTrackModalOpen] = useState(false);
const [isShareModalOpen, setIsShareModalOpen] = useState(false);
const [isAddCollaboratorModalOpen, setIsAddCollaboratorModalOpen] = useState(false);
const { data: playlist, isLoading, error, refetch } = usePlaylist(id || '');
const permissions = usePlaylistPermissions(playlist);
const { data: collaborators, refetch: refetchCollaborators } = useQuery({
queryKey: ['playlistCollaborators', id],
queryFn: () => playlistsApi.getCollaborators(id || ''),
enabled: !!id && permissions.canRead,
});
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<LoadingSpinner />
</div>
);
}
if (error || !playlist) {
return (
<div className="container mx-auto px-4 py-8 flex flex-col items-center justify-center min-h-layout-page text-center">
<div className="text-9xl mb-4">👾</div>
<h2 className="text-3xl font-display font-bold text-destructive mb-2">Playlist Not Found</h2>
<Button variant="outline" className="mt-8" asChild>
<Link to="/features/library">Back to Library</Link>
</Button>
</div>
);
}
const tracks = (playlist.tracks?.map((pt) => pt.track).filter((t) => !!t) as Track[]) || [];
const playlistTracks = playlist.tracks || [];
const handleTrackAdded = () => { setIsAddTrackModalOpen(false); refetch(); toast.success('Track added'); };
const handleTrackRemoved = () => { refetch(); toast.success('Track removed'); };
const handleTracksReordered = () => { refetch(); toast.success('Reordered'); };
return (
<div className="min-h-screen pb-24">
{/* 1. Cinematic Hero Banner */}
<div className="relative h-80 md:h-96 w-full overflow-hidden">
{/* Dynamic Background */}
<div className="absolute inset-0 bg-gradient-to-br from-cyan-900/50 via-black to-magenta-900/50" />
{playlist.cover_url && (
<div className="absolute inset-0 opacity-30 blur-3xl scale-110"
style={{ backgroundImage: `url(${playlist.cover_url})`, backgroundSize: 'cover', backgroundPosition: 'center' }}
/>
)}
<div className="absolute inset-0 bg-gradient-to-t from-background via-transparent to-transparent" />
{/* Abstract Shapes/Noise */}
<div className="absolute inset-0 noise opacity-30" />
</div>
<div className="container mx-auto px-4 md:px-8 relative -mt-40 z-10">
<div className="flex flex-col md:flex-row gap-8 items-end">
{/* Cover Art Card */}
<Card variant="glass" className="w-52 h-52 md:w-64 md:h-64 flex-shrink-0 p-2 border-white/10 shadow-[0_20px_50px_rgba(0,0,0,0.5)] bg-black/40 backdrop-blur-3xl overflow-hidden rounded-2xl group">
{playlist.cover_url ? (
<img
src={playlist.cover_url}
alt={playlist.title}
className="w-full h-full object-cover rounded-xl shadow-inner group-hover:scale-105 transition-transform duration-700"
/>
) : (
<div className="w-full h-full bg-gradient-to-br from-cyan-900 to-magenta-900 rounded-xl flex items-center justify-center">
<Music className="w-20 h-20 text-white/20" />
</div>
)}
</Card>
{/* Playlist Info */}
<div className="flex-1 pb-4">
<div className="flex items-center gap-2 mb-2">
<span className={cn(
"px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider border",
playlist.is_public ? "bg-cyan-500/10 text-cyan-500 border-cyan-500/20" : "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
)}>
{playlist.is_public ? "Public Signal" : "Encrypted"}
</span>
<span className="text-xs text-muted-foreground/80 font-mono flex items-center gap-1">
<Calendar className="w-3 h-3" /> Updated {format(new Date(playlist.updated_at), 'MMM d, yyyy')}
</span>
</div>
<h1 className="text-4xl md:text-6xl font-display font-bold text-white mb-4 tracking-tight drop-shadow-lg">
{playlist.title}
</h1>
{playlist.description && (
<p className="text-lg text-white/70 max-w-2xl font-light leading-relaxed mb-6 font-sans">
{playlist.description}
</p>
)}
<div className="flex items-center gap-4 text-sm md:text-base">
<div className="flex items-center gap-2 text-white/90">
<Avatar className="w-6 h-6 border border-white/20" fallback="U" src={playlist.user?.avatar_url} />
<span className="font-semibold">{playlist.user?.username}</span>
</div>
<span className="text-white/30"></span>
<span className="text-white/80">{playlist.track_count} tracks</span>
<span className="text-white/30"></span>
<div className="flex items-center gap-2">
<Users className="w-4 h-4 text-muted-foreground" />
{/* Casting since follower_count often missing in strict type */}
<span className="text-white/80">{(playlist as any).follower_count || 0} followers</span>
</div>
</div>
</div>
</div>
{/* Actions Bar */}
<div className="mt-8 flex flex-wrap items-center gap-4 mb-8">
<Button size="lg" className="rounded-full h-14 px-8 text-lg font-bold shadow-[0_0_30px_rgba(var(--primary),0.4)] hover:shadow-[0_0_50px_rgba(var(--primary),0.6)] hover:scale-105 transition-all bg-primary text-primary-foreground">
<Play className="w-5 h-5 mr-2 fill-current" /> Play All
</Button>
<Button size="lg" variant="outline" className="rounded-full h-14 px-6 border-white/10 hover:bg-white/5 backdrop-blur-sm">
<Shuffle className="w-5 h-5 mr-2" /> Shuffle
</Button>
<div className="flex-1" />
<div className="flex items-center gap-2">
<PlaylistFollowButton playlistId={playlist.id} initialFollowerCount={(playlist as any).follower_count} initialFollowing={(playlist as any).is_following} />
<PlaylistActions
playlist={playlist}
onUpdated={refetch}
onShareClick={() => setIsShareModalOpen(true)}
canShare={permissions.canRead}
/>
</div>
</div>
{/* Content Tabs */}
<Tabs defaultValue="tracks" className="w-full">
<TabsList className="bg-transparent border-b border-white/10 w-full justify-start h-auto p-0 rounded-none gap-8 mb-6">
<TabsTrigger value="tracks" className="rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent data-[state=active]:border-primary data-[state=active]:text-primary py-4 px-0 text-lg font-display bg-transparent">
Tracks
</TabsTrigger>
{permissions.canRead && (
<TabsTrigger value="collaborators" className="rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent data-[state=active]:border-primary data-[state=active]:text-primary py-4 px-0 text-lg font-display bg-transparent">
Collaborators
</TabsTrigger>
)}
<TabsTrigger value="recommendations" className="rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent data-[state=active]:border-primary data-[state=active]:text-primary py-4 px-0 text-lg font-display bg-transparent">
Recommendations
</TabsTrigger>
</TabsList>
<TabsContent value="tracks">
<Card variant="glass" className="overflow-hidden border-white/5">
<div className="p-4 border-b border-white/5 flex justify-between items-center bg-black/20">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<input placeholder="Filter tracks..." className="bg-transparent border-none text-sm text-foreground placeholder:text-muted-foreground focus:outline-none pl-9 py-2 w-64" />
</div>
{permissions.canAddTracks && (
<Button size="sm" onClick={() => setIsAddTrackModalOpen(true)} variant="ghost" className="text-primary hover:text-primary hover:bg-primary/10">
<Plus className="w-4 h-4 mr-2" /> Add Tracks
</Button>
)}
</div>
<div className="p-0">
<PlaylistTrackList
playlistTracks={playlistTracks}
tracks={tracks}
playlistId={playlist.id}
onTrackRemoved={handleTrackRemoved}
onTracksReordered={handleTracksReordered}
enableDragAndDrop={permissions.canEdit}
canRemoveTracks={permissions.canRemoveTracks}
className="divide-y divide-white/5"
/>
</div>
</Card>
</TabsContent>
<TabsContent value="collaborators">
<Card variant="glass" className="p-6">
<div className="flex justify-between items-center mb-6">
<h3 className="text-xl font-bold flex items-center gap-2"><Users className="w-5 h-5 text-primary" /> Squad Members</h3>
{permissions.canManageCollaborators && (
<Button onClick={() => setIsAddCollaboratorModalOpen(true)}>
<Plus className="w-4 h-4 mr-2" /> Invite
</Button>
)}
</div>
<CollaboratorList
collaborators={collaborators || []}
playlistId={playlist.id}
canManage={permissions.canManageCollaborators}
/>
</Card>
</TabsContent>
<TabsContent value="recommendations">
<div className="bg-gradient-to-br from-primary/10 to-transparent p-6 rounded-2xl border border-primary/20">
<div className="flex items-center gap-2 mb-6">
<Sparkles className="w-5 h-5 text-yellow-400 animate-pulse" />
<h3 className="text-xl font-bold">Suggested for you</h3>
</div>
<PlaylistRecommendations
limit={8}
minScore={0.1}
includeOwn={false}
onPlaylistClick={(recPlaylist) => window.location.href = `/playlists/${recPlaylist.id}`}
/>
</div>
</TabsContent>
</Tabs>
<AddTrackToPlaylistModal
open={isAddTrackModalOpen}
onClose={() => setIsAddTrackModalOpen(false)}
playlistId={playlist.id}
onTracksAdded={handleTrackAdded}
/>
{playlist && (
<SharePlaylistModal
open={isShareModalOpen}
onClose={() => setIsShareModalOpen(false)}
playlistId={playlist.id}
/>
)}
{playlist && (
<AddCollaboratorModal
open={isAddCollaboratorModalOpen}
onClose={() => setIsAddCollaboratorModalOpen(false)}
playlistId={playlist.id}
onAdded={() => { refetchCollaborators(); refetch(); }}
/>
)}
</div>
</div>
);
}