49 lines
1.4 KiB
TypeScript
49 lines
1.4 KiB
TypeScript
import { useState } from 'react';
|
|
import { Star } from 'lucide-react';
|
|
|
|
interface StarRatingProps {
|
|
value: number;
|
|
onChange?: (rating: number) => void;
|
|
size?: number;
|
|
readonly?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
export function StarRating({
|
|
value,
|
|
onChange,
|
|
size = 20,
|
|
readonly = false,
|
|
className = '',
|
|
}: StarRatingProps) {
|
|
const [hover, setHover] = useState(0);
|
|
const interactive = !readonly && !!onChange;
|
|
|
|
return (
|
|
<div className={`flex items-center gap-1 ${className}`}>
|
|
{[1, 2, 3, 4, 5].map((star) => {
|
|
const filled = (hover || value) >= star;
|
|
return (
|
|
<button
|
|
key={star}
|
|
type="button"
|
|
disabled={!interactive}
|
|
onClick={() => interactive && onChange && onChange(star)}
|
|
onMouseEnter={() => interactive && setHover(star)}
|
|
onMouseLeave={() => interactive && setHover(0)}
|
|
className={`${interactive ? 'cursor-pointer transition-transform hover:scale-110' : 'cursor-default'} rounded p-0.5 focus:outline-none focus-visible:ring-1 focus-visible:ring-sky-500`}
|
|
aria-label={`${star} star${star > 1 ? 's' : ''}`}
|
|
>
|
|
<Star
|
|
size={size}
|
|
className={filled ? 'fill-amber-400 text-amber-500' : 'fill-none text-slate-300 dark:text-slate-600'}
|
|
/>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default StarRating;
|