From 29cc0289d6ff4c6ad0bf7955465cf57c044e1dc6 Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Wed, 31 Dec 2025 10:05:53 -0800 Subject: [PATCH] feat: redesign song detail page with artist stats and grid layout --- backend/routers/songs.py | 12 ++ backend/schemas.py | 3 + frontend/app/songs/[slug]/page.tsx | 154 ++++++++++-------- .../components/songs/most-played-by-card.tsx | 38 +++++ .../components/songs/performance-list.tsx | 9 +- 5 files changed, 149 insertions(+), 67 deletions(-) create mode 100644 frontend/components/songs/most-played-by-card.tsx diff --git a/backend/routers/songs.py b/backend/routers/songs.py index 326cb2d..a2cb920 100644 --- a/backend/routers/songs.py +++ b/backend/routers/songs.py @@ -110,6 +110,8 @@ def read_song(slug: str, session: Session = Depends(get_session)): venue_name = "Unknown" venue_city = "" venue_state = "" + artist_name = None + artist_slug = None show_date = datetime.now() show_slug = None @@ -120,6 +122,9 @@ def read_song(slug: str, session: Session = Depends(get_session)): venue_name = p.show.venue.name venue_city = p.show.venue.city venue_state = p.show.venue.state + if p.show.vertical: + artist_name = p.show.vertical.name + artist_slug = p.show.vertical.slug r_stats = rating_stats.get(p.id, {"avg": 0.0, "count": 0}) @@ -139,15 +144,22 @@ def read_song(slug: str, session: Session = Depends(get_session)): venue_name=venue_name, venue_city=venue_city, venue_state=venue_state, + artist_name=artist_name, + artist_slug=artist_slug, avg_rating=r_stats["avg"], total_reviews=r_stats["count"] )) + # Calculate artist distribution + from collections import Counter + artist_dist = Counter(p.artist_name for p in perf_dtos if p.artist_name) + # Merge song data with stats song_with_stats = SongReadWithStats( **song.model_dump(), **stats ) + song_with_stats.artist_distribution = artist_dist song_with_stats.tags = tags song_with_stats.performances = perf_dtos return song_with_stats diff --git a/backend/schemas.py b/backend/schemas.py index 1799693..814320f 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -135,6 +135,8 @@ class PerformanceReadWithShow(PerformanceRead): venue_name: str venue_city: str venue_state: Optional[str] = None + artist_name: Optional[str] = None + artist_slug: Optional[str] = None avg_rating: Optional[float] = 0.0 total_reviews: Optional[int] = 0 @@ -143,6 +145,7 @@ class SongReadWithStats(SongRead): gap: int last_played: Optional[datetime] = None set_breakdown: Dict[str, int] = {} + artist_distribution: Dict[str, int] = {} performances: List[PerformanceReadWithShow] = [] class PerformanceDetailRead(PerformanceRead): diff --git a/frontend/app/songs/[slug]/page.tsx b/frontend/app/songs/[slug]/page.tsx index 7308d17..637f17f 100644 --- a/frontend/app/songs/[slug]/page.tsx +++ b/frontend/app/songs/[slug]/page.tsx @@ -100,60 +100,95 @@ export default async function SongDetailPage({ params }: { params: Promise<{ slu -
- - - Times Played - - -
- - {song.times_played} -
-
-
- - - Gap (Shows) - - -
- - {song.gap} -
-
-
- - - Last Played - - -
- - {song.last_played ? new Date(song.last_played).toLocaleDateString() : "Never"} -
-
-
-
- - {/* Set Breakdown */} - {song.set_breakdown && Object.keys(song.set_breakdown).length > 0 && ( - - - Set Distribution - - -
- {Object.entries(song.set_breakdown).sort((a, b) => (b[1] as number) - (a[1] as number)).map(([set, count]) => ( -
- {count as number} - {set} +
+ {/* Left Sidebar: Stats & Charts */} +
+ {/* Basic Stats Grid - Compact */} +
+ + + Times Played + + +
+ + {song.times_played}
- ))} -
- - - )} + + + + + Gap + + +
+ + {song.gap} +
+
+
+ + + Last Played + + +
+ + {song.last_played ? new Date(song.last_played).toLocaleDateString(undefined, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + }) : "Never"} +
+
+
+
+ + {/* Most Played By */} + {song.artist_distribution && ( + + )} + + {/* Set Breakdown */} + {song.set_breakdown && Object.keys(song.set_breakdown).length > 0 && ( + + + Set Distribution + + +
+ {Object.entries(song.set_breakdown).sort((a, b) => (b[1] as number) - (a[1] as number)).map(([set, count]) => ( +
+ {set} + {count as number} +
+ ))} +
+
+
+ )} +
+ + {/* Right Content: Performance History */} +
+ + + {/* Song Evolution (moved to bottom) */} + + +
+ + + + +
+
+
{/* Heady Version Section */} {headyVersions.length > 0 && ( @@ -282,20 +317,7 @@ export default async function SongDetailPage({ params }: { params: Promise<{ slu )} - - {/* Performance List Component (Handles Client Sorting) */} - - -
- - - - - - - -
) } diff --git a/frontend/components/songs/most-played-by-card.tsx b/frontend/components/songs/most-played-by-card.tsx new file mode 100644 index 0000000..7c00438 --- /dev/null +++ b/frontend/components/songs/most-played-by-card.tsx @@ -0,0 +1,38 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Trophy } from "lucide-react" + +interface MostPlayedByProps { + distribution: Record +} + +export function MostPlayedByCard({ distribution }: MostPlayedByProps) { + if (!distribution || Object.keys(distribution).length === 0) return null + + // Sort entries by count descending + const sortedEntries = Object.entries(distribution).sort((a, b) => b[1] - a[1]) + + return ( + + + + + Most Played By + + + +
+ {sortedEntries.map(([artist, count], index) => ( +
+
+ {artist} +
+ + {count} + +
+ ))} +
+
+
+ ) +} diff --git a/frontend/components/songs/performance-list.tsx b/frontend/components/songs/performance-list.tsx index bf132c5..2c6df7a 100644 --- a/frontend/components/songs/performance-list.tsx +++ b/frontend/components/songs/performance-list.tsx @@ -21,6 +21,8 @@ export interface Performance { venue_name: string venue_city: string venue_state: string | null + artist_name?: string + artist_slug?: string avg_rating: number total_reviews: number youtube_link?: string | null @@ -86,9 +88,14 @@ export function PerformanceList({ performances }: PerformanceListProps) { >
+ {perf.artist_name && ( + + {perf.artist_name} + + )} {new Date(perf.show_date).toLocaleDateString(undefined, { year: 'numeric',