import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Animated, Easing, PanResponder, StyleProp, View, ViewStyle } from 'react-native'; import { tw } from '../../core/tailwind'; import StarIcon from '../icon/StarIcon'; type AnimationConfig = { easing?: (value: number) => number; duration?: number; delay?: number; scale?: number; }; type StarRatingProps = { rating: number; onChange: (rating: number) => void; minRating?: number; maxStars?: number; starSize?: number; onRatingStart?: () => void; onRatingEnd?: () => void; style?: StyleProp; starStyle?: StyleProp; animationConfig?: AnimationConfig; disabled?: boolean; }; const defaultAnimationConfig: Required = { easing: Easing.elastic(2), duration: 300, scale: 1.2, delay: 300, }; export function StarRating({ rating, maxStars = 5, starSize = 32, onChange, onRatingStart, onRatingEnd, animationConfig = defaultAnimationConfig, style, starStyle, disabled, }: StarRatingProps) { const width = useRef(); const ref = useRef(null); const [isInteracting, setInteracting] = useState(false); const getStars = (rating: number, maxStars: number) => { return [...Array(maxStars)].map((_, i) => { if (rating - i >= 1) { return 'full'; } return 'empty'; }); }; const handleInteraction = useCallback( (x: number) => { if (width.current) { const newRating = Math.max(0, Math.min(Math.round((x / width.current) * maxStars * 2 + 0.2) / 2, maxStars)); if (newRating !== rating) { onChange(Math.ceil(newRating)); } } }, [maxStars, onChange, rating] ); const panResponder = useMemo(() => { if (disabled) { return { panHandlers: {} }; } return PanResponder.create({ onStartShouldSetPanResponder: () => true, onStartShouldSetPanResponderCapture: () => true, onMoveShouldSetPanResponder: () => true, onMoveShouldSetPanResponderCapture: () => true, onPanResponderMove: (_e) => { // if (enableSwiping) { // handleInteraction(e.nativeEvent.locationX); // } }, onPanResponderStart: (e) => { onRatingStart?.(); handleInteraction(e.nativeEvent.locationX); setInteracting(true); }, onPanResponderEnd: () => { onRatingEnd?.(); setTimeout(() => { setInteracting(false); }, animationConfig.delay || defaultAnimationConfig.delay); }, }); }, [animationConfig.delay, handleInteraction, onRatingStart, onRatingEnd]); return ( { if (ref.current) { ref.current.measure((_x, _y, w, _h) => (width.current = w)); } }} > {getStars(rating, maxStars).map((starType, i) => { return ( = 0.5} animationConfig={animationConfig} style={starStyle} > ); })} ); } type AnimatedIconProps = { active: boolean; children: React.ReactElement; animationConfig: AnimationConfig; style?: StyleProp; }; const AnimatedIcon: React.FC = ({ active, animationConfig, children, style }) => { const { scale = defaultAnimationConfig.scale, easing = defaultAnimationConfig.easing, duration = defaultAnimationConfig.duration, } = animationConfig; const animatedSize = useRef(new Animated.Value(active ? scale : 1)); useEffect(() => { const animation = Animated.timing(animatedSize.current, { toValue: active ? scale : 1, useNativeDriver: true, easing, duration, }); animation.start(); return animation.stop; }, [active, scale, easing, duration]); return ( {children} ); };