mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 10:51:12 -05:00
110 lines
3.0 KiB
TypeScript
110 lines
3.0 KiB
TypeScript
import { useState } from 'react';
|
|
import { Star } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface StarRatingProps {
|
|
rating: number;
|
|
onChange?: (rating: number) => void;
|
|
size?: 'sm' | 'md' | 'lg';
|
|
interactive?: boolean;
|
|
showLabel?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
export function StarRating({
|
|
rating,
|
|
onChange,
|
|
size = 'md',
|
|
interactive = false,
|
|
showLabel = false,
|
|
className
|
|
}: StarRatingProps) {
|
|
const [hoverRating, setHoverRating] = useState(0);
|
|
|
|
const sizes = {
|
|
sm: 'w-4 h-4',
|
|
md: 'w-6 h-6',
|
|
lg: 'w-8 h-8'
|
|
};
|
|
|
|
const starSize = sizes[size];
|
|
const displayRating = hoverRating || rating;
|
|
|
|
const handleStarClick = (starIndex: number, isHalf: boolean) => {
|
|
if (!interactive || !onChange) return;
|
|
const newRating = starIndex + (isHalf ? 0.5 : 1);
|
|
onChange(newRating);
|
|
};
|
|
|
|
const handleStarHover = (starIndex: number, isHalf: boolean) => {
|
|
if (!interactive) return;
|
|
const newHoverRating = starIndex + (isHalf ? 0.5 : 1);
|
|
setHoverRating(newHoverRating);
|
|
};
|
|
|
|
const handleMouseLeave = () => {
|
|
if (!interactive) return;
|
|
setHoverRating(0);
|
|
};
|
|
|
|
const renderStar = (index: number) => {
|
|
const starValue = index + 1;
|
|
const isFullStar = displayRating >= starValue;
|
|
const isHalfStar = displayRating >= starValue - 0.5 && displayRating < starValue;
|
|
|
|
const getStarFill = () => {
|
|
if (isFullStar) return '100%';
|
|
if (isHalfStar) return '50%';
|
|
return '0%';
|
|
};
|
|
|
|
return (
|
|
<div key={index} className="relative inline-block">
|
|
{/* Background unfilled star */}
|
|
<Star className={cn(starSize, 'text-muted-foreground')} />
|
|
|
|
{/* Filled star overlay */}
|
|
<div
|
|
className="absolute inset-0 overflow-hidden"
|
|
style={{ width: getStarFill() }}
|
|
>
|
|
<Star className={cn(starSize, 'fill-yellow-400 text-yellow-400')} />
|
|
</div>
|
|
|
|
{/* Interactive click areas */}
|
|
{interactive && (
|
|
<>
|
|
{/* Left half click area */}
|
|
<div
|
|
className="absolute top-0 left-0 w-1/2 h-full cursor-pointer z-10"
|
|
onClick={() => handleStarClick(index, true)}
|
|
onMouseEnter={() => handleStarHover(index, true)}
|
|
onMouseLeave={handleMouseLeave}
|
|
/>
|
|
|
|
{/* Right half click area */}
|
|
<div
|
|
className="absolute top-0 right-0 w-1/2 h-full cursor-pointer z-10"
|
|
onClick={() => handleStarClick(index, false)}
|
|
onMouseEnter={() => handleStarHover(index, false)}
|
|
onMouseLeave={handleMouseLeave}
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className={cn('flex items-center gap-1', className)}>
|
|
<div className="flex items-center">
|
|
{Array.from({ length: 5 }, (_, i) => renderStar(i))}
|
|
</div>
|
|
{showLabel && (
|
|
<span className="ml-2 text-sm text-muted-foreground">
|
|
{displayRating} star{displayRating !== 1 ? 's' : ''}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
} |