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

more ui improvements bc it's still ugly

+136 -108
+2
src/App.tsx
··· 201 201 {currentStep === 'login' && ( 202 202 <LoginPage 203 203 onSubmit={handleLogin} 204 + session={session} 205 + onNavigate={setCurrentStep} 204 206 /> 205 207 )} 206 208
+14 -25
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="p-4 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700"> 31 - <div className="flex items-center space-x-3"> 32 - <div className={`w-10 h-10 rounded-full bg-gradient-to-r ${platform.color} flex items-center justify-center text-white font-bold`}> 33 - {result.tiktokUser.username.charAt(0).toUpperCase()} 34 - </div> 35 - <div className="flex-1"> 36 - <div className="font-bold text-gray-900 dark:text-gray-100"> 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-center justify-between"> 32 + <div className="flex-1 min-w-0"> 33 + <div className="font-bold text-gray-900 dark:text-gray-100 truncate"> 37 34 @{result.tiktokUser.username} 38 35 </div> 39 - <div className="text-sm text-gray-500 dark:text-gray-400"> 40 - from {platform.name} 36 + <div className="text-xs text-gray-500 dark:text-gray-400"> 37 + {platform.name} 41 38 </div> 42 39 </div> 43 - <div className={`text-xs px-2 py-1 rounded-full ${platform.accentBg} text-white`}> 40 + <div className={`text-xs px-2 py-1 rounded-full ${platform.accentBg} text-white whitespace-nowrap ml-2`}> 44 41 {result.atprotoMatches.length} {result.atprotoMatches.length === 1 ? 'match' : 'matches'} 45 42 </div> 46 43 </div> 47 44 </div> 48 45 49 - {/* Bluesky Matches */} 46 + {/* ATProto Matches */} 50 47 <div className="p-4"> 51 48 {result.atprotoMatches.length === 0 ? ( 52 49 <div className="text-center py-6 text-gray-500 dark:text-gray-400"> 53 50 <MessageCircle className="w-8 h-8 mx-auto mb-2 opacity-50" /> 54 - <p className="text-sm">Not found on Bluesky yet</p> 51 + <p className="text-sm">Not found on the ATmosphere yet</p> 55 52 </div> 56 53 ) : ( 57 54 <div className="space-y-3"> ··· 102 99 <button 103 100 onClick={() => onToggleMatchSelection(match.did)} 104 101 disabled={isFollowed} 105 - className={`flex items-center space-x-1 px-3 py-2 rounded-full font-medium transition-all flex-shrink-0 ${ 102 + className={`p-2 rounded-full font-medium transition-all flex-shrink-0 ${ 106 103 isFollowed 107 104 ? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 cursor-not-allowed opacity-60' 108 105 : isSelected 109 106 ? 'bg-blue-600 text-white' 110 107 : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600' 111 108 }`} 109 + title={isFollowed ? 'Already followed' : isSelected ? 'Selected to follow' : 'Select to follow'} 112 110 > 113 111 {isFollowed ? ( 114 - <> 115 - <Check className="w-4 h-4" /> 116 - <span className="text-sm">Followed</span> 117 - </> 112 + <Check className="w-4 h-4" /> 118 113 ) : isSelected ? ( 119 - <> 120 - <Check className="w-4 h-4" /> 121 - <span className="text-sm">Selected</span> 122 - </> 114 + <Check className="w-4 h-4" /> 123 115 ) : ( 124 - <> 125 - <UserPlus className="w-4 h-4" /> 126 - <span className="text-sm">Select</span> 127 - </> 116 + <UserPlus className="w-4 h-4" /> 128 117 )} 129 118 </button> 130 119 </div>
+6 -6
src/pages/Home.tsx
··· 163 163 <Upload className="w-6 h-6 text-white" /> 164 164 </div> 165 165 <div className="flex-1 min-w-0"> 166 - <div className="flex items-center space-x-2 mb-1"> 167 - <span className="font-semibold text-gray-900 dark:text-gray-100 capitalize"> 168 - {upload.sourcePlatform} 169 - </span> 170 - <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"> 171 - {upload.matchedUsers} matches 166 + <div className="font-semibold text-gray-900 dark:text-gray-100 capitalize mb-1"> 167 + {upload.sourcePlatform} 168 + </div> 169 + <div className="flex items-center space-x-2"> 170 + <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"> 171 + {upload.matchedUsers} {upload.matchedUsers === 1 ? 'match' : 'matches'} 172 172 </span> 173 173 </div> 174 174 <div className="text-sm text-gray-600 dark:text-gray-400">
+114 -77
src/pages/Login.tsx
··· 1 1 import { useState } from "react"; 2 - import { Heart, Upload, Search } from "lucide-react"; 2 + import { Heart, Upload, Search, ArrowRight } from "lucide-react"; 3 3 4 4 interface LoginPageProps { 5 5 onSubmit: (handle: string) => void; 6 + session?: { handle: string } | null; 7 + onNavigate?: (step: 'home') => void; 6 8 } 7 9 8 - export default function LoginPage({ onSubmit }: LoginPageProps) { 10 + export default function LoginPage({ onSubmit, session, onNavigate }: LoginPageProps) { 9 11 const [handle, setHandle] = useState(""); 10 12 11 13 const handleSubmit = (e: React.FormEvent) => { ··· 15 17 16 18 return ( 17 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"> 18 - <div className="max-w-6xl mx-auto px-4 py-12"> 19 - {/* Welcome Section */} 20 - <div className="text-center mb-16"> 21 - <div className="inline-flex items-center justify-center w-24 h-24 bg-gradient-to-br from-blue-500 to-purple-600 rounded-3xl mb-6 shadow-xl"> 22 - <Heart className="w-12 h-12 text-white" /> 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> 43 + 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 83 + id="atproto-handle" 84 + type="text" 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> 116 + </div> 117 + </div> 118 + )} 23 119 </div> 24 - <h1 className="text-5xl md:text-6xl font-bold text-gray-900 dark:text-gray-100 mb-4"> 25 - Welcome to ATlast 26 - </h1> 27 - <p className="text-xl md:text-2xl text-gray-700 dark:text-gray-300 mb-8 max-w-2xl mx-auto"> 28 - Reunite with your community on the ATmosphere 29 - </p> 30 120 </div> 31 121 32 122 {/* Value Props */} 33 - <div className="grid md:grid-cols-3 gap-6 mb-16 max-w-5xl mx-auto"> 123 + <div className="grid md:grid-cols-3 gap-4 md:gap-6 mb-12 md:mb-16 max-w-5xl mx-auto"> 34 124 <div className="bg-white dark:bg-gray-800 rounded-2xl p-6 shadow-lg border border-gray-100 dark:border-gray-700"> 35 125 <div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-xl flex items-center justify-center mb-4"> 36 126 <Upload className="w-6 h-6 text-blue-600 dark:text-blue-400" /> ··· 38 128 <h3 className="text-lg font-bold text-gray-900 dark:text-gray-100 mb-2"> 39 129 Upload Your Data 40 130 </h3> 41 - <p className="text-gray-600 dark:text-gray-400"> 131 + <p className="text-gray-600 dark:text-gray-400 text-sm"> 42 132 Import your following lists from Twitter, TikTok, Instagram, and more. Your data stays private. 43 133 </p> 44 134 </div> ··· 50 140 <h3 className="text-lg font-bold text-gray-900 dark:text-gray-100 mb-2"> 51 141 Find Matches 52 142 </h3> 53 - <p className="text-gray-600 dark:text-gray-400"> 143 + <p className="text-gray-600 dark:text-gray-400 text-sm"> 54 144 We'll search the ATmosphere to find which of your follows have already migrated. 55 145 </p> 56 146 </div> ··· 62 152 <h3 className="text-lg font-bold text-gray-900 dark:text-gray-100 mb-2"> 63 153 Reconnect Instantly 64 154 </h3> 65 - <p className="text-gray-600 dark:text-gray-400"> 155 + <p className="text-gray-600 dark:text-gray-400 text-sm"> 66 156 Follow everyone at once or pick and choose. Build your community on the ATmosphere. 67 157 </p> 68 158 </div> 69 159 </div> 70 160 71 - {/* Login Card */} 72 - <div className="max-w-md mx-auto"> 73 - <div className="bg-white dark:bg-gray-800 rounded-3xl shadow-2xl p-8 border border-gray-100 dark:border-gray-700"> 74 - <h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-2 text-center"> 75 - Get Started 76 - </h2> 77 - <p className="text-gray-600 dark:text-gray-400 text-center mb-6"> 78 - Connect your ATmosphere account to begin finding your people 79 - </p> 80 - 81 - <form onSubmit={handleSubmit} className="space-y-4" method="post"> 82 - <div> 83 - <label htmlFor="atproto-handle" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> 84 - Your ATmosphere Handle 85 - </label> 86 - <input 87 - id="atproto-handle" 88 - type="text" 89 - value={handle} 90 - onChange={(e) => setHandle(e.target.value)} 91 - placeholder="yourname.bsky.social" 92 - 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" 93 - aria-required="true" 94 - aria-describedby="handle-description" 95 - /> 96 - <p id="handle-description" className="text-xs text-gray-500 dark:text-gray-400 mt-2"> 97 - Enter your full ATmosphere handle (e.g., username.bsky.social) 98 - </p> 99 - </div> 100 - 101 - <button 102 - type="submit" 103 - 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" 104 - aria-label="Connect to the ATmosphere" 105 - > 106 - Connect to the ATmosphere 107 - </button> 108 - </form> 109 - 110 - <div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700"> 111 - <div className="flex items-start space-x-2 text-sm text-gray-600 dark:text-gray-400"> 112 - <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"> 113 - <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" /> 114 - </svg> 115 - <div> 116 - <p className="font-medium text-gray-700 dark:text-gray-300">Secure OAuth Connection</p> 117 - <p className="text-xs mt-1">We use official AT Protocol OAuth. We never see your password and you can revoke access anytime.</p> 118 - </div> 119 - </div> 120 - </div> 121 - </div> 122 - 123 - {/* Privacy Notice */} 124 - <div className="mt-8 text-center"> 125 - <p className="text-sm text-gray-600 dark:text-gray-400 max-w-md mx-auto"> 126 - Your data is processed locally and never stored on our servers. We only help you find matches and reconnect with your community. 127 - </p> 128 - </div> 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> 129 166 </div> 130 167 131 168 {/* How It Works */} 132 - <div className="mt-16 max-w-4xl mx-auto"> 169 + <div className="max-w-4xl mx-auto"> 133 170 <h2 className="text-2xl font-bold text-center text-gray-900 dark:text-gray-100 mb-8"> 134 171 How It Works 135 172 </h2> 136 - <div className="grid md:grid-cols-4 gap-4"> 173 + <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> 137 174 <div className="text-center"> 138 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"> 139 176 1