feat(frontend): Implement Heady Version mechanics with performance ratings
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run
This commit is contained in:
parent
b879f28813
commit
c4905d7470
3 changed files with 63 additions and 35 deletions
|
|
@ -86,29 +86,44 @@ 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">
|
||||||
<div className="font-medium">
|
<span className="text-muted-foreground w-6 text-right text-sm font-mono">{perf.position}.</span>
|
||||||
{perf.song?.title || "Unknown Song"}
|
<div className="font-medium">
|
||||||
{perf.segue && <span className="ml-1 text-muted-foreground">></span>}
|
{perf.song?.title || "Unknown Song"}
|
||||||
|
{perf.segue && <span className="ml-1 text-muted-foreground">></span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nicknames */}
|
||||||
|
{perf.nicknames && perf.nicknames.length > 0 && (
|
||||||
|
<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}"
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Suggest Nickname Button */}
|
||||||
|
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<SuggestNicknameDialog
|
||||||
|
performanceId={perf.id}
|
||||||
|
songTitle={perf.song?.title || "Song"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Nicknames */}
|
{/* Rating Column */}
|
||||||
{perf.nicknames && perf.nicknames.length > 0 && (
|
<div className="flex items-center">
|
||||||
<div className="flex gap-1">
|
<SocialWrapper type="ratings">
|
||||||
{perf.nicknames.map((nick: any) => (
|
<EntityRating
|
||||||
<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}>
|
entityType="performance"
|
||||||
"{nick.nickname}"
|
entityId={perf.id}
|
||||||
</span>
|
compact={true}
|
||||||
))}
|
/>
|
||||||
</div>
|
</SocialWrapper>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{/* Suggest Nickname Button */}
|
|
||||||
<SuggestNicknameDialog
|
|
||||||
performanceId={perf.id}
|
|
||||||
songTitle={perf.song?.title || "Song"}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue