fediversion/frontend/app/playlists/[id]/page.tsx
fullsizemalt 7b8ba4b54c
Some checks failed
Deploy Fediversion / deploy (push) Failing after 1s
feat: User Personalization, Playlists, Recommendations, and DSO Importer
2025-12-29 16:28:43 -08:00

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