feat: add half-star rating support with click position detection and visual rendering
This commit is contained in:
@@ -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) => (
|
||||
{[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={() => onChange?.(star === value ? 0 : star)}
|
||||
className={`p-0 border-0 bg-transparent transition-colors ${
|
||||
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={18}
|
||||
className={
|
||||
star <= value
|
||||
? "fill-amber-400 text-amber-400"
|
||||
: "fill-none text-gray-300"
|
||||
}
|
||||
size={20}
|
||||
className="absolute inset-0 text-amber-400 fill-amber-400"
|
||||
style={{ clipPath: `inset(0 ${100 - filled * 100}% 0 0)` }}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user