diff --git a/picker-wheel/src/components/Wheel.tsx b/picker-wheel/src/components/Wheel.tsx index 4286206..d01ece7 100644 --- a/picker-wheel/src/components/Wheel.tsx +++ b/picker-wheel/src/components/Wheel.tsx @@ -12,15 +12,19 @@ type Props = { } export function Wheel({ options, onWinner, spinning, setSpinning }: Props) { - const [rotation, setRotation] = useState(0) - const baseRotRef = useRef(0) + const [rotation, setRotation] = useState(() => Math.random() * 360) + const baseRotRef = useRef(Math.random() * 360) const spinButtonRef = useRef(null) const slices = useMemo(() => computeSlices(options.map(o => o.label)), [options]) useEffect(() => { - // Reset rotation if options change significantly - setRotation(prev => prev % 360) + // Reset to a new random rotation if options change significantly + if (options.length > 0) { + const newRandomRotation = Math.random() * 360 + setRotation(newRandomRotation) + baseRotRef.current = newRandomRotation + } }, [options.length]) const handleSpin = () => { @@ -34,9 +38,16 @@ export function Wheel({ options, onWinner, spinning, setSpinning }: Props) { const onTransitionEnd = () => { // Determine the winner based on final rotation - const normalized = ((rotation % 360) + 360) % 360 - const landingAngleFromTop = (360 - (normalized % 360)) % 360 - const idx = angleToIndex(landingAngleFromTop, slices.length) + // The pointer is at the top (12 o'clock position) + // Since slices now start at the top and go clockwise, we can directly + // calculate which slice is under the pointer + const normalizedRotation = ((rotation % 360) + 360) % 360 + + // The wheel rotates clockwise, so we need to find which slice + // is at the top after rotation + const pointerAngle = normalizedRotation % 360 + const idx = angleToIndex(pointerAngle, slices.length) + const winner = options[idx] if (winner) onWinner(winner) setSpinning(false) @@ -48,8 +59,16 @@ export function Wheel({ options, onWinner, spinning, setSpinning }: Props) { return (
+ {/* Pointer - moved outside and made more prominent */} +
+
+ +
{slices.map((slice, i) => ( - + ))} {slices.map((slice, i) => ( {options[i]?.label || ''} ))} - + {/* Center circle */} + - - {/* Pointer */} -
-
) diff --git a/picker-wheel/src/components/WinnerAnnouncement.tsx b/picker-wheel/src/components/WinnerAnnouncement.tsx index 383100c..4dfe07c 100644 --- a/picker-wheel/src/components/WinnerAnnouncement.tsx +++ b/picker-wheel/src/components/WinnerAnnouncement.tsx @@ -1,14 +1,34 @@ +'use client' + +import { useEffect, useState } from 'react' + type Props = { winner: string } export function WinnerAnnouncement({ winner }: Props) { + const [showAnimation, setShowAnimation] = useState(false) + + useEffect(() => { + if (winner) { + setShowAnimation(true) + const timer = setTimeout(() => setShowAnimation(false), 2000) + return () => clearTimeout(timer) + } + }, [winner]) + return ( -
+
{winner ? ( -
- Winner: {winner} +
+
🎉
+
🏆 Winner! 🏆
+
{winner}
+
Congratulations!
) : ( -

Spin the wheel to pick a winner.

+
+
🎲
+

Spin the wheel to pick a winner!

+
)}
) diff --git a/picker-wheel/src/lib/__tests__/wheel.test.ts b/picker-wheel/src/lib/__tests__/wheel.test.ts index 3ff04eb..43336ff 100644 --- a/picker-wheel/src/lib/__tests__/wheel.test.ts +++ b/picker-wheel/src/lib/__tests__/wheel.test.ts @@ -8,11 +8,29 @@ describe('wheel math', () => { expect(slices[0].path).toContain('A 100 100') // arc command exists }) - test('angleToIndex maps angles to indices', () => { - expect(angleToIndex(0, 4)).toBe(0) - expect(angleToIndex(89.9, 4)).toBe(0) - expect(angleToIndex(90, 4)).toBe(1) - expect(angleToIndex(359, 4)).toBe(3) + test('angleToIndex maps angles to indices correctly', () => { + // Test with 4 slices (90 degrees each) + expect(angleToIndex(0, 4)).toBe(0) // First slice + expect(angleToIndex(45, 4)).toBe(0) // Still first slice + expect(angleToIndex(89.9, 4)).toBe(0) // Still first slice + expect(angleToIndex(90, 4)).toBe(1) // Second slice + expect(angleToIndex(180, 4)).toBe(2) // Third slice + expect(angleToIndex(270, 4)).toBe(3) // Fourth slice + expect(angleToIndex(359, 4)).toBe(3) // Still fourth slice + expect(angleToIndex(360, 4)).toBe(0) // Back to first slice + }) + + test('angleToIndex works with different slice counts', () => { + // Test with 3 slices (120 degrees each) + expect(angleToIndex(0, 3)).toBe(0) + expect(angleToIndex(60, 3)).toBe(0) + expect(angleToIndex(120, 3)).toBe(1) + expect(angleToIndex(240, 3)).toBe(2) + + // Test with 6 slices (60 degrees each) + expect(angleToIndex(30, 6)).toBe(0) + expect(angleToIndex(60, 6)).toBe(1) + expect(angleToIndex(120, 6)).toBe(2) }) test('shuffleArray produces permutation', () => { diff --git a/picker-wheel/src/lib/wheel.ts b/picker-wheel/src/lib/wheel.ts index 2fd5908..8540cd5 100644 --- a/picker-wheel/src/lib/wheel.ts +++ b/picker-wheel/src/lib/wheel.ts @@ -10,6 +10,7 @@ export type Slice = { } // Create pie slice paths for n options, centered at 0,0 radius 100 +// Slices start at the top (12 o'clock) and go clockwise export function computeSlices(labels: string[], colors?: string[]): Slice[] { const n = Math.max(1, labels.length) const anglePer = 360 / n @@ -18,8 +19,9 @@ export function computeSlices(labels: string[], colors?: string[]): Slice[] { const toRad = (deg: number) => (deg * Math.PI) / 180 return labels.map((label, i) => { - const startAngle = i * anglePer - const endAngle = (i + 1) * anglePer + // Start at -90 degrees to position first slice at top (12 o'clock) + const startAngle = i * anglePer - 90 + const endAngle = (i + 1) * anglePer - 90 const largeArc = endAngle - startAngle > 180 ? 1 : 0