diff --git a/backend/models.py b/backend/models.py index 7efd17d..d1fba22 100644 --- a/backend/models.py +++ b/backend/models.py @@ -148,7 +148,7 @@ class Comment(SQLModel, table=True): class Rating(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) user_id: int = Field(foreign_key="user.id") - score: int = Field(ge=1, le=10, description="Rating from 1 to 10") + score: float = Field(ge=1.0, le=10.0, description="Rating from 1.0 to 10.0") created_at: datetime = Field(default_factory=datetime.utcnow) show_id: Optional[int] = Field(default=None, foreign_key="show.id") @@ -222,7 +222,7 @@ class Review(SQLModel, table=True): user_id: int = Field(foreign_key="user.id") blurb: str = Field(description="One-liner/pullquote") content: str = Field(description="Full review text") - score: int = Field(ge=1, le=10) + score: float = Field(ge=1.0, le=10.0) show_id: Optional[int] = Field(default=None, foreign_key="show.id") venue_id: Optional[int] = Field(default=None, foreign_key="venue.id") song_id: Optional[int] = Field(default=None, foreign_key="song.id") diff --git a/backend/schemas.py b/backend/schemas.py index e45ed61..00cc99e 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -219,7 +219,7 @@ class CommentRead(CommentBase): # We might want to include the username here later class RatingBase(SQLModel): - score: int + score: float show_id: Optional[int] = None song_id: Optional[int] = None performance_id: Optional[int] = None @@ -237,7 +237,7 @@ class RatingRead(RatingBase): class ReviewBase(SQLModel): blurb: str content: str - score: int + score: float show_id: Optional[int] = None venue_id: Optional[int] = None song_id: Optional[int] = None diff --git a/frontend/components/ui/star-rating.tsx b/frontend/components/ui/star-rating.tsx index 287fd17..e946fff 100644 --- a/frontend/components/ui/star-rating.tsx +++ b/frontend/components/ui/star-rating.tsx @@ -1,47 +1,125 @@ -import { useState } from "react" +"use client" + +import { useState, useRef } 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" + size?: "sm" | "md" | "lg" + precision?: "full" | "half" | "decimal" } -export function StarRating({ value, onChange, readonly = false, className, size = "md" }: RatingProps) { +export function StarRating({ + value, + onChange, + readonly = false, + className, + size = "md", + precision = "decimal" +}: RatingProps) { const [hoverValue, setHoverValue] = useState(null) + const containerRef = useRef(null) - const stars = Array.from({ length: 10 }, (_, i) => i + 1) - const starSize = size === "sm" ? "h-3 w-3" : "h-4 w-4" + const stars = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + const starSize = { + sm: "h-3 w-3", + md: "h-4 w-4", + lg: "h-5 w-5" + }[size] + + const handleMouseMove = (e: React.MouseEvent, starIndex: number) => { + if (readonly) return + + const rect = e.currentTarget.getBoundingClientRect() + const x = e.clientX - rect.left + const width = rect.width + const fraction = x / width + + let newValue: number + if (precision === "decimal") { + // Allow 0.1 precision (e.g., 9.2) + newValue = starIndex + Math.round(fraction * 10) / 10 + newValue = Math.max(starIndex, Math.min(starIndex + 1, newValue)) + } else if (precision === "half") { + // Half-star precision + newValue = fraction < 0.5 ? starIndex + 0.5 : starIndex + 1 + } else { + // Full star only + newValue = starIndex + 1 + } + + setHoverValue(newValue) + } + + const handleClick = () => { + if (!readonly && hoverValue !== null && onChange) { + // Round to 1 decimal place + onChange(Math.round(hoverValue * 10) / 10) + } + } + + const displayValue = hoverValue !== null ? hoverValue : value + + // Calculate fill percentage for each star + const getStarFill = (starIndex: number): number => { + const starStart = starIndex + const starEnd = starIndex + 1 + + if (displayValue >= starEnd) return 100 + if (displayValue <= starStart) return 0 + return (displayValue - starStart) * 100 + } return ( -
- {stars.map((star) => ( - - ))} + onMouseMove={(e) => handleMouseMove(e, index)} + onClick={handleClick} + > + {/* Background star (empty) */} + + {/* Foreground star (filled) - uses clip-path for partial fill */} +
+ +
+ + ) + })} + + {/* Display numeric value */} + {!readonly && ( + + {displayValue > 0 ? displayValue.toFixed(1) : "—"} + + )}
) }