1
+ 'use client' ;
2
+
3
+ import { useState , useEffect , useRef } from 'react' ;
4
+ import items from '../../data/items.json' ;
5
+
6
+ type SpinnerState = 'idle' | 'spinning' | 'slowing' | 'finished' ;
7
+
8
+ export default function RoleSpinner ( ) {
9
+ const [ currentItem , setCurrentItem ] = useState ( 'Ready to start your run?' ) ;
10
+ const [ isSpinning , setIsSpinning ] = useState ( false ) ;
11
+ const [ spinnerState , setSpinnerState ] = useState < SpinnerState > ( 'idle' ) ;
12
+ const [ finalItem , setFinalItem ] = useState < string | null > ( null ) ;
13
+
14
+ const intervalRef = useRef < NodeJS . Timeout | null > ( null ) ;
15
+ const timeoutRef = useRef < NodeJS . Timeout | null > ( null ) ;
16
+
17
+ const getRandomItem = ( ) => {
18
+ return items [ Math . floor ( Math . random ( ) * items . length ) ] ;
19
+ } ;
20
+
21
+ const startSpinner = ( ) => {
22
+ if ( isSpinning ) return ;
23
+
24
+ setIsSpinning ( true ) ;
25
+ setSpinnerState ( 'spinning' ) ;
26
+ setFinalItem ( null ) ;
27
+
28
+ // Fast spinning phase (100ms intervals)
29
+ intervalRef . current = setInterval ( ( ) => {
30
+ setCurrentItem ( getRandomItem ( ) ) ;
31
+ } , 100 ) ;
32
+
33
+ // After 2 seconds, start slowing down
34
+ timeoutRef . current = setTimeout ( ( ) => {
35
+ setSpinnerState ( 'slowing' ) ;
36
+ if ( intervalRef . current ) {
37
+ clearInterval ( intervalRef . current ) ;
38
+ }
39
+
40
+ // Slowing down phase - increasing intervals
41
+ let interval = 150 ;
42
+ const slowDown = ( ) => {
43
+ setCurrentItem ( getRandomItem ( ) ) ;
44
+ interval += 50 ; // Increase interval each time to slow down
45
+
46
+ if ( interval <= 800 ) {
47
+ setTimeout ( slowDown , interval ) ;
48
+ } else {
49
+ // Final selection
50
+ const selected = getRandomItem ( ) ;
51
+ setCurrentItem ( selected ) ;
52
+ setFinalItem ( selected ) ;
53
+ setSpinnerState ( 'finished' ) ;
54
+ setIsSpinning ( false ) ;
55
+ }
56
+ } ;
57
+
58
+ slowDown ( ) ;
59
+ } , 2000 ) ;
60
+ } ;
61
+
62
+ const resetSpinner = ( ) => {
63
+ if ( intervalRef . current ) {
64
+ clearInterval ( intervalRef . current ) ;
65
+ }
66
+ if ( timeoutRef . current ) {
67
+ clearTimeout ( timeoutRef . current ) ;
68
+ }
69
+
70
+ setIsSpinning ( false ) ;
71
+ setSpinnerState ( 'idle' ) ;
72
+ setCurrentItem ( 'Ready to start your run?' ) ;
73
+ setFinalItem ( null ) ;
74
+ } ;
75
+
76
+ useEffect ( ( ) => {
77
+ return ( ) => {
78
+ if ( intervalRef . current ) {
79
+ clearInterval ( intervalRef . current ) ;
80
+ }
81
+ if ( timeoutRef . current ) {
82
+ clearTimeout ( timeoutRef . current ) ;
83
+ }
84
+ } ;
85
+ } , [ ] ) ;
86
+
87
+ return (
88
+ < div className = "bg-white dark:bg-gray-800 rounded-2xl shadow-2xl p-8 md:p-12" >
89
+ { /* Display Area */ }
90
+ < div className = "mb-8" >
91
+ < div className = "h-32 md:h-40 flex items-center justify-center bg-gradient-to-r from-indigo-50 to-purple-50 dark:from-gray-700 dark:to-gray-600 rounded-xl border-2 border-dashed border-indigo-200 dark:border-gray-500" >
92
+ < div className = "text-center px-4" >
93
+ { spinnerState === 'idle' ? (
94
+ < div className = "text-2xl md:text-3xl font-bold text-gray-700 dark:text-gray-300" >
95
+ { currentItem }
96
+ </ div >
97
+ ) : spinnerState === 'finished' && finalItem ? (
98
+ < div >
99
+ < div className = "text-lg md:text-xl text-gray-600 dark:text-gray-400 mb-2" >
100
+ Run until you see a:
101
+ </ div >
102
+ < div className = "text-2xl md:text-3xl font-bold text-green-600 dark:text-green-400 transform scale-110" >
103
+ { finalItem }
104
+ </ div >
105
+ </ div >
106
+ ) : (
107
+ < div >
108
+ < div className = "text-lg md:text-xl text-gray-600 dark:text-gray-400 mb-2" >
109
+ Run until you see a:
110
+ </ div >
111
+ < div className = "text-2xl md:text-3xl font-bold text-indigo-600 dark:text-indigo-400 animate-pulse" >
112
+ { currentItem }
113
+ </ div >
114
+ </ div >
115
+ ) }
116
+ { spinnerState === 'finished' && finalItem && (
117
+ < div className = "mt-2" >
118
+ < span className = "inline-flex items-center px-4 py-2 rounded-full text-sm font-medium bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200" >
119
+ 🎉 Start Running!
120
+ </ span >
121
+ </ div >
122
+ ) }
123
+ </ div >
124
+ </ div >
125
+ </ div >
126
+
127
+ { /* Action Button */ }
128
+ < div className = "text-center" >
129
+ { spinnerState === 'idle' || spinnerState === 'finished' ? (
130
+ < button
131
+ onClick = { spinnerState === 'finished' ? resetSpinner : startSpinner }
132
+ className = "bg-indigo-600 hover:bg-indigo-700 dark:bg-indigo-500 dark:hover:bg-indigo-600 text-white font-bold py-4 px-8 rounded-xl text-lg transition-all duration-200 transform hover:scale-105 active:scale-95 shadow-lg hover:shadow-xl"
133
+ >
134
+ { spinnerState === 'finished' ? 'Roll Again' : 'Roll' }
135
+ </ button >
136
+ ) : (
137
+ < div className = "flex flex-col items-center" >
138
+ < div className = "animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600 dark:border-indigo-400 mb-4" > </ div >
139
+ < span className = "text-gray-600 dark:text-gray-400 font-medium" >
140
+ { spinnerState === 'spinning' ? 'Rolling...' : 'Finalizing...' }
141
+ </ span >
142
+ </ div >
143
+ ) }
144
+ </ div >
145
+
146
+ { /* Stats */ }
147
+ < div className = "mt-8 text-center text-sm text-gray-500 dark:text-gray-400" >
148
+ < p > Choose from { items . length } things to see on your run</ p >
149
+ </ div >
150
+ </ div >
151
+ ) ;
152
+ }
0 commit comments