feat: Add Heady Versions (performances) page
- /performances page with top-rated performance leaderboard - Added to Browse dropdown in navbar - Updated home page CTA to feature Heady Versions - Medal icons for top 3 performances
This commit is contained in:
parent
3987b64209
commit
06dc8889b5
3 changed files with 148 additions and 3 deletions
|
|
@ -86,13 +86,13 @@ export default async function Home() {
|
||||||
<p className="max-w-[600px] text-lg text-muted-foreground">
|
<p className="max-w-[600px] text-lg text-muted-foreground">
|
||||||
The ultimate community archive for Goose history.
|
The ultimate community archive for Goose history.
|
||||||
<br />
|
<br />
|
||||||
Discover shows, rate performances, and connect with fans.
|
Discover shows, rate performances, and find the <strong>heady versions</strong>.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<Link href="/leaderboards">
|
<Link href="/performances">
|
||||||
<Button size="lg" className="gap-2">
|
<Button size="lg" className="gap-2">
|
||||||
<Trophy className="h-4 w-4" />
|
<Trophy className="h-4 w-4" />
|
||||||
View Leaderboards
|
Heady Versions
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/shows">
|
<Link href="/shows">
|
||||||
|
|
|
||||||
142
frontend/app/performances/page.tsx
Normal file
142
frontend/app/performances/page.tsx
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { Trophy, Star, Calendar, MapPin, ExternalLink } from "lucide-react"
|
||||||
|
import Link from "next/link"
|
||||||
|
import { getApiUrl } from "@/lib/api-config"
|
||||||
|
|
||||||
|
interface TopPerformance {
|
||||||
|
performance: {
|
||||||
|
id: number
|
||||||
|
position: number
|
||||||
|
set_name: string
|
||||||
|
notes?: string
|
||||||
|
youtube_link?: string
|
||||||
|
}
|
||||||
|
song: {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
show: {
|
||||||
|
id: number
|
||||||
|
date: string
|
||||||
|
}
|
||||||
|
venue: {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
city: string
|
||||||
|
state?: string
|
||||||
|
}
|
||||||
|
avg_score: number
|
||||||
|
rating_count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTopPerformances(): Promise<TopPerformance[]> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${getApiUrl()}/leaderboards/performances/top?limit=50`, {
|
||||||
|
cache: 'no-store'
|
||||||
|
})
|
||||||
|
if (!res.ok) return []
|
||||||
|
return res.json()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch top performances:', e)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function PerformancesPage() {
|
||||||
|
const performances = await getTopPerformances()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<h1 className="text-4xl font-bold tracking-tight flex items-center justify-center gap-3">
|
||||||
|
<Trophy className="h-10 w-10 text-yellow-500" />
|
||||||
|
Heady Versions
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground mt-2 text-lg">
|
||||||
|
The top-rated performances as voted by the community
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{performances.length > 0 ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{performances.map((item, index) => (
|
||||||
|
<Link key={item.performance.id} href={`/performances/${item.performance.id}`}>
|
||||||
|
<Card className={`hover:bg-accent/50 transition-colors cursor-pointer ${index === 0 ? 'border-2 border-yellow-500 bg-gradient-to-r from-yellow-50 to-orange-50 dark:from-yellow-900/10 dark:to-orange-900/10' :
|
||||||
|
index === 1 ? 'border-gray-300 dark:border-gray-600' :
|
||||||
|
index === 2 ? 'border-amber-600 dark:border-amber-700' : ''
|
||||||
|
}`}>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{/* Rank */}
|
||||||
|
<div className="text-3xl font-bold w-12 text-center shrink-0">
|
||||||
|
{index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : <span className="text-muted-foreground">{index + 1}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Song & Show Info */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<span className="font-bold text-lg truncate">{item.song.title}</span>
|
||||||
|
{item.performance.youtube_link && (
|
||||||
|
<Badge variant="outline" className="text-red-500 border-red-300">
|
||||||
|
<ExternalLink className="h-3 w-3 mr-1" />
|
||||||
|
Video
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4 text-sm text-muted-foreground mt-1">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Calendar className="h-4 w-4" />
|
||||||
|
{new Date(item.show.date).toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric'
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<MapPin className="h-4 w-4" />
|
||||||
|
{item.venue.name}
|
||||||
|
{item.venue.city && ` - ${item.venue.city}`}
|
||||||
|
{item.venue.state && `, ${item.venue.state}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{item.performance.notes && (
|
||||||
|
<p className="text-xs text-primary mt-1 italic">
|
||||||
|
{item.performance.notes}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Rating */}
|
||||||
|
<div className="text-right shrink-0">
|
||||||
|
<div className="flex items-center gap-1 text-yellow-600">
|
||||||
|
<Star className="h-5 w-5 fill-current" />
|
||||||
|
<span className="font-bold text-xl">{item.avg_score.toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{item.rating_count} {item.rating_count === 1 ? 'rating' : 'ratings'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Card className="p-12 text-center">
|
||||||
|
<CardContent>
|
||||||
|
<Trophy className="h-16 w-16 mx-auto mb-4 text-muted-foreground/30" />
|
||||||
|
<h2 className="text-xl font-bold">No rated performances yet</h2>
|
||||||
|
<p className="text-muted-foreground mt-2">
|
||||||
|
Be the first to rate a performance! Browse shows and rate your favorite jams.
|
||||||
|
</p>
|
||||||
|
<Link href="/shows" className="text-primary hover:underline mt-4 inline-block">
|
||||||
|
Browse Shows →
|
||||||
|
</Link>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -45,6 +45,9 @@ export function Navbar() {
|
||||||
<Link href="/songs">
|
<Link href="/songs">
|
||||||
<DropdownMenuItem>Songs</DropdownMenuItem>
|
<DropdownMenuItem>Songs</DropdownMenuItem>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link href="/performances">
|
||||||
|
<DropdownMenuItem>Top Performances</DropdownMenuItem>
|
||||||
|
</Link>
|
||||||
<Link href="/tours">
|
<Link href="/tours">
|
||||||
<DropdownMenuItem>Tours</DropdownMenuItem>
|
<DropdownMenuItem>Tours</DropdownMenuItem>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue