- Add missing @radix-ui/react-select dependency - Sort tour shows chronologically by date - Add context to review forms (song name, date) - Redesign performance page with distinct visual identity - Update ReviewForm to use RatingInput slider
321 lines
17 KiB
TypeScript
321 lines
17 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 } 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"
|
|
|
|
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">
|
|
{/* 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}
|
|
entityContext={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>
|
|
|
|
{/* 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>
|
|
)
|
|
}
|