import { useEffect, useRef, useState, useMemo, memo } from 'react' import type { WheelPrize } from '../../api/wheel' interface FortuneWheelProps { prizes: WheelPrize[] isSpinning: boolean targetRotation: number | null onSpinComplete: () => void } // Pre-generate sparkle positions to avoid recalculating on each render const SPARKLE_POSITIONS = Array.from({ length: 8 }, (_, i) => ({ top: `${20 + (i * 10) % 60}%`, left: `${15 + (i * 13) % 70}%`, delay: `${i * 0.15}s`, })) const FortuneWheel = memo(function FortuneWheel({ prizes, isSpinning, targetRotation, onSpinComplete, }: FortuneWheelProps) { const wheelRef = useRef(null) const [currentRotation, setCurrentRotation] = useState(0) const [lightPhase, setLightPhase] = useState(0) // Animated lights effect - use phase instead of random array (less re-renders) useEffect(() => { if (isSpinning) { const interval = setInterval(() => { setLightPhase(p => (p + 1) % 3) // Just toggle phase 0-1-2 }, 250) // Slower interval = better performance return () => clearInterval(interval) } else { setLightPhase(0) } }, [isSpinning]) useEffect(() => { if (isSpinning && targetRotation !== null && wheelRef.current) { setCurrentRotation(targetRotation) const timeout = setTimeout(() => { onSpinComplete() }, 5000) return () => clearTimeout(timeout) } }, [isSpinning, targetRotation, onSpinComplete]) // Memoize light pattern calculation const lightPattern = useMemo(() => { return Array.from({ length: 20 }, (_, i) => { if (!isSpinning) return i % 2 === 0 return (i + lightPhase) % 3 !== 0 }) }, [isSpinning, lightPhase]) if (prizes.length === 0) { return (

No prizes configured

) } const size = 400 const center = size / 2 const outerRadius = size / 2 - 20 const innerRadius = outerRadius - 15 const prizeRadius = innerRadius - 5 const sectorAngle = 360 / prizes.length const hubRadius = 45 const createSectorPath = (index: number) => { const startAngle = (index * sectorAngle - 90) * (Math.PI / 180) const endAngle = ((index + 1) * sectorAngle - 90) * (Math.PI / 180) const x1 = center + prizeRadius * Math.cos(startAngle) const y1 = center + prizeRadius * Math.sin(startAngle) const x2 = center + prizeRadius * Math.cos(endAngle) const y2 = center + prizeRadius * Math.sin(endAngle) const x1Inner = center + hubRadius * Math.cos(startAngle) const y1Inner = center + hubRadius * Math.sin(startAngle) const x2Inner = center + hubRadius * Math.cos(endAngle) const y2Inner = center + hubRadius * Math.sin(endAngle) const largeArc = sectorAngle > 180 ? 1 : 0 return `M ${x1Inner} ${y1Inner} L ${x1} ${y1} A ${prizeRadius} ${prizeRadius} 0 ${largeArc} 1 ${x2} ${y2} L ${x2Inner} ${y2Inner} A ${hubRadius} ${hubRadius} 0 ${largeArc} 0 ${x1Inner} ${y1Inner} Z` } // Position for emoji - closer to outer edge const getEmojiPosition = (index: number) => { const angle = ((index * sectorAngle + sectorAngle / 2) - 90) * (Math.PI / 180) const emojiRadius = prizeRadius * 0.75 return { x: center + emojiRadius * Math.cos(angle), y: center + emojiRadius * Math.sin(angle), rotation: index * sectorAngle + sectorAngle / 2, } } // Position for text - between hub and emoji const getTextPosition = (index: number) => { const angle = ((index * sectorAngle + sectorAngle / 2) - 90) * (Math.PI / 180) const textRadius = prizeRadius * 0.45 return { x: center + textRadius * Math.cos(angle), y: center + textRadius * Math.sin(angle), rotation: index * sectorAngle + sectorAngle / 2, } } // Alternate colors for sectors const getSectorColors = (index: number, baseColor?: string) => { if (baseColor) return baseColor const colors = [ '#8B5CF6', '#EC4899', '#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#6366F1', '#14B8A6' ] return colors[index % colors.length] } // Truncate text intelligently const truncateText = (text: string, maxLen: number) => { if (text.length <= maxLen) return text return text.substring(0, maxLen - 1) + '..' } // Calculate max text length based on number of sectors const maxTextLength = prizes.length <= 4 ? 12 : prizes.length <= 6 ? 10 : 8 return (
{/* Outer glow effect */}
{/* Pointer */}
{/* Main Wheel */}
{/* Sector gradients */} {prizes.map((prize, index) => { const color = getSectorColors(index, prize.color) return ( ) })} {/* Outer ring gradient */} {/* Hub gradient */} {/* Text shadow filter */} {/* Background shadow */} {/* Outer decorative ring */} {/* Inner ring border */} {/* LED lights on outer ring */} {Array.from({ length: 20 }).map((_, i) => { const angle = (i * 18 - 90) * (Math.PI / 180) const dotX = center + outerRadius * Math.cos(angle) const dotY = center + outerRadius * Math.sin(angle) const isLit = lightPattern[i] ?? (i % 2 === 0) return ( {isLit && ( )} ) })} {/* Rotating wheel group */} {/* Sectors */} {prizes.map((prize, index) => ( ))} {/* Sector dividers */} {prizes.map((_, index) => { const angle = (index * sectorAngle - 90) * (Math.PI / 180) const x1 = center + hubRadius * Math.cos(angle) const y1 = center + hubRadius * Math.sin(angle) const x2 = center + prizeRadius * Math.cos(angle) const y2 = center + prizeRadius * Math.sin(angle) return ( ) })} {/* Prize content - Emoji */} {prizes.map((prize, index) => { const pos = getEmojiPosition(index) return ( {prize.emoji} ) })} {/* Prize content - Text */} {prizes.map((prize, index) => { const pos = getTextPosition(index) const displayText = truncateText(prize.display_name, maxTextLength) return ( {displayText} ) })} {/* Center hub */} {/* Hub inner decoration */} {/* Hub shine */} {/* Center button */} {/* Center text */} {isSpinning ? '...' : 'SPIN'} {/* Spinning overlay glow */} {isSpinning && (
)}
{/* Sparkle effects when spinning - optimized with pre-calculated positions */} {isSpinning && (
{SPARKLE_POSITIONS.map((pos, i) => (
))}
)}
) }) export default FortuneWheel