feat: Enhance Performance Page with Top Rated Versions list

This commit is contained in:
fullsizemalt 2025-12-21 21:52:23 -08:00
parent 5e123463f7
commit 16bacc29df
3 changed files with 92 additions and 14 deletions

View file

@ -2,8 +2,8 @@ from typing import List
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from sqlmodel import Session, select, func from sqlmodel import Session, select, func
from database import get_session from database import get_session
from models import Performance, PerformanceNickname, Tag, EntityTag, Show from models import Performance, PerformanceNickname, Tag, EntityTag, Show, Venue, Rating
from schemas import PerformanceDetailRead, PerformanceNicknameCreate, PerformanceNicknameRead from schemas import PerformanceDetailRead, PerformanceNicknameCreate, PerformanceNicknameRead, PerformanceReadWithShow
from auth import get_current_user from auth import get_current_user
router = APIRouter(prefix="/performances", tags=["performances"]) 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 --- # --- Calculate Stats & Navigation ---
# Get all performances of this song, ordered by date # Get all performances of this song, ordered by date
# We need to join Show to order by date # Join Show and Venue for list display
all_perfs = session.exec( all_perfs_data = session.exec(
select(Performance, Show.date) select(Performance, Show, Venue)
.join(Show) .join(Show, Performance.show_id == Show.id)
.outerjoin(Venue, Show.venue_id == Venue.id)
.where(Performance.song_id == performance.song_id) .where(Performance.song_id == performance.song_id)
.order_by(Show.date) .order_by(Show.date)
).all() ).all()
# Find current index # Find current index
# all_perfs is a list of tuples (Performance, date)
current_index = -1 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: if p.id == performance_id:
current_index = i current_index = i
break 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 times_played = current_index + 1 # 1-based count
if current_index > 0: 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 # Calculate Gap
# Gap is number of shows between prev performance and this one prev_date = all_perfs_data[current_index - 1][1].date
prev_date = all_perfs[current_index - 1][1] current_date = all_perfs_data[current_index][1].date
current_date = all_perfs[current_index][1]
gap = session.exec( gap = session.exec(
select(func.count(Show.id)) 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) .where(Show.date < current_date)
).one() ).one()
if current_index < len(all_perfs) - 1: if current_index < len(all_perfs_data) - 1:
next_id = all_perfs[current_index + 1][0].id 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 # Construct response manually to include extra fields
# We need to ensure nested models (show, song) are validated correctly # 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['next_performance_id'] = next_id
perf_dict['gap'] = gap perf_dict['gap'] = gap
perf_dict['times_played'] = times_played perf_dict['times_played'] = times_played
perf_dict['other_performances'] = other_performances
return perf_dict return perf_dict

View file

@ -113,6 +113,7 @@ class PerformanceDetailRead(PerformanceRead):
next_performance_id: Optional[int] = None next_performance_id: Optional[int] = None
gap: Optional[int] = 0 gap: Optional[int] = 0
times_played: Optional[int] = 0 times_played: Optional[int] = 0
other_performances: List[PerformanceReadWithShow] = []
# --- Groups --- # --- Groups ---
class GroupBase(SQLModel): class GroupBase(SQLModel):

View file

@ -281,6 +281,48 @@ export default async function PerformanceDetailPage({ params }: { params: Promis
</CardContent> </CardContent>
</Card> </Card>
{/* Top Rated Versions */}
{performance.other_performances && performance.other_performances.length > 0 && (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<Sparkles className="h-4 w-4 text-yellow-500" />
Top Rated Versions
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{performance.other_performances.slice(0, 5).map((perf: any) => (
<Link
key={perf.id}
href={`/performances/${perf.slug || perf.id}`}
className="flex items-start justify-between group"
>
<div className="flex flex-col">
<span className="font-medium group-hover:text-primary transition-colors text-sm">
{new Date(perf.show_date).toLocaleDateString()}
</span>
<span className="text-xs text-muted-foreground">
{perf.venue_name}
</span>
</div>
{perf.avg_rating > 0 && (
<div className="flex items-center gap-1 bg-secondary px-1.5 py-0.5 rounded text-xs font-mono">
<span className="text-yellow-500 text-[10px]"></span>
<span>{perf.avg_rating.toFixed(1)}</span>
</div>
)}
</Link>
))}
<Link
href={`/songs/${performance.song.id}`}
className="block text-xs text-center text-muted-foreground hover:text-primary pt-2 border-t mt-2"
>
View all {performance.other_performances.length + 1} versions
</Link>
</CardContent>
</Card>
)}
{/* Quick Links */} {/* Quick Links */}
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">