feat(frontend): Implement Heady Version mechanics with performance ratings
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run

This commit is contained in:
fullsizemalt 2025-12-21 01:17:00 -08:00
parent b879f28813
commit c4905d7470
3 changed files with 63 additions and 35 deletions

View file

@ -86,8 +86,9 @@ export default async function ShowDetailPage({ params }: { params: Promise<{ id:
{show.performances && show.performances.length > 0 ? ( {show.performances && show.performances.length > 0 ? (
<div className="space-y-2"> <div className="space-y-2">
{show.performances.map((perf: any) => ( {show.performances.map((perf: any) => (
<div key={perf.id} className="flex items-center gap-2 group"> <div key={perf.id} className="flex items-center justify-between group py-1 hover:bg-muted/50 rounded px-2 -mx-2">
<span className="text-muted-foreground w-6 text-right text-sm">{perf.position}.</span> <div className="flex items-center gap-2">
<span className="text-muted-foreground w-6 text-right text-sm font-mono">{perf.position}.</span>
<div className="font-medium"> <div className="font-medium">
{perf.song?.title || "Unknown Song"} {perf.song?.title || "Unknown Song"}
{perf.segue && <span className="ml-1 text-muted-foreground">&gt;</span>} {perf.segue && <span className="ml-1 text-muted-foreground">&gt;</span>}
@ -95,7 +96,7 @@ export default async function ShowDetailPage({ params }: { params: Promise<{ id:
{/* Nicknames */} {/* Nicknames */}
{perf.nicknames && perf.nicknames.length > 0 && ( {perf.nicknames && perf.nicknames.length > 0 && (
<div className="flex gap-1"> <div className="flex gap-1 ml-2">
{perf.nicknames.map((nick: any) => ( {perf.nicknames.map((nick: any) => (
<span key={nick.id} className="text-xs bg-yellow-100 text-yellow-800 px-1.5 py-0.5 rounded-full border border-yellow-200" title={nick.description}> <span key={nick.id} className="text-xs bg-yellow-100 text-yellow-800 px-1.5 py-0.5 rounded-full border border-yellow-200" title={nick.description}>
"{nick.nickname}" "{nick.nickname}"
@ -105,11 +106,25 @@ export default async function ShowDetailPage({ params }: { params: Promise<{ id:
)} )}
{/* Suggest Nickname Button */} {/* Suggest Nickname Button */}
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
<SuggestNicknameDialog <SuggestNicknameDialog
performanceId={perf.id} performanceId={perf.id}
songTitle={perf.song?.title || "Song"} songTitle={perf.song?.title || "Song"}
/> />
</div> </div>
</div>
{/* Rating Column */}
<div className="flex items-center">
<SocialWrapper type="ratings">
<EntityRating
entityType="performance"
entityId={perf.id}
compact={true}
/>
</SocialWrapper>
</div>
</div>
))} ))}
</div> </div>
) : ( ) : (

View file

@ -5,11 +5,12 @@ import { StarRating } from "@/components/ui/star-rating"
import { getApiUrl } from "@/lib/api-config" import { getApiUrl } from "@/lib/api-config"
interface EntityRatingProps { interface EntityRatingProps {
entityType: "show" | "song" | "venue" | "tour" entityType: "show" | "song" | "venue" | "tour" | "performance"
entityId: number entityId: number
compact?: boolean
} }
export function EntityRating({ entityType, entityId }: EntityRatingProps) { export function EntityRating({ entityType, entityId, compact = false }: EntityRatingProps) {
const [userRating, setUserRating] = useState(0) const [userRating, setUserRating] = useState(0)
const [averageRating, setAverageRating] = useState(0) const [averageRating, setAverageRating] = useState(0)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@ -22,15 +23,7 @@ export function EntityRating({ entityType, entityId }: EntityRatingProps) {
.catch(err => console.error("Failed to fetch avg rating", err)) .catch(err => console.error("Failed to fetch avg rating", err))
// Fetch user rating (if logged in) // Fetch user rating (if logged in)
const token = localStorage.getItem("token") // TODO: Implement fetching user's existing rating
if (token) {
// We don't have a direct "get my rating" endpoint in the snippet I saw,
// but we can infer it or maybe we need to add one.
// For now, let's assume we can't easily get *my* rating without a specific endpoint
// or filtering the list.
// Actually, the backend `create_rating` checks for existing.
// Let's just implement setting it for now.
}
}, [entityType, entityId]) }, [entityType, entityId])
const handleRate = async (score: number) => { const handleRate = async (score: number) => {
@ -58,7 +51,11 @@ export function EntityRating({ entityType, entityId }: EntityRatingProps) {
const data = await res.json() const data = await res.json()
setUserRating(data.score) setUserRating(data.score)
// Refresh average?
// Re-fetch average to keep it lively
fetch(`${getApiUrl()}/social/ratings/average?${entityType}_id=${entityId}`)
.then(res => res.json())
.then(setAverageRating)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
alert("Error submitting rating") alert("Error submitting rating")
@ -67,6 +64,19 @@ export function EntityRating({ entityType, entityId }: EntityRatingProps) {
} }
} }
if (compact) {
return (
<div className="flex items-center gap-1.5 opacity-80 hover:opacity-100 transition-opacity">
<StarRating value={userRating} onChange={handleRate} size="sm" />
{averageRating > 0 && (
<span className="text-[10px] text-muted-foreground font-mono">
{averageRating.toFixed(1)}
</span>
)}
</div>
)
}
return ( return (
<div className="flex items-center gap-2 border-l pl-4"> <div className="flex items-center gap-2 border-l pl-4">
<div className="flex flex-col"> <div className="flex flex-col">

View file

@ -2,17 +2,20 @@ import { useState } from "react"
import { Star } from "lucide-react" import { Star } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
interface RatingProps { interface RatingProps {
value: number value: number
onChange?: (value: number) => void onChange?: (value: number) => void
readonly?: boolean readonly?: boolean
className?: string className?: string
size?: "sm" | "md"
} }
export function StarRating({ value, onChange, readonly = false, className }: RatingProps) { export function StarRating({ value, onChange, readonly = false, className, size = "md" }: RatingProps) {
const [hoverValue, setHoverValue] = useState<number | null>(null) const [hoverValue, setHoverValue] = useState<number | null>(null)
const stars = Array.from({ length: 10 }, (_, i) => i + 1) const stars = Array.from({ length: 10 }, (_, i) => i + 1)
const starSize = size === "sm" ? "h-3 w-3" : "h-4 w-4"
return ( return (
<div className={cn("flex gap-0.5", className)}> <div className={cn("flex gap-0.5", className)}>
@ -31,7 +34,7 @@ export function StarRating({ value, onChange, readonly = false, className }: Rat
> >
<Star <Star
className={cn( className={cn(
"h-4 w-4", starSize,
(hoverValue !== null ? star <= hoverValue : star <= value) (hoverValue !== null ? star <= hoverValue : star <= value)
? "fill-primary text-primary" ? "fill-primary text-primary"
: "fill-muted text-muted-foreground" : "fill-muted text-muted-foreground"