204 lines
9.4 KiB
TypeScript
204 lines
9.4 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useState } from "react"
|
|
import { useParams, useRouter } from "next/navigation"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
|
import { ArrowLeft, Trash2, Calendar, Music, User as UserIcon, PlayCircle, MoreHorizontal } from "lucide-react"
|
|
import Link from "next/link"
|
|
import { getApiUrl } from "@/lib/api-config"
|
|
import { useToast } from "@/components/ui/use-toast"
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu"
|
|
|
|
export default function PlaylistDetailPage() {
|
|
const params = useParams()
|
|
const router = useRouter()
|
|
const { toast } = useToast()
|
|
const [playlist, setPlaylist] = useState<any>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [currentUser, setCurrentUser] = useState<any>(null)
|
|
|
|
useEffect(() => {
|
|
const token = localStorage.getItem("token")
|
|
|
|
// Fetch current user
|
|
if (token) {
|
|
fetch(`${getApiUrl()}/auth/users/me`, {
|
|
headers: { Authorization: `Bearer ${token}` }
|
|
})
|
|
.then(res => res.ok ? res.json() : null)
|
|
.then(data => setCurrentUser(data))
|
|
}
|
|
|
|
// Fetch playlist
|
|
fetch(`${getApiUrl()}/playlists/${params.id}`, {
|
|
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
|
})
|
|
.then(res => {
|
|
if (!res.ok) throw new Error("Failed to fetch playlist")
|
|
return res.json()
|
|
})
|
|
.then(data => setPlaylist(data))
|
|
.catch(err => {
|
|
console.error(err)
|
|
toast({
|
|
title: "Error",
|
|
description: "Could not load playlist",
|
|
variant: "destructive"
|
|
})
|
|
})
|
|
.finally(() => setLoading(false))
|
|
}, [params.id])
|
|
|
|
const handleDeletePlaylist = async () => {
|
|
if (!confirm("Are you sure you want to delete this playlist?")) return
|
|
|
|
const token = localStorage.getItem("token")
|
|
try {
|
|
const res = await fetch(`${getApiUrl()}/playlists/${params.id}`, {
|
|
method: "DELETE",
|
|
headers: { Authorization: `Bearer ${token}` }
|
|
})
|
|
|
|
if (res.ok) {
|
|
toast({ title: "Playlist deleted" })
|
|
router.push("/profile")
|
|
} else {
|
|
throw new Error("Failed to delete")
|
|
}
|
|
} catch (error) {
|
|
toast({ title: "Error", description: "Could not delete playlist", variant: "destructive" })
|
|
}
|
|
}
|
|
|
|
const handleRemoveTrack = async (performanceId: number) => {
|
|
const token = localStorage.getItem("token")
|
|
try {
|
|
const res = await fetch(`${getApiUrl()}/playlists/${params.id}/performances/${performanceId}`, {
|
|
method: "DELETE",
|
|
headers: { Authorization: `Bearer ${token}` }
|
|
})
|
|
|
|
if (res.ok) {
|
|
// Optimistic update
|
|
setPlaylist((prev: any) => ({
|
|
...prev,
|
|
performances: prev.performances.filter((p: any) => p.performance_id !== performanceId)
|
|
}))
|
|
toast({ title: "Track removed" })
|
|
}
|
|
} catch (error) {
|
|
toast({ title: "Error", description: "Could not remove track", variant: "destructive" })
|
|
}
|
|
}
|
|
|
|
if (loading) return <div className="container py-20 text-center">Loading playlist...</div>
|
|
if (!playlist) return <div className="container py-20 text-center">Playlist not found</div>
|
|
|
|
const isOwner = currentUser && currentUser.id === playlist.user_id
|
|
|
|
return (
|
|
<div className="container py-10 max-w-4xl space-y-8">
|
|
<Link href="/profile" className="flex items-center text-muted-foreground hover:text-foreground mb-4">
|
|
<ArrowLeft className="mr-2 h-4 w-4" /> Back to Profile
|
|
</Link>
|
|
|
|
<div className="flex flex-col md:flex-row justify-between items-start gap-4">
|
|
<div>
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<h1 className="text-3xl font-bold tracking-tight">{playlist.name}</h1>
|
|
{!playlist.is_public && (
|
|
<span className="text-xs uppercase font-bold tracking-wider bg-muted text-muted-foreground px-2 py-1 rounded">Private</span>
|
|
)}
|
|
</div>
|
|
<p className="text-muted-foreground text-lg mb-4">{playlist.description}</p>
|
|
|
|
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
|
<div className="flex items-center gap-1">
|
|
<UserIcon className="h-4 w-4" />
|
|
<span>{playlist.username}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Calendar className="h-4 w-4" />
|
|
<span>{new Date(playlist.created_at).toLocaleDateString()}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Music className="h-4 w-4" />
|
|
<span>{playlist.performances.length} tracks</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{isOwner && (
|
|
<Button variant="destructive" size="sm" onClick={handleDeletePlaylist} className="gap-2">
|
|
<Trash2 className="h-4 w-4" /> Delete Playlist
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Tracks</CardTitle>
|
|
<CardDescription>
|
|
{playlist.performances.length === 0 ? "No tracks added yet." : "Performances in this collection."}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
{playlist.performances.length > 0 && (
|
|
<div className="divide-y">
|
|
{playlist.performances.map((perf: any, index: number) => (
|
|
<div key={perf.performance_id} className="p-4 flex items-center justify-between hover:bg-muted/30 transition-colors">
|
|
<div className="flex items-center gap-4">
|
|
<span className="text-muted-foreground font-mono w-6 text-center">{index + 1}</span>
|
|
<div>
|
|
<p className="font-medium">{perf.song_title}</p>
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<span>{new Date(perf.show_date).toLocaleDateString()}</span>
|
|
{perf.notes && (
|
|
<span className="italic text-muted-foreground/70">- {perf.notes}</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
{perf.show_slug && (
|
|
<Link href={`/shows/${perf.show_slug}`}>
|
|
<Button size="icon" variant="ghost" title="Go to Show">
|
|
<PlayCircle className="h-4 w-4" />
|
|
</Button>
|
|
</Link>
|
|
)}
|
|
|
|
{isOwner && (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button size="icon" variant="ghost">
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem
|
|
className="text-destructive focus:text-destructive"
|
|
onClick={() => handleRemoveTrack(perf.performance_id)}
|
|
>
|
|
Remove from Playlist
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|