From 06dc8889b5e5f85db433be80c223629f8be118f2 Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Sun, 21 Dec 2025 15:44:09 -0800 Subject: [PATCH] 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 --- frontend/app/page.tsx | 6 +- frontend/app/performances/page.tsx | 142 ++++++++++++++++++++++++++ frontend/components/layout/navbar.tsx | 3 + 3 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 frontend/app/performances/page.tsx diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 15d343d..c3458e6 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -86,13 +86,13 @@ export default async function Home() {

The ultimate community archive for Goose history.
- Discover shows, rate performances, and connect with fans. + Discover shows, rate performances, and find the heady versions.

- + diff --git a/frontend/app/performances/page.tsx b/frontend/app/performances/page.tsx new file mode 100644 index 0000000..1efed07 --- /dev/null +++ b/frontend/app/performances/page.tsx @@ -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 { + 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 ( +
+
+

+ + Heady Versions +

+

+ The top-rated performances as voted by the community +

+
+ + {performances.length > 0 ? ( +
+ {performances.map((item, index) => ( + + + +
+ {/* Rank */} +
+ {index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : {index + 1}} +
+ + {/* Song & Show Info */} +
+
+ {item.song.title} + {item.performance.youtube_link && ( + + + Video + + )} +
+
+ + + {new Date(item.show.date).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric' + })} + + + + {item.venue.name} + {item.venue.city && ` - ${item.venue.city}`} + {item.venue.state && `, ${item.venue.state}`} + +
+ {item.performance.notes && ( +

+ {item.performance.notes} +

+ )} +
+ + {/* Rating */} +
+
+ + {item.avg_score.toFixed(1)} +
+
+ {item.rating_count} {item.rating_count === 1 ? 'rating' : 'ratings'} +
+
+
+
+
+ + ))} +
+ ) : ( + + + +

No rated performances yet

+

+ Be the first to rate a performance! Browse shows and rate your favorite jams. +

+ + Browse Shows → + +
+
+ )} +
+ ) +} diff --git a/frontend/components/layout/navbar.tsx b/frontend/components/layout/navbar.tsx index 0e705a8..29205ac 100644 --- a/frontend/components/layout/navbar.tsx +++ b/frontend/components/layout/navbar.tsx @@ -45,6 +45,9 @@ export function Navbar() { Songs + + Top Performances + Tours