feat: add half-star rating support with click position detection and visual rendering

This commit is contained in:
2026-02-13 18:23:18 +01:00
parent e796e57e34
commit 974c0348f0

View File

@@ -1,28 +1,51 @@
import { Star } from "lucide-react";
export default function StarRating({ value, onChange, disabled = false }) {
export default function StarRating({ value = 0, onChange, disabled = false }) {
const handleSelect = (event, star) => {
if (disabled) return;
const rect = event.currentTarget.getBoundingClientRect();
let clientX = event.clientX ?? event.nativeEvent?.clientX ?? 0;
if (event.nativeEvent?.touches?.length) {
clientX = event.nativeEvent.touches[0].clientX;
} else if (event.nativeEvent?.changedTouches?.length) {
clientX = event.nativeEvent.changedTouches[0].clientX;
}
const clickX = clientX - rect.left;
const isHalf = clickX <= rect.width / 2;
const nextValue = isHalf ? star - 0.5 : star;
onChange?.(value === nextValue ? 0 : nextValue);
};
return (
<div className="flex gap-0.5">
{[1, 2, 3, 4, 5].map((star) => (
<button
key={star}
type="button"
disabled={disabled}
onClick={() => onChange?.(star === value ? 0 : star)}
className={`p-0 border-0 bg-transparent transition-colors ${
disabled ? "cursor-default" : "cursor-pointer hover:scale-110"
}`}
>
<Star
size={18}
className={
star <= value
? "fill-amber-400 text-amber-400"
: "fill-none text-gray-300"
}
/>
</button>
))}
{[1, 2, 3, 4, 5].map((star) => {
const filled = Math.min(Math.max(value - (star - 1), 0), 1);
return (
<button
key={star}
type="button"
disabled={disabled}
onClick={(event) => handleSelect(event, star)}
className={`relative w-5 h-5 p-0 border-0 bg-transparent transition-transform ${
disabled ? "cursor-default" : "cursor-pointer hover:scale-110"
}`}
>
<Star size={20} className="absolute inset-0 text-gray-300" />
{filled > 0 && (
<Star
size={20}
className="absolute inset-0 text-amber-400 fill-amber-400"
style={{ clipPath: `inset(0 ${100 - filled * 100}% 0 0)` }}
/>
)}
</button>
);
})}
</div>
);
}