From ff411321c6ceb351cef39bde798f134ef45d3010 Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Wed, 24 Dec 2025 10:29:13 -0800 Subject: [PATCH] feat(stats): add set breakdown to song page --- backend/schemas.py | 3 +- backend/services/stats.py | 12 +- frontend/app/songs/[slug]/page.tsx | 234 +++++++++++++++++++++++++++++ 3 files changed, 247 insertions(+), 2 deletions(-) create mode 100644 frontend/app/songs/[slug]/page.tsx diff --git a/backend/schemas.py b/backend/schemas.py index 4c154c0..6a16e5a 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -1,4 +1,4 @@ -from typing import Optional, List +from typing import Optional, List, Dict from sqlmodel import SQLModel from datetime import datetime @@ -111,6 +111,7 @@ class SongReadWithStats(SongRead): times_played: int gap: int last_played: Optional[datetime] = None + set_breakdown: Dict[str, int] = {} performances: List[PerformanceReadWithShow] = [] class PerformanceDetailRead(PerformanceRead): diff --git a/backend/services/stats.py b/backend/services/stats.py index 2485a50..6070436 100644 --- a/backend/services/stats.py +++ b/backend/services/stats.py @@ -28,10 +28,20 @@ def get_song_stats(session: Session, song_id: int): select(func.count(Show.id)).where(Show.date > last_performance.date) ).one() + # Set Breakdown + set_stats_query = session.exec( + select(Performance.set_name, func.count(Performance.id)) + .where(Performance.song_id == song_id) + .group_by(Performance.set_name) + ).all() + + set_breakdown = {row[0]: row[1] for row in set_stats_query if row[0]} + return { "times_played": times_played, "last_played": last_performance.date if last_performance else None, - "gap": gap + "gap": gap, + "set_breakdown": set_breakdown } def check_and_award_badges(session: Session, user_id: int): diff --git a/frontend/app/songs/[slug]/page.tsx b/frontend/app/songs/[slug]/page.tsx new file mode 100644 index 0000000..37edfb1 --- /dev/null +++ b/frontend/app/songs/[slug]/page.tsx @@ -0,0 +1,234 @@ + +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +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" +import { getApiUrl } from "@/lib/api-config" +import { CommentSection } from "@/components/social/comment-section" +import { EntityRating } from "@/components/social/entity-rating" +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 { + const res = await fetch(`${getApiUrl()}/songs/${id}`, { cache: 'no-store' }) + if (!res.ok) return null + return res.json() + } catch (e) { + console.error(e) + return null + } +} + +// 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<{ slug: string }> }) { + const { slug } = await params + const song = await getSong(slug) + + if (!song) { + notFound() + } + + const headyVersions = getHeadyVersions(song.performances || []) + const topPerformance = headyVersions[0] + + return ( +
No video available
++ {topPerformance.show?.date ? new Date(topPerformance.show.date).toLocaleDateString() : "Unknown Date"} +
++ {topPerformance.show?.venue?.name || "Unknown Venue"} +
++ {perf.show?.date ? new Date(perf.show.date).toLocaleDateString() : "Unknown"} +
++ {perf.show?.venue?.name || "Unknown Venue"} +
+