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 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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue