elmeg-demo/frontend/app/performances/[id]/page.tsx
fullsizemalt 735fd1a6ea
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run
style: Move video into bento grid left column for better layout flow
2025-12-23 00:37:34 -08:00

381 lines
21 KiB
TypeScript

import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { ArrowLeft, Calendar, MapPin, ChevronRight, ChevronLeft, Music, Clock, Hash, Play, ExternalLink, Sparkles, Youtube } from "lucide-react"
import Link from "next/link"
import { notFound } from "next/navigation"
import { getApiUrl } from "@/lib/api-config"
import { CommentSection } from "@/components/social/comment-section"
import { EntityReviews } from "@/components/reviews/entity-reviews"
import { SocialWrapper } from "@/components/social/social-wrapper"
import { EntityRating } from "@/components/social/entity-rating"
import { Badge } from "@/components/ui/badge"
import { YouTubeEmbed } from "@/components/ui/youtube-embed"
async function getPerformance(id: string) {
try {
const res = await fetch(`${getApiUrl()}/performances/${id}`, { cache: 'no-store' })
if (!res.ok) return null
return res.json()
} catch (e) {
console.error(e)
return null
}
}
export default async function PerformanceDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const performance = await getPerformance(id)
if (!performance) {
notFound()
}
const showDate = new Date(performance.show.date)
const formattedDate = showDate.toLocaleDateString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric"
})
return (
<div className="flex flex-col gap-6">
{/* Hero Banner - Distinct from Song page */}
<div className="relative -mx-4 -mt-4 px-4 pt-6 pb-8 bg-gradient-to-br from-primary/10 via-primary/5 to-transparent border-b">
{/* Breadcrumbs */}
<nav className="flex items-center gap-1 text-sm text-muted-foreground mb-4">
<Link href={`/shows/${performance.show.id}`} className="hover:text-foreground transition-colors">
Show
</Link>
<ChevronRight className="h-4 w-4" />
<Link
href={`/songs/${performance.song.id}`}
className="hover:text-foreground transition-colors"
>
{performance.song.title}
</Link>
<ChevronRight className="h-4 w-4" />
<span className="text-foreground font-medium">This Performance</span>
</nav>
<div className="flex flex-col md:flex-row md:items-start md:justify-between gap-6">
<div className="flex items-start gap-4">
<Link href={`/shows/${performance.show.id}`}>
<Button variant="outline" size="icon" className="mt-1">
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<div>
{/* Context Badge */}
<div className="flex items-center gap-2 mb-2">
<Badge variant="secondary" className="bg-primary/10 text-primary border-primary/20">
<Sparkles className="h-3 w-3 mr-1" />
Specific Performance
</Badge>
{performance.set_name && (
<Badge variant="outline">{performance.set_name}</Badge>
)}
{performance.position && (
<Badge variant="outline" className="font-mono">
#{performance.position}
</Badge>
)}
</div>
{/* Song Title (links to song page) */}
<h1 className="text-3xl md:text-4xl font-bold tracking-tight">
<Link
href={`/songs/${performance.song.id}`}
className="hover:text-primary transition-colors"
>
{performance.song.title}
</Link>
</h1>
{/* Nicknames */}
{performance.nicknames && performance.nicknames.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{performance.nicknames.map((nick: any) => (
<span
key={nick.id}
className="text-lg italic text-yellow-600 dark:text-yellow-400"
title={nick.description}
>
"{nick.nickname}"
</span>
))}
</div>
)}
{/* Show Context - THE KEY DIFFERENTIATOR */}
<div className="mt-4 p-3 rounded-lg bg-background/80 border inline-flex flex-col gap-1">
<Link
href={`/shows/${performance.show.id}`}
className="font-semibold text-lg hover:text-primary transition-colors flex items-center gap-2"
>
<Calendar className="h-4 w-4" />
{formattedDate}
</Link>
{performance.show.venue && (
<Link
href={`/venues/${performance.show.venue.id}`}
className="text-muted-foreground hover:text-foreground flex items-center gap-2 transition-colors"
>
<MapPin className="h-4 w-4" />
{performance.show.venue.name}, {performance.show.venue.city}
{performance.show.venue.state && `, ${performance.show.venue.state}`}
</Link>
)}
</div>
</div>
</div>
{/* Rating Box */}
<div className="md:text-right">
<div className="text-xs uppercase font-medium text-muted-foreground mb-2">
Rate This Version
</div>
<SocialWrapper type="ratings">
<EntityRating entityType="performance" entityId={performance.id} />
</SocialWrapper>
</div>
</div>
</div>
<div className="grid gap-6 md:grid-cols-[1fr_300px]">
<div className="flex flex-col gap-6">
{/* Video Section - Show when performance has a video */}
{performance.youtube_link && (
<Card className="border-2 border-red-500/20 bg-gradient-to-br from-red-500/5 to-transparent">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<Youtube className="h-4 w-4 text-red-500" />
Video
</CardTitle>
</CardHeader>
<CardContent>
<YouTubeEmbed
url={performance.youtube_link}
title={`${performance.song.title} - ${formattedDate}`}
/>
</CardContent>
</Card>
)}
{/* Version Navigation - Prominent */}
<Card className="border-2">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<Clock className="h-4 w-4" />
Version Timeline
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between gap-4">
{performance.previous_performance_id ? (
<Link
href={`/performances/${performance.previous_performance_id}`}
className="flex-1"
>
<Button variant="outline" className="w-full justify-start gap-2">
<ChevronLeft className="h-4 w-4" />
<div className="text-left">
<div className="text-xs text-muted-foreground">Previous</div>
<div className="font-medium">Earlier Version</div>
</div>
</Button>
</Link>
) : (
<div className="flex-1 p-3 rounded-md bg-muted/50 text-center text-sm text-muted-foreground">
🎉 Debut Performance
</div>
)}
<div className="text-center px-4">
<div className="text-2xl font-bold">#{performance.times_played || "?"}</div>
<div className="text-xs text-muted-foreground">of all time</div>
</div>
{performance.next_performance_id ? (
<Link
href={`/performances/${performance.next_performance_id}`}
className="flex-1"
>
<Button variant="outline" className="w-full justify-end gap-2">
<div className="text-right">
<div className="text-xs text-muted-foreground">Next</div>
<div className="font-medium">Later Version</div>
</div>
<ChevronRight className="h-4 w-4" />
</Button>
</Link>
) : (
<div className="flex-1 p-3 rounded-md bg-muted/50 text-center text-sm text-muted-foreground">
Most Recent 🕐
</div>
)}
</div>
</CardContent>
</Card>
{/* Notes & Details */}
{(performance.notes || performance.segue || performance.track_url) && (
<Card>
<CardHeader>
<CardTitle>About This Performance</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{performance.notes && (
<div>
<h3 className="font-medium text-sm mb-1 text-muted-foreground">Notes</h3>
<p className="text-foreground">{performance.notes}</p>
</div>
)}
{performance.segue && (
<div className="flex items-center gap-2 p-2 rounded-md bg-primary/10 text-primary">
<Music className="h-4 w-4" />
<span className="font-medium">Segues into next song </span>
</div>
)}
{performance.track_url && (
<a
href={performance.track_url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-primary hover:underline"
>
<Play className="h-4 w-4" />
Listen to this performance
<ExternalLink className="h-3 w-3" />
</a>
)}
</CardContent>
</Card>
)}
{/* Comments */}
<SocialWrapper type="comments">
<CommentSection entityType="performance" entityId={performance.id} />
</SocialWrapper>
{/* Reviews */}
<SocialWrapper type="reviews">
<EntityReviews
entityType="performance"
entityId={performance.id}
entityName={`${performance.song.title} - ${formattedDate}`}
/>
</SocialWrapper>
</div>
{/* Sidebar */}
<div className="flex flex-col gap-4">
{/* Quick Stats */}
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Performance Stats
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-muted-foreground">Set Position</span>
<span className="font-mono font-bold">
{performance.set_name || "—"} #{performance.position || "—"}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-muted-foreground">Gap Since Last</span>
<span className="font-mono font-bold">
{performance.gap !== undefined ? `${performance.gap} shows` : "—"}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-muted-foreground">Times Played</span>
<span className="font-mono font-bold">
{performance.times_played || "—"}
</span>
</div>
</CardContent>
</Card>
{/* Top Rated Versions */}
{performance.other_performances && performance.other_performances.length > 0 && (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<Sparkles className="h-4 w-4 text-yellow-500" />
Top Rated Versions
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{performance.other_performances.slice(0, 5).map((perf: any) => (
<Link
key={perf.id}
href={`/performances/${perf.slug || perf.id}`}
className="flex items-start justify-between group"
>
<div className="flex flex-col">
<span className="font-medium group-hover:text-primary transition-colors text-sm">
{new Date(perf.show_date).toLocaleDateString()}
</span>
<span className="text-xs text-muted-foreground">
{perf.venue_name}
</span>
</div>
{perf.avg_rating > 0 && (
<div className="flex items-center gap-1 bg-secondary px-1.5 py-0.5 rounded text-xs font-mono">
<span className="text-yellow-500 text-[10px]"></span>
<span>{perf.avg_rating.toFixed(1)}</span>
</div>
)}
</Link>
))}
<Link
href={`/songs/${performance.song.id}`}
className="block text-xs text-center text-muted-foreground hover:text-primary pt-2 border-t mt-2"
>
View all {performance.other_performances.length + 1} versions
</Link>
</CardContent>
</Card>
)}
{/* Quick Links */}
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Related Pages
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<Link
href={`/songs/${performance.song.id}`}
className="flex items-center gap-2 p-2 rounded-md hover:bg-muted transition-colors"
>
<Music className="h-4 w-4 text-muted-foreground" />
<span>All versions of {performance.song.title}</span>
</Link>
<Link
href={`/shows/${performance.show.id}`}
className="flex items-center gap-2 p-2 rounded-md hover:bg-muted transition-colors"
>
<Calendar className="h-4 w-4 text-muted-foreground" />
<span>Full show setlist</span>
</Link>
{performance.show.venue && (
<Link
href={`/venues/${performance.show.venue.id}`}
className="flex items-center gap-2 p-2 rounded-md hover:bg-muted transition-colors"
>
<MapPin className="h-4 w-4 text-muted-foreground" />
<span>{performance.show.venue.name}</span>
</Link>
)}
</CardContent>
</Card>
</div>
</div>
</div>
)
}