ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto

ui color branding :')

+33 -3
src/App.tsx
··· 9 import { useSearch } from "./hooks/useSearch"; 10 import { useFollow } from "./hooks/useFollows"; 11 import { useFileUpload } from "./hooks/useFileUpload"; 12 13 export default function App() { 14 // Auth hook ··· 21 login, 22 logout, 23 } = useAuth(); 24 25 // Add state to track current platform 26 const [currentPlatform, setCurrentPlatform] = useState<string>('tiktok'); ··· 161 }; 162 163 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"> 165 <div 166 role="status" 167 aria-live="polite" ··· 171 {statusMessage} 172 </div> 173 174 <a 175 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" 177 > 178 Skip to main content 179 </a> ··· 183 {currentStep === 'checking' && ( 184 <div className="p-6 max-w-md mx-auto mt-8"> 185 <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"> 187 <ArrowRight className="w-8 h-8 text-white animate-pulse" /> 188 </div> 189 <h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">Loading...</h2> ··· 198 onSubmit={handleLogin} 199 session={session} 200 onNavigate={setCurrentStep} 201 /> 202 )} 203 ··· 210 onFileUpload={processFileUpload} 211 onLoadUpload={handleLoadUpload} 212 currentStep={currentStep} 213 /> 214 )} 215 ··· 222 searchProgress={searchProgress} 223 currentStep={currentStep} 224 sourcePlatform={currentPlatform} 225 /> 226 )} 227 ··· 243 isFollowing={isFollowing} 244 currentStep={currentStep} 245 sourcePlatform={currentPlatform} 246 /> 247 )} 248 </main>
··· 9 import { useSearch } from "./hooks/useSearch"; 10 import { useFollow } from "./hooks/useFollows"; 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 + 16 17 export default function App() { 18 // Auth hook ··· 25 login, 26 logout, 27 } = useAuth(); 28 + 29 + // Theme hook 30 + const { isDark, reducedMotion, toggleTheme, toggleMotion } = useTheme(); 31 32 // Add state to track current platform 33 const [currentPlatform, setCurrentPlatform] = useState<string>('tiktok'); ··· 168 }; 169 170 return ( 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 */} 182 <div 183 role="status" 184 aria-live="polite" ··· 188 {statusMessage} 189 </div> 190 191 + {/* Skip to main content link */} 192 <a 193 href="#main-content" 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" 195 > 196 Skip to main content 197 </a> ··· 201 {currentStep === 'checking' && ( 202 <div className="p-6 max-w-md mx-auto mt-8"> 203 <div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-8 text-center space-y-4"> 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"> 205 <ArrowRight className="w-8 h-8 text-white animate-pulse" /> 206 </div> 207 <h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">Loading...</h2> ··· 216 onSubmit={handleLogin} 217 session={session} 218 onNavigate={setCurrentStep} 219 + reducedMotion={reducedMotion} 220 /> 221 )} 222 ··· 229 onFileUpload={processFileUpload} 230 onLoadUpload={handleLoadUpload} 231 currentStep={currentStep} 232 + reducedMotion={reducedMotion} 233 + isDark={isDark} 234 + onToggleTheme={toggleTheme} 235 + onToggleMotion={toggleMotion} 236 /> 237 )} 238 ··· 245 searchProgress={searchProgress} 246 currentStep={currentStep} 247 sourcePlatform={currentPlatform} 248 + isDark={isDark} 249 + onToggleTheme={toggleTheme} 250 + onToggleMotion={toggleMotion} 251 /> 252 )} 253 ··· 269 isFollowing={isFollowing} 270 currentStep={currentStep} 271 sourcePlatform={currentPlatform} 272 + reducedMotion={reducedMotion} 273 + isDark={isDark} 274 + onToggleTheme={toggleTheme} 275 + onToggleMotion={toggleMotion} 276 /> 277 )} 278 </main>
+80 -41
src/components/AppHeader.tsx
··· 1 import { useState, useEffect, useRef } from "react"; 2 import { Heart, Home, LogOut, ChevronDown } from "lucide-react"; 3 4 interface atprotoSession { 5 did: string; ··· 14 onLogout: () => void; 15 onNavigate: (step: 'home' | 'login') => void; 16 currentStep: string; 17 } 18 19 - export default function AppHeader({ session, onLogout, onNavigate, currentStep }: AppHeaderProps) { 20 const [showMenu, setShowMenu] = useState(false); 21 const menuRef = useRef<HTMLDivElement>(null); 22 ··· 31 }, []); 32 33 return ( 34 - <div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700"> 35 <div className="max-w-6xl mx-auto px-4 py-3"> 36 <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" /> 40 </div> 41 - <h1 className="text-xl font-bold text-gray-900 dark:text-gray-100">ATlast</h1> 42 </button> 43 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> 52 </div> 53 )} 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 - )} 81 </div> 82 </div> 83 </div>
··· 1 import { useState, useEffect, useRef } from "react"; 2 import { Heart, Home, LogOut, ChevronDown } from "lucide-react"; 3 + import ThemeControls from "./ThemeControls"; 4 5 interface atprotoSession { 6 did: string; ··· 15 onLogout: () => void; 16 onNavigate: (step: 'home' | 'login') => void; 17 currentStep: string; 18 + isDark?: boolean; 19 + reducedMotion?: boolean; 20 + onToggleTheme?: () => void; 21 + onToggleMotion?: () => void; 22 } 23 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) { 34 const [showMenu, setShowMenu] = useState(false); 35 const menuRef = useRef<HTMLDivElement>(null); 36 ··· 45 }, []); 46 47 return ( 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]"> 49 <div className="max-w-6xl mx-auto px-4 py-3"> 50 <div className="flex items-center justify-between"> 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" /> 57 </div> 58 + <h1 className="text-xl font-bold text-slate-900 dark:text-slate-100">ATlast</h1> 59 </button> 60 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> 115 </div> 116 )} 117 + </div> 118 + )} 119 + </div> 120 </div> 121 </div> 122 </div>
+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
··· 27 return ( 28 <div className="bg-white dark:bg-gray-800 rounded-2xl shadow-sm overflow-hidden"> 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"> 31 <div className="flex items-start justify-between gap-2"> 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"> 35 @{result.sourceUser.username} 36 </span> 37 - <span className="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"> 38 from {platform.name} 39 </span> 40 </div> 41 </div> 42 - <div className={`text-xs px-2 py-1 rounded-full ${platform.accentBg} text-white whitespace-nowrap flex-shrink-0`}> 43 {result.atprotoMatches.length} {result.atprotoMatches.length === 1 ? 'match' : 'matches'} 44 </div> 45 </div> ··· 66 {match.avatar ? ( 67 <img 68 src={match.avatar} 69 - alt="User avatar, description not provided" 70 className="w-12 h-12 rounded-full object-cover flex-shrink-0" 71 /> 72 ) : ( ··· 84 {match.displayName} 85 </div> 86 )} 87 - <div className="text-sm text-gray-600 dark:text-gray-400"> 88 @{match.handle} 89 </div> 90 {match.description && ( 91 - <div className="text-sm text-gray-500 dark:text-gray-400 mt-1 line-clamp-2">{match.description}</div> 92 )} 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"> 95 {match.postCount && match.postCount > 0 && ( 96 <span>{match.postCount.toLocaleString()} posts</span> 97 )} ··· 115 isFollowed 116 ? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 cursor-not-allowed opacity-60' 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' 120 }`} 121 title={isFollowed ? 'Already followed' : isSelected ? 'Selected to follow' : 'Select to follow'} 122 > ··· 134 {hasMoreMatches && ( 135 <button 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" 138 > 139 - <span>{isExpanded ? 'Show less' : `Show ${result.atprotoMatches.length - 1} more ${result.atprotoMatches.length - 1 === 1 ? 'match' : 'matches'}`}</span> 140 <ChevronDown className={`w-4 h-4 transition-transform ${isExpanded ? 'rotate-180' : ''}`} /> 141 </button> 142 )}
··· 27 return ( 28 <div className="bg-white dark:bg-gray-800 rounded-2xl shadow-sm overflow-hidden"> 29 {/* Source User */} 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 <div className="flex items-start justify-between gap-2"> 32 <div className="flex-1 min-w-0"> 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 @{result.sourceUser.username} 36 </span> 37 + <span className="text-sm text-slate-700 dark:text-slate-300 whitespace-nowrap"> 38 from {platform.name} 39 </span> 40 </div> 41 </div> 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 {result.atprotoMatches.length} {result.atprotoMatches.length === 1 ? 'match' : 'matches'} 44 </div> 45 </div> ··· 66 {match.avatar ? ( 67 <img 68 src={match.avatar} 69 + alt="User avatar" 70 className="w-12 h-12 rounded-full object-cover flex-shrink-0" 71 /> 72 ) : ( ··· 84 {match.displayName} 85 </div> 86 )} 87 + <div className="text-sm text-gray-800 dark:text-gray-200"> 88 @{match.handle} 89 </div> 90 {match.description && ( 91 + <div className="text-sm text-gray-700 dark:text-gray-300 mt-1 line-clamp-2">{match.description}</div> 92 )} 93 {(match.postCount || match.followerCount) && ( 94 + <div className="flex items-center space-x-3 mt-2 text-xs text-gray-700 dark:text-gray-300"> 95 {match.postCount && match.postCount > 0 && ( 96 <span>{match.postCount.toLocaleString()} posts</span> 97 )} ··· 115 isFollowed 116 ? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 cursor-not-allowed opacity-60' 117 : isSelected 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 }`} 121 title={isFollowed ? 'Already followed' : isSelected ? 'Selected to follow' : 'Select to follow'} 122 > ··· 134 {hasMoreMatches && ( 135 <button 136 onClick={onToggleExpand} 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 > 139 + <span>{isExpanded ? 'Show less' : `Show ${result.atprotoMatches.length - 1} more ${result.atprotoMatches.length - 1 === 1 ? 'option' : 'options'}`}</span> 140 <ChevronDown className={`w-4 h-4 transition-transform ${isExpanded ? 'rotate-180' : ''}`} /> 141 </button> 142 )}
+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
···
··· 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
··· 5 @layer base { 6 body { 7 font-family: system-ui, sans-serif; 8 - @apply bg-gray-50 text-gray-900 dark:bg-gray-900 dark:text-gray-100; 9 } 10 11 button { 12 cursor: pointer; 13 } 14 }
··· 5 @layer base { 6 body { 7 font-family: system-ui, sans-serif; 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; 12 } 13 14 button { 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); 45 } 46 }
+61 -33
src/pages/Home.tsx
··· 1 - import { Upload, History, FileText } from "lucide-react"; 2 import { useState, useEffect, useRef } from "react"; 3 import AppHeader from "../components/AppHeader"; 4 import PlatformSelector from "../components/PlatformSelector"; ··· 20 onFileUpload: (e: React.ChangeEvent<HTMLInputElement>, platform: string) => void; 21 onLoadUpload: (uploadId: string) => void; 22 currentStep: string; 23 } 24 25 export default function HomePage({ ··· 28 onNavigate, 29 onFileUpload, 30 onLoadUpload, 31 - currentStep 32 }: HomePageProps) { 33 const [uploads, setUploads] = useState<UploadType[]>([]); 34 const [isLoading, setIsLoading] = useState(true); ··· 80 }; 81 82 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} /> 85 86 <div className="max-w-4xl mx-auto px-4 py-8 space-y-6"> 87 {/* Upload Section */} 88 - <div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6"> 89 <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> 94 </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 97 </p> 98 99 <PlatformSelector onPlatformSelect={handlePlatformSelect} /> ··· 109 aria-label="Upload following data file" 110 /> 111 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> 115 </p> 116 <p className="text-sm text-blue-900 dark:text-blue-300 mt-2"> 117 <strong>TikTok:</strong> Profile → Settings → Account → Download your data → Upload Following.txt ··· 123 </div> 124 125 {/* Upload History Section */} 126 - <div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6"> 127 <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 131 </h2> 132 </div> 133 134 {isLoading ? ( 135 <div className="space-y-3"> 136 {[...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" /> 139 <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" /> 142 </div> 143 </div> 144 ))} 145 </div> 146 ) : uploads.length === 0 ? ( 147 <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"> 151 Upload your first file to get started 152 </p> 153 </div> ··· 157 <button 158 key={upload.uploadId} 159 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" 161 > 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" /> 164 </div> 165 <div className="flex-1 min-w-0"> 166 <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"> 168 {upload.sourcePlatform} 169 </div> 170 <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'} 173 </span> 174 - <div className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap"> 175 {Math.round((upload.matchedUsers / upload.totalUsers) * 100)}% 176 </div> 177 </div> 178 </div> 179 - <div className="text-sm text-gray-600 dark:text-gray-400"> 180 {upload.totalUsers} users • {formatDate(upload.createdAt)} 181 </div> 182 </div>
··· 1 + import { Upload, History, FileText, Sparkles } from "lucide-react"; 2 import { useState, useEffect, useRef } from "react"; 3 import AppHeader from "../components/AppHeader"; 4 import PlatformSelector from "../components/PlatformSelector"; ··· 20 onFileUpload: (e: React.ChangeEvent<HTMLInputElement>, platform: string) => void; 21 onLoadUpload: (uploadId: string) => void; 22 currentStep: string; 23 + reducedMotion?: boolean; 24 + isDark?: boolean; 25 + onToggleTheme?: () => void; 26 + onToggleMotion?: () => void; 27 } 28 29 export default function HomePage({ ··· 32 onNavigate, 33 onFileUpload, 34 onLoadUpload, 35 + currentStep, 36 + reducedMotion = false, 37 + isDark = false, 38 + onToggleTheme, 39 + onToggleMotion 40 }: HomePageProps) { 41 const [uploads, setUploads] = useState<UploadType[]>([]); 42 const [isLoading, setIsLoading] = useState(true); ··· 88 }; 89 90 return ( 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 + /> 102 103 <div className="max-w-4xl mx-auto px-4 py-8 space-y-6"> 104 {/* Upload Section */} 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"> 106 <div className="flex items-center space-x-3 mb-4"> 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> 122 </div> 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 125 </p> 126 127 <PlatformSelector onPlatformSelect={handlePlatformSelect} /> ··· 137 aria-label="Upload following data file" 138 /> 139 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: 143 </p> 144 <p className="text-sm text-blue-900 dark:text-blue-300 mt-2"> 145 <strong>TikTok:</strong> Profile → Settings → Account → Download your data → Upload Following.txt ··· 151 </div> 152 153 {/* Upload History Section */} 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"> 155 <div className="flex items-center space-x-3 mb-6"> 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 159 </h2> 160 </div> 161 162 {isLoading ? ( 163 <div className="space-y-3"> 164 {[...Array(3)].map((_, i) => ( 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" /> 167 <div className="flex-1 space-y-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" /> 170 </div> 171 </div> 172 ))} 173 </div> 174 ) : uploads.length === 0 ? ( 175 <div className="text-center py-12"> 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"> 179 Upload your first file to get started 180 </p> 181 </div> ··· 185 <button 186 key={upload.uploadId} 187 onClick={() => onLoadUpload(upload.uploadId)} 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" 189 > 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" /> 192 </div> 193 <div className="flex-1 min-w-0"> 194 <div className="flex flex-wrap items-start justify-between gap-x-4 gap-y-2 mb-1"> 195 + <div className="font-semibold text-slate-900 dark:text-slate-100 capitalize"> 196 {upload.sourcePlatform} 197 </div> 198 <div className="flex items-center gap-2 flex-shrink-0"> 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'} 201 </span> 202 + <div className="text-sm text-slate-600 dark:text-slate-400 font-medium whitespace-nowrap"> 203 {Math.round((upload.matchedUsers / upload.totalUsers) * 100)}% 204 </div> 205 </div> 206 </div> 207 + <div className="text-sm text-slate-700 dark:text-slate-300"> 208 {upload.totalUsers} users • {formatDate(upload.createdAt)} 209 </div> 210 </div>
+52 -28
src/pages/Loading.tsx
··· 1 - import { Search } from "lucide-react"; 2 import AppHeader from "../components/AppHeader"; 3 import { PLATFORMS } from "../constants/platforms"; 4 ··· 23 searchProgress: SearchProgress; 24 currentStep: string; 25 sourcePlatform: string; 26 } 27 28 - export default function LoadingPage({ session, onLogout, onNavigate, searchProgress, currentStep, sourcePlatform }: LoadingPageProps) { 29 const platform = PLATFORMS[sourcePlatform] || PLATFORMS.tiktok; 30 const PlatformIcon = platform.icon; 31 32 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} /> 35 36 {/* Platform Banner - Searching State */} 37 - <div className={`bg-gradient-to-r ${platform.color} text-white`}> 38 <div className="max-w-3xl mx-auto px-4 py-6"> 39 <div className="flex items-center justify-between"> 40 <div className="flex items-center space-x-4"> ··· 43 <Search className="w-6 h-6 absolute -bottom-1 -right-1 animate-pulse" aria-hidden="true" /> 44 </div> 45 <div> 46 - <h2 className="text-xl font-bold">Finding Your People</h2> 47 <p className="text-white/90 text-sm"> 48 Searching the ATmosphere for {platform.name} follows... 49 </p> ··· 60 </div> 61 62 {/* Progress Stats */} 63 - <div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700"> 64 <div className="max-w-3xl mx-auto px-4 py-4"> 65 <div className="grid grid-cols-3 gap-4 text-center mb-4"> 66 <div> 67 - <div className="text-2xl font-bold text-gray-900 dark:text-gray-100" aria-label={`${searchProgress.searched} searched`}> 68 {searchProgress.searched} 69 </div> 70 - <div className="text-sm text-gray-600 dark:text-gray-300">Searched</div> 71 </div> 72 <div> 73 - <div className="text-2xl font-bold text-blue-600 dark:text-blue-400" aria-label={`${searchProgress.found} found`}> 74 {searchProgress.found} 75 </div> 76 - <div className="text-sm text-gray-600 dark:text-gray-300">Found</div> 77 </div> 78 <div> 79 - <div className="text-2xl font-bold text-gray-400 dark:text-gray-500" aria-label={`${searchProgress.total} total`}> 80 {searchProgress.total} 81 </div> 82 - <div className="text-sm text-gray-600 dark:text-gray-300">Total</div> 83 </div> 84 </div> 85 86 <div 87 - className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-3" 88 role="progressbar" 89 aria-valuenow={searchProgress.total > 0 ? Math.round((searchProgress.searched / searchProgress.total) * 100) : 0} 90 aria-valuemin={0} 91 aria-valuemax={100} 92 > 93 <div 94 - className="bg-gradient-to-r from-blue-500 to-purple-600 h-full rounded-full transition-all" 95 style={{ width: `${searchProgress.total > 0 ? (searchProgress.searched / searchProgress.total) * 100 : 0}%` }} 96 /> 97 </div> ··· 101 {/* Skeleton Results - Matches layout of Results page */} 102 <div className="max-w-3xl mx-auto px-4 py-4 space-y-4"> 103 {[...Array(8)].map((_, i) => ( 104 - <div key={i} className="bg-white dark:bg-gray-800 rounded-2xl shadow-sm overflow-hidden animate-pulse"> 105 {/* Source User Skeleton */} 106 - <div className="p-4 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700"> 107 <div className="flex items-center space-x-3"> 108 - <div className="w-10 h-10 bg-gray-300 dark:bg-gray-600 rounded-full" /> 109 <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" /> 112 </div> 113 - <div className="h-5 w-16 bg-gray-300 dark:bg-gray-600 rounded-full" /> 114 </div> 115 </div> 116 117 {/* Match Skeleton */} 118 <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" /> 121 <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" /> 126 </div> 127 - <div className="w-20 h-8 bg-gray-300 dark:bg-gray-600 rounded-full flex-shrink-0" /> 128 </div> 129 </div> 130 </div>
··· 1 + import { Search, Sparkles } from "lucide-react"; 2 import AppHeader from "../components/AppHeader"; 3 import { PLATFORMS } from "../constants/platforms"; 4 ··· 23 searchProgress: SearchProgress; 24 currentStep: string; 25 sourcePlatform: string; 26 + isDark?: boolean; 27 + reducedMotion?: boolean; 28 + onToggleTheme?: () => void; 29 + onToggleMotion?: () => void; 30 } 31 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) { 44 const platform = PLATFORMS[sourcePlatform] || PLATFORMS.tiktok; 45 const PlatformIcon = platform.icon; 46 47 return ( 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 + /> 59 60 {/* Platform Banner - Searching State */} 61 + <div className={`bg-firefly-banner dark:bg-firefly-banner-dark text-white`}> 62 <div className="max-w-3xl mx-auto px-4 py-6"> 63 <div className="flex items-center justify-between"> 64 <div className="flex items-center space-x-4"> ··· 67 <Search className="w-6 h-6 absolute -bottom-1 -right-1 animate-pulse" aria-hidden="true" /> 68 </div> 69 <div> 70 + <h2 className="text-xl font-bold">Finding Your Fireflies</h2> 71 <p className="text-white/90 text-sm"> 72 Searching the ATmosphere for {platform.name} follows... 73 </p> ··· 84 </div> 85 86 {/* Progress Stats */} 87 + <div className="bg-white/95 dark:bg-slate-800/95 border-b-2 border-slate-200 dark:border-slate-700 backdrop-blur-sm"> 88 <div className="max-w-3xl mx-auto px-4 py-4"> 89 <div className="grid grid-cols-3 gap-4 text-center mb-4"> 90 <div> 91 + <div className="text-2xl font-bold text-slate-900 dark:text-slate-100" aria-label={`${searchProgress.searched} searched`}> 92 {searchProgress.searched} 93 </div> 94 + <div className="text-sm text-slate-700 dark:text-slate-300 font-medium">Searched</div> 95 </div> 96 <div> 97 + <div className="text-2xl font-bold text-firefly-orange" aria-label={`${searchProgress.found} found`}> 98 {searchProgress.found} 99 </div> 100 + <div className="text-sm text-slate-700 dark:text-slate-300 font-medium">Fireflies Found</div> 101 </div> 102 <div> 103 + <div className="text-2xl font-bold text-slate-600 dark:text-slate-400" aria-label={`${searchProgress.total} total`}> 104 {searchProgress.total} 105 </div> 106 + <div className="text-sm text-slate-700 dark:text-slate-300 font-medium">Total</div> 107 </div> 108 </div> 109 110 <div 111 + className="w-full bg-slate-200 dark:bg-slate-700 rounded-full h-3" 112 role="progressbar" 113 aria-valuenow={searchProgress.total > 0 ? Math.round((searchProgress.searched / searchProgress.total) * 100) : 0} 114 aria-valuemin={0} 115 aria-valuemax={100} 116 > 117 <div 118 + className="bg-gradient-to-r from-firefly-amber via-firefly-orange to-firefly-pink h-full rounded-full transition-all" 119 style={{ width: `${searchProgress.total > 0 ? (searchProgress.searched / searchProgress.total) * 100 : 0}%` }} 120 /> 121 </div> ··· 125 {/* Skeleton Results - Matches layout of Results page */} 126 <div className="max-w-3xl mx-auto px-4 py-4 space-y-4"> 127 {[...Array(8)].map((_, i) => ( 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"> 129 {/* Source User Skeleton */} 130 + <div className="p-4 bg-slate-50 dark:bg-slate-900/50 border-b-2 border-slate-200 dark:border-slate-700"> 131 <div className="flex items-center space-x-3"> 132 + <div className="w-10 h-10 bg-slate-300 dark:bg-slate-600 rounded-full" /> 133 <div className="flex-1 space-y-2"> 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" /> 136 </div> 137 + <div className="h-5 w-16 bg-slate-300 dark:bg-slate-600 rounded-full" /> 138 </div> 139 </div> 140 141 {/* Match Skeleton */} 142 <div className="p-4"> 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" /> 145 <div className="flex-1 space-y-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" /> 150 </div> 151 + <div className="w-20 h-8 bg-slate-300 dark:bg-slate-600 rounded-full flex-shrink-0" /> 152 </div> 153 </div> 154 </div>
+101 -64
src/pages/Login.tsx
··· 5 onSubmit: (handle: string) => void; 6 session?: { handle: string } | null; 7 onNavigate?: (step: 'home') => void; 8 } 9 10 - export default function LoginPage({ onSubmit, session, onNavigate }: LoginPageProps) { 11 const [handle, setHandle] = useState(""); 12 13 const handleSubmit = (e: React.FormEvent) => { ··· 16 }; 17 18 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="max-w-6xl mx-auto px-4 py-8 md:py-12"> 21 22 {/* Hero Section - Side by side on desktop */} 23 <div className="grid md:grid-cols-2 gap-8 md:gap-12 items-start mb-12 md:mb-16"> 24 {/* Left: Welcome */} 25 <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" /> 28 </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 31 </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 34 </p> 35 36 {/* Privacy Notice - visible on mobile */} 37 <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. 40 </p> 41 </div> 42 </div> ··· 44 {/* Right: Login Card or Dashboard Button */} 45 <div className="w-full"> 46 {session ? ( 47 - <div className="bg-white dark:bg-gray-800 rounded-3xl shadow-2xl p-8 border border-gray-100 dark:border-gray-700"> 48 <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" /> 51 </div> 52 - <h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-2"> 53 You're logged in! 54 </h2> 55 - <p className="text-gray-600 dark:text-gray-400"> 56 Welcome back, @{session.handle} 57 </p> 58 </div> 59 60 <button 61 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" 63 > 64 <span>Go to Dashboard</span> 65 <ArrowRight className="w-5 h-5" /> 66 </button> 67 </div> 68 ) : ( 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 72 </h2> 73 - <p className="text-gray-600 dark:text-gray-400 text-center mb-6"> 74 Connect your ATmosphere account to begin 75 </p> 76 77 <form onSubmit={handleSubmit} className="space-y-4" method="post"> 78 <div> 79 - <label htmlFor="atproto-handle" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> 80 Your ATmosphere Handle 81 </label> 82 <input ··· 85 value={handle} 86 onChange={(e) => setHandle(e.target.value)} 87 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" 89 aria-required="true" 90 aria-describedby="handle-description" 91 /> 92 - <p id="handle-description" className="text-xs text-gray-500 dark:text-gray-400 mt-2"> 93 Enter your full ATmosphere handle (e.g., username.bsky.social or yourname.com) 94 </p> 95 </div> 96 97 <button 98 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" 100 aria-label="Connect to the ATmosphere" 101 > 102 - Connect to the ATmosphere 103 </button> 104 </form> 105 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"> 108 <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 <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 </svg> 111 <div> 112 - <p className="font-medium text-gray-700 dark:text-gray-300">Secure OAuth Connection</p> 113 <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 </div> 115 </div> ··· 121 122 {/* Value Props */} 123 <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" /> 127 </div> 128 - <h3 className="text-lg font-bold text-gray-900 dark:text-gray-100 mb-2"> 129 - Upload Your Data 130 </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. 133 </p> 134 </div> 135 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" /> 139 </div> 140 - <h3 className="text-lg font-bold text-gray-900 dark:text-gray-100 mb-2"> 141 - Find Matches 142 </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. 145 </p> 146 </div> 147 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" /> 151 </div> 152 - <h3 className="text-lg font-bold text-gray-900 dark:text-gray-100 mb-2"> 153 - Reconnect Instantly 154 </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. 157 </p> 158 </div> 159 </div> 160 161 {/* Privacy Notice - desktop only */} 162 <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. 165 </p> 166 </div> 167 168 {/* How It Works */} 169 <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"> 171 How It Works 172 </h2> 173 <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> 174 <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"> 176 1 177 </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> 180 </div> 181 <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"> 183 2 184 </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> 187 </div> 188 <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"> 190 3 191 </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> 194 </div> 195 <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"> 197 4 198 </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> 201 </div> 202 </div> 203 </div>
··· 5 onSubmit: (handle: string) => void; 6 session?: { handle: string } | null; 7 onNavigate?: (step: 'home') => void; 8 + reducedMotion?: boolean; 9 } 10 11 + export default function LoginPage({ onSubmit, session, onNavigate, reducedMotion = false }: LoginPageProps) { 12 const [handle, setHandle] = useState(""); 13 14 const handleSubmit = (e: React.FormEvent) => { ··· 17 }; 18 19 return ( 20 + <div className="min-h-screen"> 21 <div className="max-w-6xl mx-auto px-4 py-8 md:py-12"> 22 23 {/* Hero Section - Side by side on desktop */} 24 <div className="grid md:grid-cols-2 gap-8 md:gap-12 items-start mb-12 md:mb-16"> 25 {/* Left: Welcome */} 26 <div className="text-center md:text-left"> 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> 44 </div> 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 48 </h1> 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 ✨ 54 </p> 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 + 73 {/* Privacy Notice - visible on mobile */} 74 <div className="md:hidden mt-6"> 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. 77 </p> 78 </div> 79 </div> ··· 81 {/* Right: Login Card or Dashboard Button */} 82 <div className="w-full"> 83 {session ? ( 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"> 85 <div className="text-center mb-6"> 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" /> 88 </div> 89 + <h2 className="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-2"> 90 You're logged in! 91 </h2> 92 + <p className="text-slate-700 dark:text-slate-300"> 93 Welcome back, @{session.handle} 94 </p> 95 </div> 96 97 <button 98 onClick={() => onNavigate?.('home')} 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" 100 > 101 <span>Go to Dashboard</span> 102 <ArrowRight className="w-5 h-5" /> 103 </button> 104 </div> 105 ) : ( 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 109 </h2> 110 + <p className="text-slate-700 dark:text-slate-300 text-center mb-6"> 111 Connect your ATmosphere account to begin 112 </p> 113 114 <form onSubmit={handleSubmit} className="space-y-4" method="post"> 115 <div> 116 + <label htmlFor="atproto-handle" className="block text-sm font-semibold text-slate-900 dark:text-slate-100 mb-2"> 117 Your ATmosphere Handle 118 </label> 119 <input ··· 122 value={handle} 123 onChange={(e) => setHandle(e.target.value)} 124 placeholder="yourname.bsky.social" 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" 126 aria-required="true" 127 aria-describedby="handle-description" 128 /> 129 + <p id="handle-description" className="text-xs text-slate-600 dark:text-slate-400 mt-2"> 130 Enter your full ATmosphere handle (e.g., username.bsky.social or yourname.com) 131 </p> 132 </div> 133 134 <button 135 type="submit" 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" 137 aria-label="Connect to the ATmosphere" 138 > 139 + Join the Swarm ✨ 140 </button> 141 </form> 142 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"> 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"> 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" /> 147 </svg> 148 <div> 149 + <p className="font-semibold text-slate-900 dark:text-slate-100">Secure OAuth Connection</p> 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> 151 </div> 152 </div> ··· 158 159 {/* Value Props */} 160 <div className="grid md:grid-cols-3 gap-4 md:gap-6 mb-12 md:mb-16 max-w-5xl mx-auto"> 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" /> 164 </div> 165 + <h3 className="text-lg font-bold text-slate-900 dark:text-slate-100 mb-2"> 166 + Share Your Light 167 </h3> 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. 170 </p> 171 </div> 172 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" /> 176 </div> 177 + <h3 className="text-lg font-bold text-slate-900 dark:text-slate-100 mb-2"> 178 + Find Your Swarm 179 </h3> 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. 182 </p> 183 </div> 184 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" /> 188 </div> 189 + <h3 className="text-lg font-bold text-slate-900 dark:text-slate-100 mb-2"> 190 + Sync Your Glow 191 </h3> 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. 194 </p> 195 </div> 196 </div> 197 198 {/* Privacy Notice - desktop only */} 199 <div className="hidden md:block text-center mb-8"> 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. 202 </p> 203 </div> 204 205 {/* How It Works */} 206 <div className="max-w-4xl mx-auto"> 207 + <h2 className="text-2xl font-bold text-center text-slate-900 dark:text-slate-100 mb-8"> 208 How It Works 209 </h2> 210 <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> 211 <div className="text-center"> 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"> 213 1 214 </div> 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> 217 </div> 218 <div className="text-center"> 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"> 220 2 221 </div> 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> 224 </div> 225 <div className="text-center"> 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"> 227 3 228 </div> 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> 231 </div> 232 <div className="text-center"> 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"> 234 4 235 </div> 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> 238 </div> 239 </div> 240 </div>
+53 -18
src/pages/Results.tsx
··· 1 - import { Video, Heart } from "lucide-react"; 2 import { PLATFORMS } from "../constants/platforms"; 3 import AppHeader from "../components/AppHeader"; 4 import SearchResultCard from "../components/SearchResultCard"; ··· 41 isFollowing: boolean; 42 currentStep: string; 43 sourcePlatform: string; 44 } 45 46 export default function ResultsPage({ ··· 58 totalFound, 59 isFollowing, 60 currentStep, 61 - sourcePlatform 62 }: ResultsPageProps) { 63 const platform = PLATFORMS[sourcePlatform] || PLATFORMS.tiktok; 64 const PlatformIcon = platform.icon; 65 66 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} /> 69 70 {/* 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"> 73 <div className="flex items-center justify-between"> 74 <div className="flex items-center space-x-4"> 75 - <PlatformIcon className="w-12 h-12" /> 76 <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 80 </p> 81 </div> 82 </div> 83 {totalSelected > 0 && ( 84 <div className="text-right"> 85 <div className="text-2xl font-bold">{totalSelected}</div> 86 - <div className="text-xs text-white/80">selected</div> 87 </div> 88 )} 89 </div> ··· 91 </div> 92 93 {/* 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"> 95 <div className="max-w-3xl mx-auto px-4 py-3 flex space-x-2"> 96 <button 97 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" 99 type="button" 100 > 101 Select All 102 </button> 103 <button 104 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" 106 type="button" 107 > 108 Clear ··· 148 149 {/* Fixed Bottom Action Bar */} 150 {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"> 152 <div className="max-w-3xl mx-auto px-4"> 153 <button 154 onClick={onFollowSelected} 155 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" 157 > 158 - <Heart className="w-6 h-6" /> 159 - <span>Follow {totalSelected} Selected {totalSelected === 1 ? 'User' : 'Users'}</span> 160 </button> 161 </div> 162 </div>
··· 1 + import { Sparkles, Heart } from "lucide-react"; 2 import { PLATFORMS } from "../constants/platforms"; 3 import AppHeader from "../components/AppHeader"; 4 import SearchResultCard from "../components/SearchResultCard"; ··· 41 isFollowing: boolean; 42 currentStep: string; 43 sourcePlatform: string; 44 + reducedMotion?: boolean; 45 + isDark?: boolean; 46 + onToggleTheme?: () => void; 47 + onToggleMotion?: () => void; 48 } 49 50 export default function ResultsPage({ ··· 62 totalFound, 63 isFollowing, 64 currentStep, 65 + sourcePlatform, 66 + reducedMotion = false, 67 + isDark = false, 68 + onToggleTheme, 69 + onToggleMotion 70 }: ResultsPageProps) { 71 const platform = PLATFORMS[sourcePlatform] || PLATFORMS.tiktok; 72 const PlatformIcon = platform.icon; 73 74 return ( 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 + /> 86 87 {/* Platform Info Banner */} 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"> 106 <div className="flex items-center justify-between"> 107 <div className="flex items-center space-x-4"> 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> 111 <div> 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 115 </p> 116 </div> 117 </div> 118 {totalSelected > 0 && ( 119 <div className="text-right"> 120 <div className="text-2xl font-bold">{totalSelected}</div> 121 + <div className="text-xs font-medium">selected</div> 122 </div> 123 )} 124 </div> ··· 126 </div> 127 128 {/* Action Buttons */} 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"> 130 <div className="max-w-3xl mx-auto px-4 py-3 flex space-x-2"> 131 <button 132 onClick={onSelectAll} 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" 134 type="button" 135 > 136 Select All 137 </button> 138 <button 139 onClick={onDeselectAll} 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" 141 type="button" 142 > 143 Clear ··· 183 184 {/* Fixed Bottom Action Bar */} 185 {totalSelected > 0 && ( 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"> 187 <div className="max-w-3xl mx-auto px-4"> 188 <button 189 onClick={onFollowSelected} 190 disabled={isFollowing} 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" 192 > 193 + <Sparkles className="w-6 h-6" /> 194 + <span>Light Up {totalSelected} Connection{totalSelected === 1 ? '' : 's'} ✨</span> 195 </button> 196 </div> 197 </div>
+34 -2
tailwind.config.js
··· 1 /** @type {import('tailwindcss').Config} */ 2 export default { 3 - darkMode: 'media', // use system prefs 4 content: [ 5 "./index.html", 6 "./src/**/*.{js,ts,jsx,tsx}", 7 ], 8 theme: { 9 - extend: {}, 10 }, 11 plugins: [], 12 }
··· 1 /** @type {import('tailwindcss').Config} */ 2 export default { 3 + darkMode: 'class', // Changed from 'media' to 'class' for manual control 4 content: [ 5 "./index.html", 6 "./src/**/*.{js,ts,jsx,tsx}", 7 ], 8 theme: { 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 + }, 42 }, 43 plugins: [], 44 }