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

View file

@ -5,11 +5,12 @@ import { StarRating } from "@/components/ui/star-rating"
import { getApiUrl } from "@/lib/api-config"
interface EntityRatingProps {
entityType: "show" | "song" | "venue" | "tour"
entityType: "show" | "song" | "venue" | "tour" | "performance"
entityId: number
compact?: boolean
}
export function EntityRating({ entityType, entityId }: EntityRatingProps) {
export function EntityRating({ entityType, entityId, compact = false }: EntityRatingProps) {
const [userRating, setUserRating] = useState(0)
const [averageRating, setAverageRating] = useState(0)
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))
// Fetch user rating (if logged in)
const token = localStorage.getItem("token")
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.
}
// TODO: Implement fetching user's existing rating
}, [entityType, entityId])
const handleRate = async (score: number) => {
@ -58,7 +51,11 @@ export function EntityRating({ entityType, entityId }: EntityRatingProps) {
const data = await res.json()
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) {
console.error(err)
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 (
<div className="flex items-center gap-2 border-l pl-4">
<div className="flex flex-col">

View file

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