diff --git a/backend/routers/songs.py b/backend/routers/songs.py index 052d0c5..3d12213 100644 --- a/backend/routers/songs.py +++ b/backend/routers/songs.py @@ -80,7 +80,7 @@ def read_song(song_id: int, session: Session = Depends(get_session)): venue_city = p.show.venue.city venue_state = p.show.venue.state - stats = rating_stats.get(p.id, {"avg": 0.0, "count": 0}) + r_stats = rating_stats.get(p.id, {"avg": 0.0, "count": 0}) perf_dtos.append(PerformanceReadWithShow( **p.model_dump(), @@ -88,8 +88,8 @@ def read_song(song_id: int, session: Session = Depends(get_session)): venue_name=venue_name, venue_city=venue_city, venue_state=venue_state, - avg_rating=stats["avg"], - total_reviews=stats["count"] + avg_rating=r_stats["avg"], + total_reviews=r_stats["count"] )) # Merge song data with stats diff --git a/frontend/app/songs/[id]/page.tsx b/frontend/app/songs/[id]/page.tsx index 8d1f3d3..f43a8c4 100644 --- a/frontend/app/songs/[id]/page.tsx +++ b/frontend/app/songs/[id]/page.tsx @@ -1,15 +1,16 @@ + import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { ArrowLeft, PlayCircle, History } from "lucide-react" +import { ArrowLeft, PlayCircle, History, Calendar } from "lucide-react" import Link from "next/link" import { notFound } from "next/navigation" import { Badge } from "@/components/ui/badge" import { getApiUrl } from "@/lib/api-config" -import { RatePerformanceDialog } from "@/components/songs/rate-performance-dialog" import { CommentSection } from "@/components/social/comment-section" import { EntityRating } from "@/components/social/entity-rating" import { EntityReviews } from "@/components/reviews/entity-reviews" import { SocialWrapper } from "@/components/social/social-wrapper" +import { PerformanceList } from "@/components/songs/performance-list" async function getSong(id: string) { try { @@ -32,24 +33,30 @@ export default async function SongDetailPage({ params }: { params: Promise<{ id: return (
-
- - - -
-

{song.title}

-

{song.original_artist}

- {song.tags && song.tags.length > 0 && ( -
- {song.tags.map((tag: any) => ( - - #{tag.name} - - ))} +
+
+ + + +
+
+

{song.title}

+ {song.original_artist && ( + ({song.original_artist}) + )}
- )} + {song.tags && song.tags.length > 0 && ( +
+ {song.tags.map((tag: any) => ( + + #{tag.name} + + ))} +
+ )} +
@@ -84,70 +91,16 @@ export default async function SongDetailPage({ params }: { params: Promise<{ id: Last Played -
+
+ {song.last_played ? new Date(song.last_played).toLocaleDateString() : "Never"}
- - - Performance History - - - {song.performances && song.performances.length > 0 ? ( -
- {song.performances.map((perf: any) => ( -
-
- - {new Date(perf.show_date).toLocaleDateString(undefined, { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric' - })} - -
- - {perf.venue_name}, {perf.venue_city} {perf.venue_state} - -
- {perf.notes && ( -

"{perf.notes}"

- )} -
-
-
- {perf.avg_rating > 0 && ( - - ★ {perf.avg_rating.toFixed(1)} - - )} - { - // Trigger refresh if possible, or just ignore. - // Ideal: reload song data. - window.location.reload() - }} - /> -
-
-
- ))} -
- ) : ( -

No performances recorded.

- )} -
-
+ {/* Performance List Component (Handles Client Sorting) */} +
diff --git a/frontend/components/songs/performance-list.tsx b/frontend/components/songs/performance-list.tsx new file mode 100644 index 0000000..60940ae --- /dev/null +++ b/frontend/components/songs/performance-list.tsx @@ -0,0 +1,151 @@ +"use client" + +import { useState } from "react" +import Link from "next/link" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { RatePerformanceDialog } from "@/components/songs/rate-performance-dialog" +import { ArrowUpDown, Star, Calendar, Music } from "lucide-react" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" + +export interface Performance { + id: number + show_id: number + song_id: number + position: number + set_name: string | null + segue: boolean + notes: string | null + show_date: string + venue_name: string + venue_city: string + venue_state: string | null + avg_rating: number + total_reviews: number +} + +interface PerformanceListProps { + performances: Performance[] + songTitle?: string +} + +type SortOption = "date_desc" | "date_asc" | "rating_desc" + +export function PerformanceList({ performances, songTitle }: PerformanceListProps) { + const [sort, setSort] = useState("date_desc") + + const sortedPerformances = [...performances].sort((a, b) => { + if (sort === "date_desc") { + return new Date(b.show_date).getTime() - new Date(a.show_date).getTime() + } + if (sort === "date_asc") { + return new Date(a.show_date).getTime() - new Date(b.show_date).getTime() + } + if (sort === "rating_desc") { + // Primary: Rating, Secondary: Review Count, Tertiary: Date + if (b.avg_rating !== a.avg_rating) return b.avg_rating - a.avg_rating + if (b.total_reviews !== a.total_reviews) return b.total_reviews - a.total_reviews + return new Date(b.show_date).getTime() - new Date(a.show_date).getTime() + } + return 0 + }) + + return ( + + + + + Performance History + + {performances.length} + + +
+ Sort by: + +
+
+ + {sortedPerformances.length > 0 ? ( +
+ {sortedPerformances.map((perf) => ( +
+
+
+ + {new Date(perf.show_date).toLocaleDateString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + weekday: 'short' + })} + + + {perf.set_name || "Set ?"} + +
+
+ {perf.venue_name} • {perf.venue_city}{perf.venue_state ? `, ${perf.venue_state}` : ""} +
+ {perf.notes && ( +

+ "{perf.notes}" +

+ )} +
+
+ {perf.avg_rating > 0 && ( +
+ = 4.5 ? 'border-yellow-500/50 bg-yellow-500/10 text-yellow-600' : 'border-muted'} + `} + > + = 4.5 ? 'fill-yellow-600' : ''}`} /> + {perf.avg_rating.toFixed(1)} + + + {perf.total_reviews} review{perf.total_reviews !== 1 ? 's' : ''} + +
+ )} + { + // Optional: trigger refresh + window.location.reload() + }} + /> +
+
+ ))} +
+ ) : ( +
+ No performances recorded yet. +
+ )} +
+
+ ) +}