+33
-3
src/App.tsx
+33
-3
src/App.tsx
···
9
9
import { useSearch } from "./hooks/useSearch";
10
10
import { useFollow } from "./hooks/useFollows";
11
11
import { useFileUpload } from "./hooks/useFileUpload";
12
+
import { useTheme } from "./hooks/useTheme";
13
+
import ThemeControls from "./components/ThemeControls";
14
+
import Firefly from "./components/Firefly";
15
+
12
16
13
17
export default function App() {
14
18
// Auth hook
···
21
25
login,
22
26
logout,
23
27
} = useAuth();
28
+
29
+
// Theme hook
30
+
const { isDark, reducedMotion, toggleTheme, toggleMotion } = useTheme();
24
31
25
32
// Add state to track current platform
26
33
const [currentPlatform, setCurrentPlatform] = useState<string>('tiktok');
···
161
168
};
162
169
163
170
return (
164
-
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-purple-50 dark:from-gray-900 dark:to-gray-800">
171
+
<div className="min-h-screen relative overflow-hidden">
172
+
{/* Firefly particles - only render if motion not reduced */}
173
+
{!reducedMotion && (
174
+
<div className="fixed inset-0 pointer-events-none" aria-hidden="true">
175
+
{[...Array(15)].map((_, i) => (
176
+
<Firefly key={i} delay={i * 0.5} duration={3 + Math.random() * 2} />
177
+
))}
178
+
</div>
179
+
)}
180
+
181
+
{/* Status message for screen readers */}
165
182
<div
166
183
role="status"
167
184
aria-live="polite"
···
171
188
{statusMessage}
172
189
</div>
173
190
191
+
{/* Skip to main content link */}
174
192
<a
175
193
href="#main-content"
176
-
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:bg-blue-600 focus:text-white focus:px-4 focus:py-2 focus:rounded-lg"
194
+
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:bg-firefly-orange focus:text-white focus:px-4 focus:py-2 focus:rounded-lg"
177
195
>
178
196
Skip to main content
179
197
</a>
···
183
201
{currentStep === 'checking' && (
184
202
<div className="p-6 max-w-md mx-auto mt-8">
185
203
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-8 text-center space-y-4">
186
-
<div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-2xl mx-auto flex items-center justify-center">
204
+
<div className="w-16 h-16 bbg-firefly-banner dark:bg-firefly-banner-dark text-white rounded-2xl mx-auto flex items-center justify-center">
187
205
<ArrowRight className="w-8 h-8 text-white animate-pulse" />
188
206
</div>
189
207
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">Loading...</h2>
···
198
216
onSubmit={handleLogin}
199
217
session={session}
200
218
onNavigate={setCurrentStep}
219
+
reducedMotion={reducedMotion}
201
220
/>
202
221
)}
203
222
···
210
229
onFileUpload={processFileUpload}
211
230
onLoadUpload={handleLoadUpload}
212
231
currentStep={currentStep}
232
+
reducedMotion={reducedMotion}
233
+
isDark={isDark}
234
+
onToggleTheme={toggleTheme}
235
+
onToggleMotion={toggleMotion}
213
236
/>
214
237
)}
215
238
···
222
245
searchProgress={searchProgress}
223
246
currentStep={currentStep}
224
247
sourcePlatform={currentPlatform}
248
+
isDark={isDark}
249
+
onToggleTheme={toggleTheme}
250
+
onToggleMotion={toggleMotion}
225
251
/>
226
252
)}
227
253
···
243
269
isFollowing={isFollowing}
244
270
currentStep={currentStep}
245
271
sourcePlatform={currentPlatform}
272
+
reducedMotion={reducedMotion}
273
+
isDark={isDark}
274
+
onToggleTheme={toggleTheme}
275
+
onToggleMotion={toggleMotion}
246
276
/>
247
277
)}
248
278
</main>
+80
-41
src/components/AppHeader.tsx
+80
-41
src/components/AppHeader.tsx
···
1
1
import { useState, useEffect, useRef } from "react";
2
2
import { Heart, Home, LogOut, ChevronDown } from "lucide-react";
3
+
import ThemeControls from "./ThemeControls";
3
4
4
5
interface atprotoSession {
5
6
did: string;
···
14
15
onLogout: () => void;
15
16
onNavigate: (step: 'home' | 'login') => void;
16
17
currentStep: string;
18
+
isDark?: boolean;
19
+
reducedMotion?: boolean;
20
+
onToggleTheme?: () => void;
21
+
onToggleMotion?: () => void;
17
22
}
18
23
19
-
export default function AppHeader({ session, onLogout, onNavigate, currentStep }: AppHeaderProps) {
24
+
export default function AppHeader({
25
+
session,
26
+
onLogout,
27
+
onNavigate,
28
+
currentStep,
29
+
isDark = false,
30
+
reducedMotion = false,
31
+
onToggleTheme,
32
+
onToggleMotion
33
+
}: AppHeaderProps) {
20
34
const [showMenu, setShowMenu] = useState(false);
21
35
const menuRef = useRef<HTMLDivElement>(null);
22
36
···
31
45
}, []);
32
46
33
47
return (
34
-
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
48
+
<div className="bg-white/95 dark:bg-slate-800/95 border-b-2 border-slate-200 dark:border-slate-700 backdrop-blur-sm relative z-[100]">
35
49
<div className="max-w-6xl mx-auto px-4 py-3">
36
50
<div className="flex items-center justify-between">
37
-
<button onClick={() => onNavigate(session ? 'home' : 'login')} className="flex items-center space-x-3 hover:opacity-80 transition-opacity focus:outline-none focus:ring-2 focus:ring-blue-500 rounded-lg px-2 py-1">
38
-
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-xl flex items-center justify-center">
39
-
<Heart className="w-5 h-5 text-white" />
51
+
<button
52
+
onClick={() => onNavigate(session ? 'home' : 'login')}
53
+
className="flex items-center space-x-3 hover:opacity-80 transition-opacity focus:outline-none focus:ring-2 focus:ring-firefly-orange rounded-lg px-2 py-1"
54
+
>
55
+
<div className="w-10 h-10 bg-gradient-to-br from-firefly-amber via-firefly-orange to-firefly-pink rounded-xl flex items-center justify-center shadow-md">
56
+
<Heart className="w-5 h-5 text-slate-900" />
40
57
</div>
41
-
<h1 className="text-xl font-bold text-gray-900 dark:text-gray-100">ATlast</h1>
58
+
<h1 className="text-xl font-bold text-slate-900 dark:text-slate-100">ATlast</h1>
42
59
</button>
43
60
44
-
{session && (
45
-
<div className="relative" ref={menuRef}>
46
-
<button onClick={() => setShowMenu(!showMenu)} className="flex items-center space-x-3 px-3 py-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500">
47
-
{session?.avatar ? (
48
-
<img src={session.avatar} alt="" className="w-8 h-8 rounded-full object-cover" />
49
-
) : (
50
-
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center">
51
-
<span className="text-white font-bold text-sm">{session?.handle?.charAt(0).toUpperCase()}</span>
61
+
<div className="flex items-center space-x-4">
62
+
{onToggleTheme && onToggleMotion && (
63
+
<ThemeControls
64
+
isDark={isDark}
65
+
reducedMotion={reducedMotion}
66
+
onToggleTheme={onToggleTheme}
67
+
onToggleMotion={onToggleMotion}
68
+
/>
69
+
)}
70
+
{session && (
71
+
<div className="relative z-[9999]" ref={menuRef}>
72
+
<button
73
+
onClick={() => setShowMenu(!showMenu)}
74
+
className="flex items-center space-x-3 px-3 py-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors focus:outline-none focus:ring-2 focus:ring-firefly-orange"
75
+
>
76
+
{session?.avatar ? (
77
+
<img src={session.avatar} alt="" className="w-8 h-8 rounded-full object-cover" />
78
+
) : (
79
+
<div className="w-8 h-8 bg-gradient-to-br from-firefly-cyan to-blue-500 rounded-full flex items-center justify-center shadow-sm">
80
+
<span className="text-white font-bold text-sm">{session?.handle?.charAt(0).toUpperCase()}</span>
81
+
</div>
82
+
)}
83
+
<span className="text-sm font-medium text-slate-900 dark:text-slate-100 hidden sm:inline">@{session?.handle}</span>
84
+
<ChevronDown className={`w-4 h-4 text-slate-600 dark:text-slate-400 transition-transform ${showMenu ? 'rotate-180' : ''}`} />
85
+
</button>
86
+
87
+
{showMenu && (
88
+
<div className="absolute right-0 mt-2 w-64 bg-white dark:bg-slate-800 rounded-lg shadow-lg border-2 border-slate-200 dark:border-slate-700 py-2 z-[9999]">
89
+
<div className="px-4 py-3 border-b-2 border-slate-200 dark:border-slate-700">
90
+
<div className="font-semibold text-slate-900 dark:text-slate-100">{session?.displayName || session.handle}</div>
91
+
<div className="text-sm text-slate-600 dark:text-slate-400">@{session?.handle}</div>
92
+
</div>
93
+
<button
94
+
onClick={() => { setShowMenu(false); onNavigate('home'); }}
95
+
className="w-full flex items-center space-x-3 px-4 py-2 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors text-left"
96
+
>
97
+
<Home className="w-4 h-4 text-slate-600 dark:text-slate-400" />
98
+
<span className="text-slate-900 dark:text-slate-100">Dashboard</span>
99
+
</button>
100
+
<button
101
+
onClick={() => { setShowMenu(false); onNavigate('login'); }}
102
+
className="w-full flex items-center space-x-3 px-4 py-2 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors text-left"
103
+
>
104
+
<Heart className="w-4 h-4 text-slate-600 dark:text-slate-400" />
105
+
<span className="text-slate-900 dark:text-slate-100">About</span>
106
+
</button>
107
+
<div className="border-t-2 border-slate-200 dark:border-slate-700 my-2"></div>
108
+
<button
109
+
onClick={() => { setShowMenu(false); onLogout(); }}
110
+
className="w-full flex items-center space-x-3 px-4 py-2 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors text-left text-red-600 dark:text-red-400"
111
+
>
112
+
<LogOut className="w-4 h-4" />
113
+
<span>Log out</span>
114
+
</button>
52
115
</div>
53
116
)}
54
-
<span className="text-sm font-medium text-gray-900 dark:text-gray-100 hidden sm:inline">@{session?.handle}</span>
55
-
<ChevronDown className={`w-4 h-4 text-gray-500 transition-transform ${showMenu ? 'rotate-180' : ''}`} />
56
-
</button>
57
-
58
-
{showMenu && (
59
-
<div className="absolute right-0 mt-2 w-64 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-2 z-50">
60
-
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
61
-
<div className="font-medium text-gray-900 dark:text-gray-100">{session?.displayName || session.handle}</div>
62
-
<div className="text-sm text-gray-500 dark:text-gray-400">@{session?.handle}</div>
63
-
</div>
64
-
<button onClick={() => { setShowMenu(false); onNavigate('home'); }} className="w-full flex items-center space-x-3 px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors text-left">
65
-
<Home className="w-4 h-4 text-gray-500" />
66
-
<span className="text-gray-900 dark:text-gray-100">Dashboard</span>
67
-
</button>
68
-
<button onClick={() => { setShowMenu(false); onNavigate('login'); }} className="w-full flex items-center space-x-3 px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors text-left">
69
-
<Heart className="w-4 h-4 text-gray-500" />
70
-
<span className="text-gray-900 dark:text-gray-100">About</span>
71
-
</button>
72
-
<div className="border-t border-gray-200 dark:border-gray-700 my-2"></div>
73
-
<button onClick={() => { setShowMenu(false); onLogout(); }} className="w-full flex items-center space-x-3 px-4 py-2 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors text-left text-red-600 dark:text-red-400">
74
-
<LogOut className="w-4 h-4" />
75
-
<span>Log out</span>
76
-
</button>
77
-
</div>
78
-
)}
79
-
</div>
80
-
)}
117
+
</div>
118
+
)}
119
+
</div>
81
120
</div>
82
121
</div>
83
122
</div>
+22
src/components/Firefly.tsx
+22
src/components/Firefly.tsx
···
1
+
interface FireflyProps {
2
+
delay?: number;
3
+
duration?: number;
4
+
}
5
+
6
+
export default function Firefly({ delay = 0, duration = 3 }: FireflyProps) {
7
+
const style = {
8
+
animation: `float ${duration}s ease-in-out ${delay}s infinite`,
9
+
left: `${Math.random() * 100}%`,
10
+
top: `${Math.random() * 100}%`,
11
+
};
12
+
13
+
return (
14
+
<div
15
+
className="absolute w-1 h-1 bg-firefly-amber dark:bg-firefly-glow rounded-full opacity-40 pointer-events-none"
16
+
style={style}
17
+
aria-hidden="true"
18
+
>
19
+
<div className="absolute inset-0 bg-firefly-glow dark:bg-firefly-amber rounded-full animate-pulse blur-sm" />
20
+
</div>
21
+
);
22
+
}
+13
-13
src/components/SearchResultCard.tsx
+13
-13
src/components/SearchResultCard.tsx
···
27
27
return (
28
28
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-sm overflow-hidden">
29
29
{/* Source User */}
30
-
<div className="px-4 py-3 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
30
+
<div className="px-4 py-3 bg-slate-50 dark:bg-slate-900/50 border-b-2 border-slate-200 dark:border-slate-700">
31
31
<div className="flex items-start justify-between gap-2">
32
32
<div className="flex-1 min-w-0">
33
-
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-1">
34
-
<span className="font-bold text-gray-900 dark:text-gray-100 truncate">
33
+
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
34
+
<span className="font-bold text-slate-900 dark:text-slate-100 truncate text-base">
35
35
@{result.sourceUser.username}
36
36
</span>
37
-
<span className="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap">
37
+
<span className="text-sm text-slate-700 dark:text-slate-300 whitespace-nowrap">
38
38
from {platform.name}
39
39
</span>
40
40
</div>
41
41
</div>
42
-
<div className={`text-xs px-2 py-1 rounded-full ${platform.accentBg} text-white whitespace-nowrap flex-shrink-0`}>
42
+
<div className={`text-xs px-2 py-1 rounded-full bg-indigo-700 dark:bg-pink-700/70 text-white whitespace-nowrap flex-shrink-0`}>
43
43
{result.atprotoMatches.length} {result.atprotoMatches.length === 1 ? 'match' : 'matches'}
44
44
</div>
45
45
</div>
···
66
66
{match.avatar ? (
67
67
<img
68
68
src={match.avatar}
69
-
alt="User avatar, description not provided"
69
+
alt="User avatar"
70
70
className="w-12 h-12 rounded-full object-cover flex-shrink-0"
71
71
/>
72
72
) : (
···
84
84
{match.displayName}
85
85
</div>
86
86
)}
87
-
<div className="text-sm text-gray-600 dark:text-gray-400">
87
+
<div className="text-sm text-gray-800 dark:text-gray-200">
88
88
@{match.handle}
89
89
</div>
90
90
{match.description && (
91
-
<div className="text-sm text-gray-500 dark:text-gray-400 mt-1 line-clamp-2">{match.description}</div>
91
+
<div className="text-sm text-gray-700 dark:text-gray-300 mt-1 line-clamp-2">{match.description}</div>
92
92
)}
93
93
{(match.postCount || match.followerCount) && (
94
-
<div className="flex items-center space-x-3 mt-2 text-xs text-gray-500 dark:text-gray-400">
94
+
<div className="flex items-center space-x-3 mt-2 text-xs text-gray-700 dark:text-gray-300">
95
95
{match.postCount && match.postCount > 0 && (
96
96
<span>{match.postCount.toLocaleString()} posts</span>
97
97
)}
···
115
115
isFollowed
116
116
? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 cursor-not-allowed opacity-60'
117
117
: isSelected
118
-
? 'bg-blue-600 text-white'
119
-
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
118
+
? 'bg-cyan-500 dark:bg-cyan-300 text-white dark:text-slate-700 shadow-md'
119
+
: 'bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-300 dark:hover:bg-slate-600'
120
120
}`}
121
121
title={isFollowed ? 'Already followed' : isSelected ? 'Selected to follow' : 'Select to follow'}
122
122
>
···
134
134
{hasMoreMatches && (
135
135
<button
136
136
onClick={onToggleExpand}
137
-
className="w-full py-2 text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium transition-colors flex items-center justify-center space-x-1"
137
+
className="w-full py-2 text-sm text-cyan-700 hover:text-cyan-900 dark:text-cyan-400 dark:hover:text-cyan-200 font-medium transition-colors flex items-center justify-center space-x-1"
138
138
>
139
-
<span>{isExpanded ? 'Show less' : `Show ${result.atprotoMatches.length - 1} more ${result.atprotoMatches.length - 1 === 1 ? 'match' : 'matches'}`}</span>
139
+
<span>{isExpanded ? 'Show less' : `Show ${result.atprotoMatches.length - 1} more ${result.atprotoMatches.length - 1 === 1 ? 'option' : 'options'}`}</span>
140
140
<ChevronDown className={`w-4 h-4 transition-transform ${isExpanded ? 'rotate-180' : ''}`} />
141
141
</button>
142
142
)}
+43
src/components/ThemeControls.tsx
+43
src/components/ThemeControls.tsx
···
1
+
import { Sun, Moon, Pause, Play } from 'lucide-react';
2
+
3
+
interface ThemeControlsProps {
4
+
isDark: boolean;
5
+
reducedMotion: boolean;
6
+
onToggleTheme: () => void;
7
+
onToggleMotion: () => void;
8
+
}
9
+
10
+
export default function ThemeControls({
11
+
isDark,
12
+
reducedMotion,
13
+
onToggleTheme,
14
+
onToggleMotion
15
+
}: ThemeControlsProps) {
16
+
return (
17
+
<div className="flex items-center space-x-2">
18
+
<button
19
+
onClick={onToggleMotion}
20
+
className="p-2 bg-white/90 dark:bg-slate-800/90 backdrop-blur-sm rounded-lg border border-slate-200 dark:border-slate-700 hover:bg-white dark:hover:bg-slate-700 transition-colors shadow-lg"
21
+
aria-label={reducedMotion ? "Enable animations" : "Reduce motion"}
22
+
title={reducedMotion ? "Enable animations" : "Reduce motion"}
23
+
>
24
+
{reducedMotion ? (
25
+
<Play className="w-5 h-5 text-slate-700 dark:text-slate-300" />
26
+
) : (
27
+
<Pause className="w-5 h-5 text-slate-700 dark:text-slate-300" />
28
+
)}
29
+
</button>
30
+
<button
31
+
onClick={onToggleTheme}
32
+
className="p-2 bg-white/90 dark:bg-slate-800/90 backdrop-blur-sm rounded-lg border border-slate-200 dark:border-slate-700 hover:bg-white dark:hover:bg-slate-700 transition-colors shadow-lg"
33
+
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
34
+
>
35
+
{isDark ? (
36
+
<Sun className="w-5 h-5 text-firefly-amber" />
37
+
) : (
38
+
<Moon className="w-5 h-5 text-slate-700" />
39
+
)}
40
+
</button>
41
+
</div>
42
+
);
43
+
}
+37
src/hooks/useTheme.ts
+37
src/hooks/useTheme.ts
···
1
+
import { useState, useEffect } from 'react';
2
+
3
+
export function useTheme() {
4
+
const [isDark, setIsDark] = useState(() => {
5
+
// Check localStorage first, then system preference
6
+
const stored = localStorage.getItem('theme');
7
+
if (stored) return stored === 'dark';
8
+
return window.matchMedia('(prefers-color-scheme: dark)').matches;
9
+
});
10
+
11
+
const [reducedMotion, setReducedMotion] = useState(() => {
12
+
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
13
+
});
14
+
15
+
useEffect(() => {
16
+
// Apply theme to document
17
+
if (isDark) {
18
+
document.documentElement.classList.add('dark');
19
+
} else {
20
+
document.documentElement.classList.remove('dark');
21
+
}
22
+
localStorage.setItem('theme', isDark ? 'dark' : 'light');
23
+
}, [isDark]);
24
+
25
+
useEffect(() => {
26
+
// Listen for system motion preference changes
27
+
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
28
+
const handler = (e: MediaQueryListEvent) => setReducedMotion(e.matches);
29
+
mediaQuery.addEventListener('change', handler);
30
+
return () => mediaQuery.removeEventListener('change', handler);
31
+
}, []);
32
+
33
+
const toggleTheme = () => setIsDark(!isDark);
34
+
const toggleMotion = () => setReducedMotion(!reducedMotion);
35
+
36
+
return { isDark, reducedMotion, toggleTheme, toggleMotion };
37
+
}
+33
-1
src/index.css
+33
-1
src/index.css
···
5
5
@layer base {
6
6
body {
7
7
font-family: system-ui, sans-serif;
8
-
@apply bg-gray-50 text-gray-900 dark:bg-gray-900 dark:text-gray-100;
8
+
@apply bg-gradient-to-br from-amber-50 via-orange-50 to-pink-50
9
+
dark:from-indigo-950 dark:via-purple-900 dark:to-slate-900
10
+
text-slate-900 dark:text-slate-100
11
+
transition-colors duration-300;
9
12
}
10
13
11
14
button {
12
15
cursor: pointer;
16
+
}
17
+
}
18
+
19
+
/* Firefly animation keyframes */
20
+
@keyframes float {
21
+
0%, 100% {
22
+
transform: translate(0, 0) scale(1);
23
+
opacity: 0.3;
24
+
}
25
+
25% {
26
+
transform: translate(10px, -20px) scale(1.2);
27
+
opacity: 0.8;
28
+
}
29
+
50% {
30
+
transform: translate(-5px, -40px) scale(1);
31
+
opacity: 0.5;
32
+
}
33
+
75% {
34
+
transform: translate(15px, -25px) scale(1.1);
35
+
opacity: 0.9;
36
+
}
37
+
}
38
+
39
+
@keyframes glow-pulse {
40
+
0%, 100% {
41
+
box-shadow: 0 0 20px rgba(251, 191, 36, 0.3);
42
+
}
43
+
50% {
44
+
box-shadow: 0 0 40px rgba(251, 191, 36, 0.6), 0 0 60px rgba(251, 191, 36, 0.3);
13
45
}
14
46
}
+61
-33
src/pages/Home.tsx
+61
-33
src/pages/Home.tsx
···
1
-
import { Upload, History, FileText } from "lucide-react";
1
+
import { Upload, History, FileText, Sparkles } from "lucide-react";
2
2
import { useState, useEffect, useRef } from "react";
3
3
import AppHeader from "../components/AppHeader";
4
4
import PlatformSelector from "../components/PlatformSelector";
···
20
20
onFileUpload: (e: React.ChangeEvent<HTMLInputElement>, platform: string) => void;
21
21
onLoadUpload: (uploadId: string) => void;
22
22
currentStep: string;
23
+
reducedMotion?: boolean;
24
+
isDark?: boolean;
25
+
onToggleTheme?: () => void;
26
+
onToggleMotion?: () => void;
23
27
}
24
28
25
29
export default function HomePage({
···
28
32
onNavigate,
29
33
onFileUpload,
30
34
onLoadUpload,
31
-
currentStep
35
+
currentStep,
36
+
reducedMotion = false,
37
+
isDark = false,
38
+
onToggleTheme,
39
+
onToggleMotion
32
40
}: HomePageProps) {
33
41
const [uploads, setUploads] = useState<UploadType[]>([]);
34
42
const [isLoading, setIsLoading] = useState(true);
···
80
88
};
81
89
82
90
return (
83
-
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
84
-
<AppHeader session={session} onLogout={onLogout} onNavigate={onNavigate} currentStep={currentStep} />
91
+
<div className="min-h-screen">
92
+
<AppHeader
93
+
session={session}
94
+
onLogout={onLogout}
95
+
onNavigate={onNavigate}
96
+
currentStep={currentStep}
97
+
isDark={isDark}
98
+
reducedMotion={reducedMotion}
99
+
onToggleTheme={onToggleTheme}
100
+
onToggleMotion={onToggleMotion}
101
+
/>
85
102
86
103
<div className="max-w-4xl mx-auto px-4 py-8 space-y-6">
87
104
{/* Upload Section */}
88
-
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
105
+
<div className="bg-white/95 dark:bg-slate-800/95 backdrop-blur-xl rounded-2xl shadow-lg p-6 border-2 border-slate-200 dark:border-slate-700">
89
106
<div className="flex items-center space-x-3 mb-4">
90
-
<Upload className="w-6 h-6 text-blue-600 dark:text-blue-400" />
91
-
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">
92
-
Upload Following Data
93
-
</h2>
107
+
<div
108
+
className={`w-12 h-12 bg-gradient-to-br from-firefly-amber to-firefly-orange rounded-xl flex items-center justify-center shadow-md ${
109
+
reducedMotion ? '' : 'animate-glow-pulse'
110
+
}`}
111
+
>
112
+
<Upload className="w-6 h-6 text-slate-900" />
113
+
</div>
114
+
<div>
115
+
<h2 className="text-xl font-bold text-slate-900 dark:text-slate-100">
116
+
Light Up Your Network
117
+
</h2>
118
+
<p className="text-sm text-slate-700 dark:text-slate-300">
119
+
Upload your data to find your fireflies
120
+
</p>
121
+
</div>
94
122
</div>
95
-
<p className="text-gray-600 dark:text-gray-400 mb-6">
96
-
Click a platform below to upload your exported data and find matches on the ATmosphere
123
+
<p className="text-slate-700 dark:text-slate-300 mb-6">
124
+
Click a platform below to upload your exported data and discover matches in the ATmosphere
97
125
</p>
98
126
99
127
<PlatformSelector onPlatformSelect={handlePlatformSelect} />
···
109
137
aria-label="Upload following data file"
110
138
/>
111
139
112
-
<div className="mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-xl">
113
-
<p className="text-sm text-blue-900 dark:text-blue-300">
114
-
💡 <strong>How to get your data:</strong>
140
+
<div className="mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-xl border-2 border-blue-200 dark:border-blue-800/30">
141
+
<p className="text-sm text-blue-900 dark:text-blue-300 font-semibold">
142
+
💡 How to get your data:
115
143
</p>
116
144
<p className="text-sm text-blue-900 dark:text-blue-300 mt-2">
117
145
<strong>TikTok:</strong> Profile → Settings → Account → Download your data → Upload Following.txt
···
123
151
</div>
124
152
125
153
{/* Upload History Section */}
126
-
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
154
+
<div className="bg-white/95 dark:bg-slate-800/95 backdrop-blur-xl rounded-2xl shadow-lg p-6 border-2 border-slate-200 dark:border-slate-700">
127
155
<div className="flex items-center space-x-3 mb-6">
128
-
<History className="w-6 h-6 text-purple-600 dark:text-purple-400" />
129
-
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">
130
-
Previous Uploads
156
+
<Sparkles className="w-6 h-6 text-firefly-amber" />
157
+
<h2 className="text-xl font-bold text-slate-900 dark:text-slate-100">
158
+
Your Light Trail
131
159
</h2>
132
160
</div>
133
161
134
162
{isLoading ? (
135
163
<div className="space-y-3">
136
164
{[...Array(3)].map((_, i) => (
137
-
<div key={i} className="animate-pulse flex items-center space-x-4 p-4 bg-gray-50 dark:bg-gray-700 rounded-xl">
138
-
<div className="w-12 h-12 bg-gray-200 dark:bg-gray-600 rounded-xl" />
165
+
<div key={i} className="animate-pulse flex items-center space-x-4 p-4 bg-slate-50 dark:bg-slate-700 rounded-xl">
166
+
<div className="w-12 h-12 bg-slate-200 dark:bg-slate-600 rounded-xl" />
139
167
<div className="flex-1 space-y-2">
140
-
<div className="h-4 bg-gray-200 dark:bg-gray-600 rounded w-3/4" />
141
-
<div className="h-3 bg-gray-200 dark:bg-gray-600 rounded w-1/2" />
168
+
<div className="h-4 bg-slate-200 dark:bg-slate-600 rounded w-3/4" />
169
+
<div className="h-3 bg-slate-200 dark:bg-slate-600 rounded w-1/2" />
142
170
</div>
143
171
</div>
144
172
))}
145
173
</div>
146
174
) : uploads.length === 0 ? (
147
175
<div className="text-center py-12">
148
-
<FileText className="w-16 h-16 text-gray-300 dark:text-gray-600 mx-auto mb-4" />
149
-
<p className="text-gray-500 dark:text-gray-400">No previous uploads yet</p>
150
-
<p className="text-sm text-gray-400 dark:text-gray-500 mt-2">
176
+
<FileText className="w-16 h-16 text-slate-300 dark:text-slate-600 mx-auto mb-4" />
177
+
<p className="text-slate-600 dark:text-slate-400 font-medium">No previous uploads yet</p>
178
+
<p className="text-sm text-slate-500 dark:text-slate-500 mt-2">
151
179
Upload your first file to get started
152
180
</p>
153
181
</div>
···
157
185
<button
158
186
key={upload.uploadId}
159
187
onClick={() => onLoadUpload(upload.uploadId)}
160
-
className="w-full flex items-start space-x-4 p-4 bg-gray-50 dark:bg-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 rounded-xl transition-colors text-left"
188
+
className="w-full flex items-start space-x-4 p-4 bg-slate-50 dark:bg-slate-900/50 hover:bg-slate-100 dark:hover:bg-slate-900/70 rounded-xl transition-all text-left border-2 border-slate-200 dark:border-slate-700 hover:border-firefly-orange dark:hover:border-firefly-orange shadow-md hover:shadow-lg"
161
189
>
162
-
<div className={`w-12 h-12 bg-gradient-to-r ${getPlatformColor(upload.sourcePlatform)} rounded-xl flex items-center justify-center flex-shrink-0`}>
163
-
<Upload className="w-6 h-6 text-white" />
190
+
<div className={`w-12 h-12 bg-gradient-to-r ${getPlatformColor(upload.sourcePlatform)} rounded-xl flex items-center justify-center flex-shrink-0 shadow-md`}>
191
+
<Sparkles className="w-6 h-6 text-white" />
164
192
</div>
165
193
<div className="flex-1 min-w-0">
166
194
<div className="flex flex-wrap items-start justify-between gap-x-4 gap-y-2 mb-1">
167
-
<div className="font-semibold text-gray-900 dark:text-gray-100 capitalize">
195
+
<div className="font-semibold text-slate-900 dark:text-slate-100 capitalize">
168
196
{upload.sourcePlatform}
169
197
</div>
170
198
<div className="flex items-center gap-2 flex-shrink-0">
171
-
<span className="text-xs px-2 py-0.5 bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 rounded-full whitespace-nowrap">
172
-
{upload.matchedUsers} {upload.matchedUsers === 1 ? 'match' : 'matches'}
199
+
<span className="text-xs px-2 py-0.5 bg-firefly-amber/20 dark:bg-firefly-amber/30 text-amber-900 dark:text-firefly-glow rounded-full font-medium border border-firefly-amber/20 dark:border-firefly-amber/50 whitespace-nowrap">
200
+
{upload.matchedUsers} {upload.matchedUsers === 1 ? 'firefly' : 'fireflies'}
173
201
</span>
174
-
<div className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
202
+
<div className="text-sm text-slate-600 dark:text-slate-400 font-medium whitespace-nowrap">
175
203
{Math.round((upload.matchedUsers / upload.totalUsers) * 100)}%
176
204
</div>
177
205
</div>
178
206
</div>
179
-
<div className="text-sm text-gray-600 dark:text-gray-400">
207
+
<div className="text-sm text-slate-700 dark:text-slate-300">
180
208
{upload.totalUsers} users • {formatDate(upload.createdAt)}
181
209
</div>
182
210
</div>
+52
-28
src/pages/Loading.tsx
+52
-28
src/pages/Loading.tsx
···
1
-
import { Search } from "lucide-react";
1
+
import { Search, Sparkles } from "lucide-react";
2
2
import AppHeader from "../components/AppHeader";
3
3
import { PLATFORMS } from "../constants/platforms";
4
4
···
23
23
searchProgress: SearchProgress;
24
24
currentStep: string;
25
25
sourcePlatform: string;
26
+
isDark?: boolean;
27
+
reducedMotion?: boolean;
28
+
onToggleTheme?: () => void;
29
+
onToggleMotion?: () => void;
26
30
}
27
31
28
-
export default function LoadingPage({ session, onLogout, onNavigate, searchProgress, currentStep, sourcePlatform }: LoadingPageProps) {
32
+
export default function LoadingPage({
33
+
session,
34
+
onLogout,
35
+
onNavigate,
36
+
searchProgress,
37
+
currentStep,
38
+
sourcePlatform,
39
+
isDark = false,
40
+
reducedMotion = false,
41
+
onToggleTheme,
42
+
onToggleMotion
43
+
}: LoadingPageProps) {
29
44
const platform = PLATFORMS[sourcePlatform] || PLATFORMS.tiktok;
30
45
const PlatformIcon = platform.icon;
31
46
32
47
return (
33
-
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
34
-
<AppHeader session={session} onLogout={onLogout} onNavigate={onNavigate} currentStep={currentStep} />
48
+
<div className="min-h-screen">
49
+
<AppHeader
50
+
session={session}
51
+
onLogout={onLogout}
52
+
onNavigate={onNavigate}
53
+
currentStep={currentStep}
54
+
isDark={isDark}
55
+
reducedMotion={reducedMotion}
56
+
onToggleTheme={onToggleTheme}
57
+
onToggleMotion={onToggleMotion}
58
+
/>
35
59
36
60
{/* Platform Banner - Searching State */}
37
-
<div className={`bg-gradient-to-r ${platform.color} text-white`}>
61
+
<div className={`bg-firefly-banner dark:bg-firefly-banner-dark text-white`}>
38
62
<div className="max-w-3xl mx-auto px-4 py-6">
39
63
<div className="flex items-center justify-between">
40
64
<div className="flex items-center space-x-4">
···
43
67
<Search className="w-6 h-6 absolute -bottom-1 -right-1 animate-pulse" aria-hidden="true" />
44
68
</div>
45
69
<div>
46
-
<h2 className="text-xl font-bold">Finding Your People</h2>
70
+
<h2 className="text-xl font-bold">Finding Your Fireflies</h2>
47
71
<p className="text-white/90 text-sm">
48
72
Searching the ATmosphere for {platform.name} follows...
49
73
</p>
···
60
84
</div>
61
85
62
86
{/* Progress Stats */}
63
-
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
87
+
<div className="bg-white/95 dark:bg-slate-800/95 border-b-2 border-slate-200 dark:border-slate-700 backdrop-blur-sm">
64
88
<div className="max-w-3xl mx-auto px-4 py-4">
65
89
<div className="grid grid-cols-3 gap-4 text-center mb-4">
66
90
<div>
67
-
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100" aria-label={`${searchProgress.searched} searched`}>
91
+
<div className="text-2xl font-bold text-slate-900 dark:text-slate-100" aria-label={`${searchProgress.searched} searched`}>
68
92
{searchProgress.searched}
69
93
</div>
70
-
<div className="text-sm text-gray-600 dark:text-gray-300">Searched</div>
94
+
<div className="text-sm text-slate-700 dark:text-slate-300 font-medium">Searched</div>
71
95
</div>
72
96
<div>
73
-
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400" aria-label={`${searchProgress.found} found`}>
97
+
<div className="text-2xl font-bold text-firefly-orange" aria-label={`${searchProgress.found} found`}>
74
98
{searchProgress.found}
75
99
</div>
76
-
<div className="text-sm text-gray-600 dark:text-gray-300">Found</div>
100
+
<div className="text-sm text-slate-700 dark:text-slate-300 font-medium">Fireflies Found</div>
77
101
</div>
78
102
<div>
79
-
<div className="text-2xl font-bold text-gray-400 dark:text-gray-500" aria-label={`${searchProgress.total} total`}>
103
+
<div className="text-2xl font-bold text-slate-600 dark:text-slate-400" aria-label={`${searchProgress.total} total`}>
80
104
{searchProgress.total}
81
105
</div>
82
-
<div className="text-sm text-gray-600 dark:text-gray-300">Total</div>
106
+
<div className="text-sm text-slate-700 dark:text-slate-300 font-medium">Total</div>
83
107
</div>
84
108
</div>
85
109
86
110
<div
87
-
className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-3"
111
+
className="w-full bg-slate-200 dark:bg-slate-700 rounded-full h-3"
88
112
role="progressbar"
89
113
aria-valuenow={searchProgress.total > 0 ? Math.round((searchProgress.searched / searchProgress.total) * 100) : 0}
90
114
aria-valuemin={0}
91
115
aria-valuemax={100}
92
116
>
93
117
<div
94
-
className="bg-gradient-to-r from-blue-500 to-purple-600 h-full rounded-full transition-all"
118
+
className="bg-gradient-to-r from-firefly-amber via-firefly-orange to-firefly-pink h-full rounded-full transition-all"
95
119
style={{ width: `${searchProgress.total > 0 ? (searchProgress.searched / searchProgress.total) * 100 : 0}%` }}
96
120
/>
97
121
</div>
···
101
125
{/* Skeleton Results - Matches layout of Results page */}
102
126
<div className="max-w-3xl mx-auto px-4 py-4 space-y-4">
103
127
{[...Array(8)].map((_, i) => (
104
-
<div key={i} className="bg-white dark:bg-gray-800 rounded-2xl shadow-sm overflow-hidden animate-pulse">
128
+
<div key={i} className="bg-white/95 dark:bg-slate-800/95 backdrop-blur-xl rounded-2xl shadow-sm overflow-hidden animate-pulse border-2 border-slate-200 dark:border-slate-700">
105
129
{/* Source User Skeleton */}
106
-
<div className="p-4 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
130
+
<div className="p-4 bg-slate-50 dark:bg-slate-900/50 border-b-2 border-slate-200 dark:border-slate-700">
107
131
<div className="flex items-center space-x-3">
108
-
<div className="w-10 h-10 bg-gray-300 dark:bg-gray-600 rounded-full" />
132
+
<div className="w-10 h-10 bg-slate-300 dark:bg-slate-600 rounded-full" />
109
133
<div className="flex-1 space-y-2">
110
-
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-32" />
111
-
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-24" />
134
+
<div className="h-4 bg-slate-300 dark:bg-slate-600 rounded w-32" />
135
+
<div className="h-3 bg-slate-200 dark:bg-slate-700 rounded w-24" />
112
136
</div>
113
-
<div className="h-5 w-16 bg-gray-300 dark:bg-gray-600 rounded-full" />
137
+
<div className="h-5 w-16 bg-slate-300 dark:bg-slate-600 rounded-full" />
114
138
</div>
115
139
</div>
116
140
117
141
{/* Match Skeleton */}
118
142
<div className="p-4">
119
-
<div className="flex items-start space-x-3 p-3 rounded-xl bg-blue-50 dark:bg-blue-900/20">
120
-
<div className="w-12 h-12 bg-gray-300 dark:bg-gray-600 rounded-full flex-shrink-0" />
143
+
<div className="flex items-start space-x-3 p-3 rounded-xl bg-amber-50 dark:bg-amber-900/10 border-2 border-amber-200 dark:border-amber-800/30">
144
+
<div className="w-12 h-12 bg-slate-300 dark:bg-slate-600 rounded-full flex-shrink-0" />
121
145
<div className="flex-1 space-y-2">
122
-
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-3/4" />
123
-
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-1/2" />
124
-
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-full" />
125
-
<div className="h-5 w-20 bg-green-200 dark:bg-green-900 rounded-full mt-2" />
146
+
<div className="h-4 bg-slate-300 dark:bg-slate-600 rounded w-3/4" />
147
+
<div className="h-3 bg-slate-200 dark:bg-slate-700 rounded w-1/2" />
148
+
<div className="h-3 bg-slate-200 dark:bg-slate-700 rounded w-full" />
149
+
<div className="h-5 w-20 bg-slate-300 dark:bg-slate-600 rounded-full mt-2" />
126
150
</div>
127
-
<div className="w-20 h-8 bg-gray-300 dark:bg-gray-600 rounded-full flex-shrink-0" />
151
+
<div className="w-20 h-8 bg-slate-300 dark:bg-slate-600 rounded-full flex-shrink-0" />
128
152
</div>
129
153
</div>
130
154
</div>
+101
-64
src/pages/Login.tsx
+101
-64
src/pages/Login.tsx
···
5
5
onSubmit: (handle: string) => void;
6
6
session?: { handle: string } | null;
7
7
onNavigate?: (step: 'home') => void;
8
+
reducedMotion?: boolean;
8
9
}
9
10
10
-
export default function LoginPage({ onSubmit, session, onNavigate }: LoginPageProps) {
11
+
export default function LoginPage({ onSubmit, session, onNavigate, reducedMotion = false }: LoginPageProps) {
11
12
const [handle, setHandle] = useState("");
12
13
13
14
const handleSubmit = (e: React.FormEvent) => {
···
16
17
};
17
18
18
19
return (
19
-
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-purple-50 to-pink-50 dark:from-gray-900 dark:via-gray-850 dark:to-gray-800">
20
+
<div className="min-h-screen">
20
21
<div className="max-w-6xl mx-auto px-4 py-8 md:py-12">
21
22
22
23
{/* Hero Section - Side by side on desktop */}
23
24
<div className="grid md:grid-cols-2 gap-8 md:gap-12 items-start mb-12 md:mb-16">
24
25
{/* Left: Welcome */}
25
26
<div className="text-center md:text-left">
26
-
<div className="inline-flex items-center justify-center w-20 h-20 md:w-24 md:h-24 bg-gradient-to-br from-blue-500 to-purple-600 rounded-3xl mb-4 md:mb-6 shadow-xl">
27
-
<Heart className="w-10 h-10 md:w-12 md:h-12 text-white" />
27
+
<div className="inline-flex items-center justify-center mb-6 relative">
28
+
<div
29
+
className={`w-20 h-20 md:w-24 md:h-24 bg-gradient-to-br from-firefly-amber via-firefly-orange to-firefly-pink rounded-3xl flex items-center justify-center relative shadow-xl ${
30
+
reducedMotion ? '' : 'animate-glow-pulse'
31
+
}`}
32
+
>
33
+
<Heart className="w-10 h-10 md:w-12 md:h-12 text-slate-900" aria-hidden="true" />
34
+
{/* Firefly mascot hint */}
35
+
<div
36
+
className={`absolute -top-2 -right-2 w-8 h-8 bg-firefly-glow rounded-full flex items-center justify-center shadow-lg ${
37
+
reducedMotion ? '' : 'animate-bounce'
38
+
}`}
39
+
aria-hidden="true"
40
+
>
41
+
<div className="w-4 h-4 bg-firefly-amber rounded-full" />
42
+
</div>
43
+
</div>
28
44
</div>
29
-
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold text-gray-900 dark:text-gray-100 mb-3 md:mb-4">
30
-
Welcome to ATlast
45
+
46
+
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold text-slate-900 dark:text-white mb-3 md:mb-4">
47
+
ATlast
31
48
</h1>
32
-
<p className="text-lg md:text-xl lg:text-2xl text-gray-700 dark:text-gray-300 mb-6">
33
-
Reunite with your community on the ATmosphere
49
+
<p className="text-lg md:text-xl lg:text-2xl text-slate-800 dark:text-slate-100 mb-2 font-medium">
50
+
Find Your Light in the ATmosphere
51
+
</p>
52
+
<p className="text-slate-700 dark:text-slate-300 mb-6">
53
+
Reconnect with your internet, one firefly at a time ✨
34
54
</p>
35
55
56
+
{/* Decorative firefly trail - only show if motion enabled */}
57
+
{!reducedMotion && (
58
+
<div className="mt-8 flex justify-center md:justify-start space-x-2" aria-hidden="true">
59
+
{[...Array(5)].map((_, i) => (
60
+
<div
61
+
key={i}
62
+
className="w-2 h-2 rounded-full bg-firefly-amber dark:bg-firefly-glow"
63
+
style={{
64
+
opacity: 1 - i * 0.15,
65
+
animation: `float ${2 + i * 0.3}s ease-in-out infinite`,
66
+
animationDelay: `${i * 0.2}s`
67
+
}}
68
+
/>
69
+
))}
70
+
</div>
71
+
)}
72
+
36
73
{/* Privacy Notice - visible on mobile */}
37
74
<div className="md:hidden mt-6">
38
-
<p className="text-sm text-gray-600 dark:text-gray-400">
39
-
Your data is processed and stored by our servers if you enable DM notifications. This is to help you find matches and reconnect with your community.
75
+
<p className="text-sm text-slate-600 dark:text-slate-400">
76
+
Your data is processed and stored by our servers. This helps you find matches and reconnect with your community.
40
77
</p>
41
78
</div>
42
79
</div>
···
44
81
{/* Right: Login Card or Dashboard Button */}
45
82
<div className="w-full">
46
83
{session ? (
47
-
<div className="bg-white dark:bg-gray-800 rounded-3xl shadow-2xl p-8 border border-gray-100 dark:border-gray-700">
84
+
<div className="bg-white/95 dark:bg-slate-800/95 backdrop-blur-xl rounded-3xl shadow-2xl p-8 border-2 border-slate-200 dark:border-slate-700">
48
85
<div className="text-center mb-6">
49
-
<div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full mx-auto mb-4 flex items-center justify-center">
50
-
<Heart className="w-8 h-8 text-white" />
86
+
<div className="w-16 h-16 bg-gradient-to-br from-firefly-amber via-firefly-orange to-firefly-pink rounded-full mx-auto mb-4 flex items-center justify-center shadow-md">
87
+
<Heart className="w-8 h-8 text-slate-900" />
51
88
</div>
52
-
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-2">
89
+
<h2 className="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-2">
53
90
You're logged in!
54
91
</h2>
55
-
<p className="text-gray-600 dark:text-gray-400">
92
+
<p className="text-slate-700 dark:text-slate-300">
56
93
Welcome back, @{session.handle}
57
94
</p>
58
95
</div>
59
96
60
97
<button
61
98
onClick={() => onNavigate?.('home')}
62
-
className="w-full bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white py-4 rounded-xl font-bold text-lg transition-all shadow-lg hover:shadow-xl focus:ring-4 focus:ring-purple-300 dark:focus:ring-purple-800 focus:outline-none flex items-center justify-center space-x-2"
99
+
className="w-full bg-gradient-to-r from-firefly-amber via-firefly-orange to-firefly-pink hover:from-amber-600 hover:via-orange-600 hover:to-pink-600 text-white py-4 rounded-xl font-bold text-lg transition-all shadow-lg hover:shadow-xl focus:ring-4 focus:ring-orange-300 dark:focus:ring-orange-800 focus:outline-none flex items-center justify-center space-x-2"
63
100
>
64
101
<span>Go to Dashboard</span>
65
102
<ArrowRight className="w-5 h-5" />
66
103
</button>
67
104
</div>
68
105
) : (
69
-
<div className="bg-white dark:bg-gray-800 rounded-3xl shadow-2xl p-6 md:p-8 border border-gray-100 dark:border-gray-700">
70
-
<h2 className="text-xl md:text-2xl font-bold text-gray-900 dark:text-gray-100 mb-2 text-center">
71
-
Get Started
106
+
<div className="bg-white/95 dark:bg-slate-800/95 backdrop-blur-xl rounded-3xl shadow-2xl p-6 md:p-8 border-2 border-slate-200 dark:border-slate-700">
107
+
<h2 className="text-xl md:text-2xl font-bold text-slate-900 dark:text-slate-100 mb-2 text-center">
108
+
Light Up Your Network
72
109
</h2>
73
-
<p className="text-gray-600 dark:text-gray-400 text-center mb-6">
110
+
<p className="text-slate-700 dark:text-slate-300 text-center mb-6">
74
111
Connect your ATmosphere account to begin
75
112
</p>
76
113
77
114
<form onSubmit={handleSubmit} className="space-y-4" method="post">
78
115
<div>
79
-
<label htmlFor="atproto-handle" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
116
+
<label htmlFor="atproto-handle" className="block text-sm font-semibold text-slate-900 dark:text-slate-100 mb-2">
80
117
Your ATmosphere Handle
81
118
</label>
82
119
<input
···
85
122
value={handle}
86
123
onChange={(e) => setHandle(e.target.value)}
87
124
placeholder="yourname.bsky.social"
88
-
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-xl bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
125
+
className="w-full px-4 py-3 bg-slate-50 dark:bg-slate-900/50 border-2 border-slate-300 dark:border-slate-600 rounded-xl text-slate-900 dark:text-slate-100 placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-firefly-orange focus:border-transparent transition-all"
89
126
aria-required="true"
90
127
aria-describedby="handle-description"
91
128
/>
92
-
<p id="handle-description" className="text-xs text-gray-500 dark:text-gray-400 mt-2">
129
+
<p id="handle-description" className="text-xs text-slate-600 dark:text-slate-400 mt-2">
93
130
Enter your full ATmosphere handle (e.g., username.bsky.social or yourname.com)
94
131
</p>
95
132
</div>
96
133
97
134
<button
98
135
type="submit"
99
-
className="w-full bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white py-4 rounded-xl font-bold text-lg transition-all shadow-lg hover:shadow-xl focus:ring-4 focus:ring-purple-300 dark:focus:ring-purple-800 focus:outline-none"
136
+
className="w-full bg-gradient-to-r from-firefly-amber via-firefly-orange to-firefly-pink hover:from-amber-600 hover:via-orange-600 hover:to-pink-600 text-white py-4 rounded-xl font-bold text-lg transition-all shadow-lg hover:shadow-xl focus:ring-4 focus:ring-orange-300 dark:focus:ring-orange-800 focus:outline-none"
100
137
aria-label="Connect to the ATmosphere"
101
138
>
102
-
Connect to the ATmosphere
139
+
Join the Swarm ✨
103
140
</button>
104
141
</form>
105
142
106
-
<div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
107
-
<div className="flex items-start space-x-2 text-sm text-gray-600 dark:text-gray-400">
143
+
<div className="mt-6 pt-6 border-t-2 border-slate-200 dark:border-slate-700">
144
+
<div className="flex items-start space-x-2 text-sm text-slate-700 dark:text-slate-300">
108
145
<svg className="w-5 h-5 text-green-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
109
146
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
110
147
</svg>
111
148
<div>
112
-
<p className="font-medium text-gray-700 dark:text-gray-300">Secure OAuth Connection</p>
149
+
<p className="font-semibold text-slate-900 dark:text-slate-100">Secure OAuth Connection</p>
113
150
<p className="text-xs mt-1">We use official AT Protocol OAuth. We never see your password and you can revoke access anytime.</p>
114
151
</div>
115
152
</div>
···
121
158
122
159
{/* Value Props */}
123
160
<div className="grid md:grid-cols-3 gap-4 md:gap-6 mb-12 md:mb-16 max-w-5xl mx-auto">
124
-
<div className="bg-white dark:bg-gray-800 rounded-2xl p-6 shadow-lg border border-gray-100 dark:border-gray-700">
125
-
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-xl flex items-center justify-center mb-4">
126
-
<Upload className="w-6 h-6 text-blue-600 dark:text-blue-400" />
161
+
<div className="bg-white/95 dark:bg-slate-800/95 backdrop-blur-xl rounded-2xl p-6 shadow-lg border-2 border-slate-200 dark:border-slate-700 hover:border-firefly-orange dark:hover:border-firefly-orange transition-all">
162
+
<div className="w-12 h-12 bg-gradient-to-br from-firefly-amber to-firefly-orange rounded-xl flex items-center justify-center mb-4 shadow-md">
163
+
<Upload className="w-6 h-6 text-slate-900" />
127
164
</div>
128
-
<h3 className="text-lg font-bold text-gray-900 dark:text-gray-100 mb-2">
129
-
Upload Your Data
165
+
<h3 className="text-lg font-bold text-slate-900 dark:text-slate-100 mb-2">
166
+
Share Your Light
130
167
</h3>
131
-
<p className="text-gray-600 dark:text-gray-400 text-sm">
132
-
Import your following lists from Twitter, TikTok, Instagram, and more. Your data stays private.
168
+
<p className="text-slate-700 dark:text-slate-300 text-sm leading-relaxed">
169
+
Import your following lists. Your data stays private, your connections shine bright.
133
170
</p>
134
171
</div>
135
172
136
-
<div className="bg-white dark:bg-gray-800 rounded-2xl p-6 shadow-lg border border-gray-100 dark:border-gray-700">
137
-
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900/30 rounded-xl flex items-center justify-center mb-4">
138
-
<Search className="w-6 h-6 text-purple-600 dark:text-purple-400" />
173
+
<div className="bg-white/95 dark:bg-slate-800/95 backdrop-blur-xl rounded-2xl p-6 shadow-lg border-2 border-slate-200 dark:border-slate-700 hover:border-firefly-orange dark:hover:border-firefly-orange transition-all">
174
+
<div className="w-12 h-12 bg-gradient-to-br from-firefly-cyan to-blue-500 rounded-xl flex items-center justify-center mb-4 shadow-md">
175
+
<Search className="w-6 h-6 text-slate-900" />
139
176
</div>
140
-
<h3 className="text-lg font-bold text-gray-900 dark:text-gray-100 mb-2">
141
-
Find Matches
177
+
<h3 className="text-lg font-bold text-slate-900 dark:text-slate-100 mb-2">
178
+
Find Your Swarm
142
179
</h3>
143
-
<p className="text-gray-600 dark:text-gray-400 text-sm">
144
-
We'll search the ATmosphere to find which of your follows have already migrated.
180
+
<p className="text-slate-700 dark:text-slate-300 text-sm leading-relaxed">
181
+
Watch as fireflies light up - discover which friends have already migrated to the ATmosphere.
145
182
</p>
146
183
</div>
147
184
148
-
<div className="bg-white dark:bg-gray-800 rounded-2xl p-6 shadow-lg border border-gray-100 dark:border-gray-700">
149
-
<div className="w-12 h-12 bg-pink-100 dark:bg-pink-900/30 rounded-xl flex items-center justify-center mb-4">
150
-
<Heart className="w-6 h-6 text-pink-600 dark:text-pink-400" />
185
+
<div className="bg-white/95 dark:bg-slate-800/95 backdrop-blur-xl rounded-2xl p-6 shadow-lg border-2 border-slate-200 dark:border-slate-700 hover:border-firefly-orange dark:hover:border-firefly-orange transition-all">
186
+
<div className="w-12 h-12 bg-gradient-to-br from-firefly-pink to-purple-500 rounded-xl flex items-center justify-center mb-4 shadow-md">
187
+
<Heart className="w-6 h-6 text-slate-900" />
151
188
</div>
152
-
<h3 className="text-lg font-bold text-gray-900 dark:text-gray-100 mb-2">
153
-
Reconnect Instantly
189
+
<h3 className="text-lg font-bold text-slate-900 dark:text-slate-100 mb-2">
190
+
Sync Your Glow
154
191
</h3>
155
-
<p className="text-gray-600 dark:text-gray-400 text-sm">
156
-
Follow everyone at once or pick and choose. Build your community on the ATmosphere.
192
+
<p className="text-slate-700 dark:text-slate-300 text-sm leading-relaxed">
193
+
Reconnect instantly. Follow everyone at once or pick and choose - light up together.
157
194
</p>
158
195
</div>
159
196
</div>
160
197
161
198
{/* Privacy Notice - desktop only */}
162
199
<div className="hidden md:block text-center mb-8">
163
-
<p className="text-sm text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
164
-
Your data is processed and stored by our servers if you enable DM notifications. This is to help you find matches and reconnect with your community.
200
+
<p className="text-sm text-slate-600 dark:text-slate-400 max-w-2xl mx-auto">
201
+
Your data is processed and stored by our servers. This helps you find matches and reconnect with your community.
165
202
</p>
166
203
</div>
167
204
168
205
{/* How It Works */}
169
206
<div className="max-w-4xl mx-auto">
170
-
<h2 className="text-2xl font-bold text-center text-gray-900 dark:text-gray-100 mb-8">
207
+
<h2 className="text-2xl font-bold text-center text-slate-900 dark:text-slate-100 mb-8">
171
208
How It Works
172
209
</h2>
173
210
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
174
211
<div className="text-center">
175
-
<div className="w-12 h-12 bg-blue-500 text-white rounded-full flex items-center justify-center mx-auto mb-3 font-bold text-lg" aria-hidden="true">
212
+
<div className="w-12 h-12 bg-firefly-orange text-white rounded-full flex items-center justify-center mx-auto mb-3 font-bold text-lg shadow-md" aria-hidden="true">
176
213
1
177
214
</div>
178
-
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-1">Connect</h3>
179
-
<p className="text-sm text-gray-600 dark:text-gray-400">Sign in with your ATmosphere account</p>
215
+
<h3 className="font-semibold text-slate-900 dark:text-slate-100 mb-1">Connect</h3>
216
+
<p className="text-sm text-slate-700 dark:text-slate-300">Sign in with your ATmosphere account</p>
180
217
</div>
181
218
<div className="text-center">
182
-
<div className="w-12 h-12 bg-purple-500 text-white rounded-full flex items-center justify-center mx-auto mb-3 font-bold text-lg" aria-hidden="true">
219
+
<div className="w-12 h-12 bg-firefly-pink text-white rounded-full flex items-center justify-center mx-auto mb-3 font-bold text-lg shadow-md" aria-hidden="true">
183
220
2
184
221
</div>
185
-
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-1">Upload</h3>
186
-
<p className="text-sm text-gray-600 dark:text-gray-400">Import your following data from other platforms</p>
222
+
<h3 className="font-semibold text-slate-900 dark:text-slate-100 mb-1">Upload</h3>
223
+
<p className="text-sm text-slate-700 dark:text-slate-300">Import your following data from other platforms</p>
187
224
</div>
188
225
<div className="text-center">
189
-
<div className="w-12 h-12 bg-pink-500 text-white rounded-full flex items-center justify-center mx-auto mb-3 font-bold text-lg" aria-hidden="true">
226
+
<div className="w-12 h-12 bg-firefly-cyan text-white rounded-full flex items-center justify-center mx-auto mb-3 font-bold text-lg shadow-md" aria-hidden="true">
190
227
3
191
228
</div>
192
-
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-1">Match</h3>
193
-
<p className="text-sm text-gray-600 dark:text-gray-400">We find your people on the ATmosphere</p>
229
+
<h3 className="font-semibold text-slate-900 dark:text-slate-100 mb-1">Match</h3>
230
+
<p className="text-sm text-slate-700 dark:text-slate-300">We find your fireflies in the ATmosphere</p>
194
231
</div>
195
232
<div className="text-center">
196
-
<div className="w-12 h-12 bg-orange-500 text-white rounded-full flex items-center justify-center mx-auto mb-3 font-bold text-lg" aria-hidden="true">
233
+
<div className="w-12 h-12 bg-firefly-amber text-slate-900 rounded-full flex items-center justify-center mx-auto mb-3 font-bold text-lg shadow-md" aria-hidden="true">
197
234
4
198
235
</div>
199
-
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-1">Follow</h3>
200
-
<p className="text-sm text-gray-600 dark:text-gray-400">Reconnect with your community</p>
236
+
<h3 className="font-semibold text-slate-900 dark:text-slate-100 mb-1">Follow</h3>
237
+
<p className="text-sm text-slate-700 dark:text-slate-300">Reconnect with your community</p>
201
238
</div>
202
239
</div>
203
240
</div>
+53
-18
src/pages/Results.tsx
+53
-18
src/pages/Results.tsx
···
1
-
import { Video, Heart } from "lucide-react";
1
+
import { Sparkles, Heart } from "lucide-react";
2
2
import { PLATFORMS } from "../constants/platforms";
3
3
import AppHeader from "../components/AppHeader";
4
4
import SearchResultCard from "../components/SearchResultCard";
···
41
41
isFollowing: boolean;
42
42
currentStep: string;
43
43
sourcePlatform: string;
44
+
reducedMotion?: boolean;
45
+
isDark?: boolean;
46
+
onToggleTheme?: () => void;
47
+
onToggleMotion?: () => void;
44
48
}
45
49
46
50
export default function ResultsPage({
···
58
62
totalFound,
59
63
isFollowing,
60
64
currentStep,
61
-
sourcePlatform
65
+
sourcePlatform,
66
+
reducedMotion = false,
67
+
isDark = false,
68
+
onToggleTheme,
69
+
onToggleMotion
62
70
}: ResultsPageProps) {
63
71
const platform = PLATFORMS[sourcePlatform] || PLATFORMS.tiktok;
64
72
const PlatformIcon = platform.icon;
65
73
66
74
return (
67
-
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 pb-24">
68
-
<AppHeader session={session} onLogout={onLogout} onNavigate={onNavigate} currentStep={currentStep} />
75
+
<div className="min-h-screen pb-24">
76
+
<AppHeader
77
+
session={session}
78
+
onLogout={onLogout}
79
+
onNavigate={onNavigate}
80
+
currentStep={currentStep}
81
+
isDark={isDark}
82
+
reducedMotion={reducedMotion}
83
+
onToggleTheme={onToggleTheme}
84
+
onToggleMotion={onToggleMotion}
85
+
/>
69
86
70
87
{/* Platform Info Banner */}
71
-
<div className={`bg-gradient-to-r ${platform.color} text-white`}>
72
-
<div className="max-w-3xl mx-auto px-4 py-6">
88
+
<div className="bg-firefly-banner dark:bg-firefly-banner-dark text-white relative overflow-hidden">
89
+
{!reducedMotion && (
90
+
<div className="absolute inset-0 opacity-20" aria-hidden="true">
91
+
{[...Array(10)].map((_, i) => (
92
+
<div
93
+
key={i}
94
+
className="absolute w-1 h-1 bg-white rounded-full"
95
+
style={{
96
+
left: `${Math.random() * 100}%`,
97
+
top: `${Math.random() * 100}%`,
98
+
animation: `float ${2 + Math.random()}s ease-in-out infinite`,
99
+
animationDelay: `${Math.random()}s`
100
+
}}
101
+
/>
102
+
))}
103
+
</div>
104
+
)}
105
+
<div className="max-w-3xl mx-auto px-4 py-6 relative">
73
106
<div className="flex items-center justify-between">
74
107
<div className="flex items-center space-x-4">
75
-
<PlatformIcon className="w-12 h-12" />
108
+
<div className="w-12 h-12 bg-white/20 backdrop-blur rounded-xl flex items-center justify-center shadow-lg">
109
+
<Sparkles className="w-6 h-6 text-white" />
110
+
</div>
76
111
<div>
77
-
<h2 className="text-xl font-bold">{platform.name} Matches</h2>
78
-
<p className="text-white/90 text-sm">
79
-
{totalFound} matches from {searchResults.length} follows
112
+
<h2 className="text-xl font-bold">{totalFound} Connections Found!</h2>
113
+
<p className="text-white/95 text-sm">
114
+
From {searchResults.length} {platform.name} follows
80
115
</p>
81
116
</div>
82
117
</div>
83
118
{totalSelected > 0 && (
84
119
<div className="text-right">
85
120
<div className="text-2xl font-bold">{totalSelected}</div>
86
-
<div className="text-xs text-white/80">selected</div>
121
+
<div className="text-xs font-medium">selected</div>
87
122
</div>
88
123
)}
89
124
</div>
···
91
126
</div>
92
127
93
128
{/* Action Buttons */}
94
-
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-10">
129
+
<div className="bg-white/95 dark:bg-slate-800/95 border-b-2 border-slate-200 dark:border-slate-700 sticky top-0 z-10 backdrop-blur-sm">
95
130
<div className="max-w-3xl mx-auto px-4 py-3 flex space-x-2">
96
131
<button
97
132
onClick={onSelectAll}
98
-
className="flex-1 bg-blue-500 hover:bg-blue-600 text-white py-3 rounded-lg text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
133
+
className="flex-1 bg-orange-600 hover:bg-orange-700 text-white py-3 rounded-xl text-sm font-semibold transition-all shadow-md hover:shadow-lg focus:outline-none focus:ring-4 focus:ring-orange-300 dark:focus:ring-orange-800"
99
134
type="button"
100
135
>
101
136
Select All
102
137
</button>
103
138
<button
104
139
onClick={onDeselectAll}
105
-
className="flex-1 bg-gray-500 hover:bg-gray-600 text-white py-3 rounded-lg text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2"
140
+
className="flex-1 bg-slate-600 dark:bg-slate-700 hover:bg-slate-700 dark:hover:bg-slate-600 text-white py-3 rounded-xl text-sm font-semibold transition-all shadow-md hover:shadow-lg focus:outline-none focus:ring-4 focus:ring-slate-400"
106
141
type="button"
107
142
>
108
143
Clear
···
148
183
149
184
{/* Fixed Bottom Action Bar */}
150
185
{totalSelected > 0 && (
151
-
<div className="fixed bottom-0 left-0 right-0 bg-gradient-to-t from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pt-8 pb-6">
186
+
<div className="fixed bottom-0 left-0 right-0 bg-gradient-to-t from-white via-white to-transparent dark:from-slate-900 dark:via-slate-900 dark:to-transparent pt-8 pb-6">
152
187
<div className="max-w-3xl mx-auto px-4">
153
188
<button
154
189
onClick={onFollowSelected}
155
190
disabled={isFollowing}
156
-
className="w-full bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 hover:from-blue-600 hover:via-purple-600 hover:to-pink-600 text-white py-5 rounded-2xl font-bold text-lg transition-all shadow-2xl hover:shadow-3xl flex items-center justify-center space-x-3 transform hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none focus:outline-none focus:ring-4 focus:ring-purple-300 dark:focus:ring-purple-800"
191
+
className="w-full bg-firefly-banner dark:bg-firefly-banner-dark text-white hover:from-amber-600 hover:via-orange-600 hover:to-pink-600 text-white py-5 rounded-2xl font-bold text-lg transition-all shadow-2xl hover:shadow-3xl flex items-center justify-center space-x-3 hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 focus:outline-none focus:ring-4 focus:ring-orange-300 dark:focus:ring-orange-800"
157
192
>
158
-
<Heart className="w-6 h-6" />
159
-
<span>Follow {totalSelected} Selected {totalSelected === 1 ? 'User' : 'Users'}</span>
193
+
<Sparkles className="w-6 h-6" />
194
+
<span>Light Up {totalSelected} Connection{totalSelected === 1 ? '' : 's'} ✨</span>
160
195
</button>
161
196
</div>
162
197
</div>
+34
-2
tailwind.config.js
+34
-2
tailwind.config.js
···
1
1
/** @type {import('tailwindcss').Config} */
2
2
export default {
3
-
darkMode: 'media', // use system prefs
3
+
darkMode: 'class', // Changed from 'media' to 'class' for manual control
4
4
content: [
5
5
"./index.html",
6
6
"./src/**/*.{js,ts,jsx,tsx}",
7
7
],
8
8
theme: {
9
-
extend: {},
9
+
extend: {
10
+
colors: {
11
+
firefly: {
12
+
glow: '#FCD34D', // close to amber-300
13
+
amber: '#F59E0B', // close to amber-500
14
+
orange: '#F97316', // close to orange-500
15
+
pink: '#EC4899', // close to tailwind pink-500
16
+
cyan: '#10D2F4', // close to tailwind cyan-300
17
+
}
18
+
},
19
+
backgroundImage: {
20
+
'firefly-banner':
21
+
'linear-gradient(90deg, rgba(9,163,190,1) 0%, rgba(91,33,182,1) 33%, rgba(236,72,153,1) 67%, rgba(244,105,6,1) 100%)',
22
+
'firefly-banner-dark':
23
+
'linear-gradient(90deg, rgba(24,21,60,1) 0%, rgba(55,20,94,1) 33%, rgba(104,25,98,1) 67%, rgba(36,16,54,1) 100%)',
24
+
},
25
+
animation: {
26
+
'float': 'float 3s ease-in-out infinite',
27
+
'glow-pulse': 'glow-pulse 3s ease-in-out infinite',
28
+
},
29
+
keyframes: {
30
+
float: {
31
+
'0%, 100%': { transform: 'translate(0, 0) scale(1)', opacity: '0.3' },
32
+
'25%': { transform: 'translate(10px, -20px) scale(1.2)', opacity: '0.8' },
33
+
'50%': { transform: 'translate(-5px, -40px) scale(1)', opacity: '0.5' },
34
+
'75%': { transform: 'translate(15px, -25px) scale(1.1)', opacity: '0.9' },
35
+
},
36
+
'glow-pulse': {
37
+
'0%, 100%': { boxShadow: '0 0 20px rgba(251, 191, 36, 0.3)' },
38
+
'50%': { boxShadow: '0 0 40px rgba(251, 191, 36, 0.6), 0 0 60px rgba(251, 191, 36, 0.3)' },
39
+
},
40
+
},
41
+
},
10
42
},
11
43
plugins: [],
12
44
}