"use client"; import React, { useState, useCallback } from 'react'; export interface RatingProps { /** Current value (controlled) */ value?: number; /** Default value */ defaultValue?: number; /** Maximum rating value */ max?: number; /** Callback when value changes */ onChange?: (value: number) => void; /** Allow half values */ allowHalf?: boolean; /** Allow clearing (clicking same value) */ allowClear?: boolean; /** Disabled state */ disabled?: boolean; /** Read-only state */ readOnly?: boolean; /** Size variant */ size?: 'sm' | 'md' | 'lg'; /** Color */ color?: string; /** Empty color */ emptyColor?: string; /** Custom icon */ icon?: React.ReactNode; /** Custom empty icon */ emptyIcon?: React.ReactNode; /** Custom half icon */ halfIcon?: React.ReactNode; /** Character to display (alternative to icon) */ character?: React.ReactNode; /** Show value text */ showValue?: boolean; /** Custom value formatter */ formatValue?: (value: number) => string; /** Tooltips for each value */ tooltips?: string[]; /** Additional CSS classes */ className?: string; } const StarIcon: React.FC<{ filled?: boolean; half?: boolean }> = ({ filled, half }) => ( {half ? ( <> ) : ( )} ); export const Rating: React.FC = ({ value: controlledValue, defaultValue = 0, max = 5, onChange, allowHalf = false, allowClear = true, disabled = false, readOnly = false, size = 'md', color = '#ffc107', emptyColor = '#e0e0e0', icon, emptyIcon, halfIcon, character, showValue = false, formatValue = (v) => v.toFixed(allowHalf ? 1 : 0), tooltips, className = '', }) => { const [internalValue, setInternalValue] = useState(defaultValue); const [hoverValue, setHoverValue] = useState(null); const value = controlledValue ?? internalValue; const displayValue = hoverValue ?? value; const handleClick = useCallback((newValue: number) => { if (disabled || readOnly) return; // Allow clear if clicking same value const finalValue = allowClear && newValue === value ? 0 : newValue; if (controlledValue === undefined) { setInternalValue(finalValue); } onChange?.(finalValue); }, [disabled, readOnly, allowClear, value, controlledValue, onChange]); const handleMouseMove = useCallback((e: React.MouseEvent, index: number) => { if (disabled || readOnly) return; const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; const isHalf = allowHalf && x < rect.width / 2; setHoverValue(isHalf ? index + 0.5 : index + 1); }, [disabled, readOnly, allowHalf]); const handleMouseLeave = () => { setHoverValue(null); }; const renderStar = (index: number) => { const starValue = index + 1; const isFilled = displayValue >= starValue; const isHalf = allowHalf && displayValue >= index + 0.5 && displayValue < starValue; const isActive = !disabled && !readOnly; let starIcon: React.ReactNode; if (character) { starIcon = character; } else if (isHalf && halfIcon) { starIcon = halfIcon; } else if (isFilled && icon) { starIcon = icon; } else if (!isFilled && emptyIcon) { starIcon = emptyIcon; } else { starIcon = ; } const tooltip = tooltips?.[index]; return ( handleClick(allowHalf && isHalf ? index + 0.5 : starValue)} onMouseMove={(e) => handleMouseMove(e, index)} title={tooltip} role="radio" aria-checked={value === starValue} > {starIcon} ); }; const classes = [ 'll-rating', `ll-rating-${size}`, disabled && 'll-rating-disabled', readOnly && 'll-rating-readonly', className, ].filter(Boolean).join(' '); return (
{Array.from({ length: max }, (_, i) => renderStar(i))}
{showValue && ( {formatValue(displayValue)} )}
); }; // Heart Rating variant export const HeartRating: React.FC> = (props) => { const HeartIcon = ({ filled }: { filled: boolean }) => ( ); return ( } emptyIcon={} /> ); }; // Emoji Rating variant export interface EmojiRatingProps extends Omit { emojis?: string[]; } export const EmojiRating: React.FC = ({ emojis = ['😞', '😕', '😐', '🙂', '😄'], ...props }) => { const max = emojis.length; return (
{emojis.map((emoji, index) => ( = index + 1 ? 'active' : ''}`} > {emoji} ))}
); };