From 16bacc29df2dc119eab6b3891e01326490f41a2f Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Sun, 21 Dec 2025 21:52:23 -0800 Subject: [PATCH] feat: Enhance Performance Page with Top Rated Versions list --- backend/routers/performances.py | 63 +++++++++++++++++++------ backend/schemas.py | 1 + frontend/app/performances/[id]/page.tsx | 42 +++++++++++++++++ 3 files changed, 92 insertions(+), 14 deletions(-) diff --git a/backend/routers/performances.py b/backend/routers/performances.py index aba36f0..6d48d10 100644 --- a/backend/routers/performances.py +++ b/backend/routers/performances.py @@ -2,8 +2,8 @@ from typing import List from fastapi import APIRouter, Depends, HTTPException, Query from sqlmodel import Session, select, func from database import get_session -from models import Performance, PerformanceNickname, Tag, EntityTag, Show -from schemas import PerformanceDetailRead, PerformanceNicknameCreate, PerformanceNicknameRead +from models import Performance, PerformanceNickname, Tag, EntityTag, Show, Venue, Rating +from schemas import PerformanceDetailRead, PerformanceNicknameCreate, PerformanceNicknameRead, PerformanceReadWithShow from auth import get_current_user router = APIRouter(prefix="/performances", tags=["performances"]) @@ -27,18 +27,18 @@ def read_performance(performance_id_or_slug: str, session: Session = Depends(get # --- Calculate Stats & Navigation --- # Get all performances of this song, ordered by date - # We need to join Show to order by date - all_perfs = session.exec( - select(Performance, Show.date) - .join(Show) + # Join Show and Venue for list display + all_perfs_data = session.exec( + select(Performance, Show, Venue) + .join(Show, Performance.show_id == Show.id) + .outerjoin(Venue, Show.venue_id == Venue.id) .where(Performance.song_id == performance.song_id) .order_by(Show.date) ).all() # Find current index - # all_perfs is a list of tuples (Performance, date) current_index = -1 - for i, (p, d) in enumerate(all_perfs): + for i, (p, s, v) in enumerate(all_perfs_data): if p.id == performance_id: current_index = i break @@ -49,12 +49,11 @@ def read_performance(performance_id_or_slug: str, session: Session = Depends(get times_played = current_index + 1 # 1-based count if current_index > 0: - prev_id = all_perfs[current_index - 1][0].id + prev_id = all_perfs_data[current_index - 1][0].id # Calculate Gap - # Gap is number of shows between prev performance and this one - prev_date = all_perfs[current_index - 1][1] - current_date = all_perfs[current_index][1] + prev_date = all_perfs_data[current_index - 1][1].date + current_date = all_perfs_data[current_index][1].date gap = session.exec( select(func.count(Show.id)) @@ -62,8 +61,43 @@ def read_performance(performance_id_or_slug: str, session: Session = Depends(get .where(Show.date < current_date) ).one() - if current_index < len(all_perfs) - 1: - next_id = all_perfs[current_index + 1][0].id + if current_index < len(all_perfs_data) - 1: + next_id = all_perfs_data[current_index + 1][0].id + + # Fetch ratings for all performances of this song + rating_stats = session.exec( + select(Rating.performance_id, func.avg(Rating.score), func.count(Rating.id)) + .where(Rating.song_id == performance.song_id) + .where(Rating.performance_id.is_not(None)) + .group_by(Rating.performance_id) + ).all() + + rating_map = {row[0]: {"avg": row[1], "count": row[2]} for row in rating_stats} + + # Build other_performances list + other_performances = [] + for p, s, v in all_perfs_data: + if p.id == performance_id: + continue + + stats = rating_map.get(p.id, {"avg": 0.0, "count": 0}) + + perf_read = PerformanceReadWithShow( + **p.model_dump(), + song=performance.song, # Reuse loaded song object + show_date=s.date, + show_slug=s.slug, + venue_name=v.name if v else "Unknown Venue", + venue_city=v.city if v else "Unknown City", + venue_state=v.state if v else None, + avg_rating=stats["avg"], + total_reviews=stats["count"], + nicknames=p.nicknames + ) + other_performances.append(perf_read) + + # Sort by rating desc, then date desc + other_performances.sort(key=lambda x: (x.avg_rating or 0, x.show_date), reverse=True) # Construct response manually to include extra fields # We need to ensure nested models (show, song) are validated correctly @@ -75,6 +109,7 @@ def read_performance(performance_id_or_slug: str, session: Session = Depends(get perf_dict['next_performance_id'] = next_id perf_dict['gap'] = gap perf_dict['times_played'] = times_played + perf_dict['other_performances'] = other_performances return perf_dict diff --git a/backend/schemas.py b/backend/schemas.py index c0abe94..2dbaa17 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -113,6 +113,7 @@ class PerformanceDetailRead(PerformanceRead): next_performance_id: Optional[int] = None gap: Optional[int] = 0 times_played: Optional[int] = 0 + other_performances: List[PerformanceReadWithShow] = [] # --- Groups --- class GroupBase(SQLModel): diff --git a/frontend/app/performances/[id]/page.tsx b/frontend/app/performances/[id]/page.tsx index 6257f70..5ca8b6d 100644 --- a/frontend/app/performances/[id]/page.tsx +++ b/frontend/app/performances/[id]/page.tsx @@ -281,6 +281,48 @@ export default async function PerformanceDetailPage({ params }: { params: Promis + {/* Top Rated Versions */} + {performance.other_performances && performance.other_performances.length > 0 && ( + + + + + Top Rated Versions + + + + {performance.other_performances.slice(0, 5).map((perf: any) => ( + +
+ + {new Date(perf.show_date).toLocaleDateString()} + + + {perf.venue_name} + +
+ {perf.avg_rating > 0 && ( +
+ + {perf.avg_rating.toFixed(1)} +
+ )} + + ))} + + View all {performance.other_performances.length + 1} versions → + +
+
+ )} + {/* Quick Links */}