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 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

View file

@ -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):

View file

@ -281,6 +281,48 @@ export default async function PerformanceDetailPage({ params }: { params: Promis
</CardContent>
</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 */}
<Card>
<CardHeader className="pb-2">