feat: Add Heady Version Leaderboard to Song Page (Phase 4-5)
- YouTube embed of #1 rated performance - Top 5 performances ranked with medal icons - Rating + rating count display - YouTube link icons for each performance - Gradient gold styling for heady section
This commit is contained in:
parent
ad2e6a107e
commit
5a8764df05
1 changed files with 97 additions and 1 deletions
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { ArrowLeft, PlayCircle, History, Calendar } from "lucide-react"
|
||||
import { ArrowLeft, PlayCircle, History, Calendar, Trophy, Youtube, Star } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { notFound } from "next/navigation"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
|
|
@ -12,6 +12,7 @@ import { EntityReviews } from "@/components/reviews/entity-reviews"
|
|||
import { SocialWrapper } from "@/components/social/social-wrapper"
|
||||
import { PerformanceList } from "@/components/songs/performance-list"
|
||||
import { SongEvolutionChart } from "@/components/songs/song-evolution-chart"
|
||||
import { YouTubeEmbed } from "@/components/ui/youtube-embed"
|
||||
|
||||
async function getSong(id: string) {
|
||||
try {
|
||||
|
|
@ -24,6 +25,15 @@ async function getSong(id: string) {
|
|||
}
|
||||
}
|
||||
|
||||
// Get top rated performances for "Heady Version" leaderboard
|
||||
function getHeadyVersions(performances: any[]) {
|
||||
if (!performances || performances.length === 0) return []
|
||||
return [...performances]
|
||||
.filter(p => p.avg_rating && p.rating_count > 0)
|
||||
.sort((a, b) => b.avg_rating - a.avg_rating)
|
||||
.slice(0, 5)
|
||||
}
|
||||
|
||||
export default async function SongDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
const song = await getSong(id)
|
||||
|
|
@ -32,6 +42,9 @@ export default async function SongDetailPage({ params }: { params: Promise<{ id:
|
|||
notFound()
|
||||
}
|
||||
|
||||
const headyVersions = getHeadyVersions(song.performances || [])
|
||||
const topPerformance = headyVersions[0]
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
|
|
@ -100,6 +113,89 @@ export default async function SongDetailPage({ params }: { params: Promise<{ id:
|
|||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Heady Version Section */}
|
||||
{headyVersions.length > 0 && (
|
||||
<Card className="border-2 border-yellow-500/20 bg-gradient-to-br from-yellow-50/50 to-orange-50/50 dark:from-yellow-900/10 dark:to-orange-900/10">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-yellow-700 dark:text-yellow-400">
|
||||
<Trophy className="h-6 w-6" />
|
||||
Heady Version Leaderboard
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Top Performance with YouTube */}
|
||||
{topPerformance && (
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
{topPerformance.youtube_link ? (
|
||||
<YouTubeEmbed url={topPerformance.youtube_link} />
|
||||
) : song.youtube_link ? (
|
||||
<YouTubeEmbed url={song.youtube_link} />
|
||||
) : (
|
||||
<div className="aspect-video bg-muted rounded-lg flex items-center justify-center">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<Youtube className="h-12 w-12 mx-auto mb-2 opacity-30" />
|
||||
<p className="text-sm">No video available</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className="bg-yellow-500 text-yellow-900">🏆 #1 Heady</Badge>
|
||||
</div>
|
||||
<p className="font-bold text-lg">
|
||||
{topPerformance.show?.date ? new Date(topPerformance.show.date).toLocaleDateString() : "Unknown Date"}
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
{topPerformance.show?.venue?.name || "Unknown Venue"}
|
||||
</p>
|
||||
<div className="flex items-center gap-1 text-yellow-600">
|
||||
<Star className="h-5 w-5 fill-current" />
|
||||
<span className="font-bold text-xl">{topPerformance.avg_rating?.toFixed(1)}</span>
|
||||
<span className="text-sm text-muted-foreground">({topPerformance.rating_count} ratings)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Leaderboard List */}
|
||||
<div className="space-y-2">
|
||||
{headyVersions.map((perf, index) => (
|
||||
<div
|
||||
key={perf.id}
|
||||
className={`flex items-center justify-between p-3 rounded-lg ${index === 0 ? 'bg-yellow-100/50 dark:bg-yellow-900/20' : 'bg-background/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-6 text-center font-bold">
|
||||
{index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : `${index + 1}.`}
|
||||
</span>
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
{perf.show?.date ? new Date(perf.show.date).toLocaleDateString() : "Unknown"}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{perf.show?.venue?.name || "Unknown Venue"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{perf.youtube_link && (
|
||||
<a href={perf.youtube_link} target="_blank" rel="noopener noreferrer">
|
||||
<Youtube className="h-4 w-4 text-red-500" />
|
||||
</a>
|
||||
)}
|
||||
<div className="text-right">
|
||||
<span className="font-bold">{perf.avg_rating?.toFixed(1)}★</span>
|
||||
<span className="text-xs text-muted-foreground ml-1">({perf.rating_count})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<SongEvolutionChart performances={song.performances || []} />
|
||||
|
||||
{/* Performance List Component (Handles Client Sorting) */}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue