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"; 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 ( return (
<div className="flex gap-0.5"> <div className="flex gap-0.5">
{[1, 2, 3, 4, 5].map((star) => ( {[1, 2, 3, 4, 5].map((star) => {
<button const filled = Math.min(Math.max(value - (star - 1), 0), 1);
key={star}
type="button" return (
disabled={disabled} <button
onClick={() => onChange?.(star === value ? 0 : star)} key={star}
className={`p-0 border-0 bg-transparent transition-colors ${ type="button"
disabled ? "cursor-default" : "cursor-pointer hover:scale-110" disabled={disabled}
}`} onClick={(event) => handleSelect(event, star)}
> className={`relative w-5 h-5 p-0 border-0 bg-transparent transition-transform ${
<Star disabled ? "cursor-default" : "cursor-pointer hover:scale-110"
size={18} }`}
className={ >
star <= value <Star size={20} className="absolute inset-0 text-gray-300" />
? "fill-amber-400 text-amber-400" {filled > 0 && (
: "fill-none text-gray-300" <Star
} size={20}
/> className="absolute inset-0 text-amber-400 fill-amber-400"
</button> style={{ clipPath: `inset(0 ${100 - filled * 100}% 0 0)` }}
))} />
)}
</button>
);
})}
</div> </div>
); );
} }