Skip to content

Commit 82c874e

Browse files
authored
Merge pull request #6 from curtissearle/copilot/fix-5
Implement role spinner with animated selection and beautiful UI
2 parents 11bea0e + 717f4fb commit 82c874e

File tree

5 files changed

+202
-33
lines changed

5 files changed

+202
-33
lines changed

app/components/RoleSpinner.tsx

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
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+
}

app/page.tsx

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,24 @@
1+
import RoleSpinner from './components/RoleSpinner';
2+
13
export default function Home() {
24
return (
3-
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
4-
<main className="flex flex-col gap-8 row-start-2 items-center">
5-
<h1 className="text-4xl font-bold text-center">
6-
Run Until You See
7-
</h1>
8-
<p className="text-lg text-center max-w-2xl">
9-
Welcome to your Next.js webapp! This is the foundation for your &ldquo;Run Until You See&rdquo; application.
10-
</p>
11-
<div className="flex gap-4 items-center flex-col sm:flex-row">
12-
<div className="text-sm text-center sm:text-left">
13-
<p className="mb-2">
14-
Get started by editing{" "}
15-
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
16-
app/page.tsx
17-
</code>
18-
</p>
19-
<p>Save and see your changes instantly.</p>
20-
</div>
5+
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 flex items-center justify-center p-4">
6+
<div className="w-full max-w-2xl mx-auto">
7+
<div className="text-center mb-12">
8+
<h1 className="text-5xl md:text-6xl font-bold text-gray-900 dark:text-white mb-4">
9+
Run Until You See
10+
</h1>
11+
<p className="text-xl text-gray-600 dark:text-gray-300">
12+
Get your running challenge! Click Roll to discover something to look for on your next run.
13+
</p>
2114
</div>
22-
</main>
23-
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center text-sm">
24-
<p>Built with Next.js {" "}</p>
25-
<p></p>
26-
<p>TypeScript</p>
27-
<p></p>
28-
<p>Tailwind CSS</p>
29-
</footer>
15+
16+
<RoleSpinner />
17+
18+
<footer className="text-center mt-16 text-sm text-gray-500 dark:text-gray-400">
19+
<p>Built with Next.js • TypeScript • Tailwind CSS</p>
20+
</footer>
21+
</div>
3022
</div>
3123
);
3224
}

data/items.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[
2+
"Yellow car",
3+
"Red bicycle",
4+
"Black dog",
5+
"Blue mailbox",
6+
"White cat",
7+
"Green tree",
8+
"Purple flower",
9+
"Orange cone",
10+
"Pink house",
11+
"Gray squirrel",
12+
"Brown bird",
13+
"Silver lamppost",
14+
"Golden retriever",
15+
"Blue jay",
16+
"Red stop sign",
17+
"Yellow school bus",
18+
"Green park bench",
19+
"White fence",
20+
"Black motorcycle",
21+
"Orange traffic light",
22+
"Purple butterfly",
23+
"Blue trash can",
24+
"Red fire hydrant",
25+
"Gray pigeon",
26+
"Green lawn mower",
27+
"Yellow dandelion",
28+
"White van",
29+
"Brown deer",
30+
"Pink flamingo",
31+
"Silver car"
32+
]

package-lock.json

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
"format:check": "prettier --check .",
1414
"type-check": "tsc --noEmit",
1515
"setup": "./scripts/setup-dev.sh"
16-
"lint": "next lint"
1716
},
1817
"dependencies": {
1918
"next": "15.4.1",
@@ -45,8 +44,4 @@
4544
],
4645
"author": "Curtis Searle",
4746
"license": "Apache-2.0"
48-
}
49-
"tailwindcss": "^4",
50-
"typescript": "^5"
51-
}
5247
}

0 commit comments

Comments
 (0)