From be7e9111f0410a375251b931825ea912d7999acd Mon Sep 17 00:00:00 2001
From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com>
Date: Sun, 21 Dec 2025 01:45:33 -0800
Subject: [PATCH] feat(frontend): Added Heady Version sorting to song page &
fixed backend bug
---
backend/routers/songs.py | 6 +-
frontend/app/songs/[id]/page.tsx | 107 ++++---------
.../components/songs/performance-list.tsx | 151 ++++++++++++++++++
3 files changed, 184 insertions(+), 80 deletions(-)
create mode 100644 frontend/components/songs/performance-list.tsx
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.
+
+ )}
+
+
+ )
+}