feat: Enhance Performance Page with Top Rated Versions list
This commit is contained in:
parent
5e123463f7
commit
16bacc29df
3 changed files with 92 additions and 14 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue