151 lines
7.6 KiB
TypeScript
151 lines
7.6 KiB
TypeScript
"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<SortOption>("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 (
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between pb-4">
|
|
<CardTitle className="text-lg font-semibold flex items-center gap-2">
|
|
<Music className="h-5 w-5 text-primary" />
|
|
Performance History
|
|
<Badge variant="secondary" className="ml-2 font-normal">
|
|
{performances.length}
|
|
</Badge>
|
|
</CardTitle>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-muted-foreground hidden sm:inline-block">Sort by:</span>
|
|
<Select value={sort} onValueChange={(v) => setSort(v as SortOption)}>
|
|
<SelectTrigger className="w-[160px] h-8 text-xs">
|
|
<SelectValue placeholder="Sort order" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="date_desc">Newest First</SelectItem>
|
|
<SelectItem value="date_asc">Oldest First</SelectItem>
|
|
<SelectItem value="rating_desc">Highest Rated</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{sortedPerformances.length > 0 ? (
|
|
<div className="space-y-1">
|
|
{sortedPerformances.map((perf) => (
|
|
<div
|
|
key={perf.id}
|
|
className="flex flex-col sm:flex-row sm:items-center justify-between p-3 rounded-lg hover:bg-muted/50 transition-colors border border-transparent hover:border-border"
|
|
>
|
|
<div className="space-y-1 flex-1 min-w-0 pr-4">
|
|
<div className="flex items-baseline gap-2 flex-wrap">
|
|
<Link
|
|
href={`/shows/${perf.show_id}`}
|
|
className="font-medium hover:underline text-primary truncate"
|
|
>
|
|
{new Date(perf.show_date).toLocaleDateString(undefined, {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
weekday: 'short'
|
|
})}
|
|
</Link>
|
|
<span className="text-xs text-muted-foreground uppercase tracking-wider font-mono">
|
|
{perf.set_name || "Set ?"}
|
|
</span>
|
|
</div>
|
|
<div className="text-sm text-muted-foreground truncate">
|
|
{perf.venue_name} • {perf.venue_city}{perf.venue_state ? `, ${perf.venue_state}` : ""}
|
|
</div>
|
|
{perf.notes && (
|
|
<p className="text-sm italic text-muted-foreground/90 line-clamp-2">
|
|
"{perf.notes}"
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-3 mt-3 sm:mt-0 shrink-0">
|
|
{perf.avg_rating > 0 && (
|
|
<div className="flex flex-col items-end">
|
|
<Badge
|
|
variant="outline"
|
|
className={`
|
|
gap-1 px-2 py-1
|
|
${perf.avg_rating >= 4.5 ? 'border-yellow-500/50 bg-yellow-500/10 text-yellow-600' : 'border-muted'}
|
|
`}
|
|
>
|
|
<Star className={`h-3 w-3 ${perf.avg_rating >= 4.5 ? 'fill-yellow-600' : ''}`} />
|
|
{perf.avg_rating.toFixed(1)}
|
|
</Badge>
|
|
<span className="text-[10px] text-muted-foreground mt-0.5">
|
|
{perf.total_reviews} review{perf.total_reviews !== 1 ? 's' : ''}
|
|
</span>
|
|
</div>
|
|
)}
|
|
<RatePerformanceDialog
|
|
performanceId={perf.id}
|
|
performanceDate={new Date(perf.show_date).toLocaleDateString()}
|
|
venue={perf.venue_name}
|
|
onRatingSubmit={() => {
|
|
// Optional: trigger refresh
|
|
window.location.reload()
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-10 text-muted-foreground text-sm">
|
|
No performances recorded yet.
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|