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

bunch of tiny changes

+7 -4
netlify/functions/db-helpers.ts
··· 150 150 return idMap; 151 151 } 152 152 153 - // ==================== THIS FUNCTION IS NOW FIXED ==================== 154 153 export async function bulkLinkUserToSourceAccounts( 155 154 uploadId: string, 156 155 did: string, ··· 190 189 atprotoHandle: string; 191 190 atprotoDisplayName?: string; 192 191 atprotoAvatar?: string; 192 + atprotoDescription?: string; 193 193 matchScore: number; 194 194 postCount?: number; 195 195 followerCount?: number; ··· 203 203 const atprotoDid = matches.map(m => m.atprotoDid) 204 204 const atprotoHandle = matches.map(m => m.atprotoHandle) 205 205 const atprotoDisplayName = matches.map(m => m.atprotoDisplayName || null) 206 - const atprotoAvatar = matches.map(m => m.atprotoAvatar || null) 206 + const atprotoAvatar = matches.map(m => m.atprotoAvatar || null) 207 + const atprotoDescription = matches.map(m => m.atprotoDescription || null) 207 208 const matchScore = matches.map(m => m.matchScore) 208 209 const postCount = matches.map(m => m.postCount || 0) 209 210 const followerCount = matches.map(m => m.followerCount || 0) ··· 211 212 const result = await sql` 212 213 INSERT INTO atproto_matches ( 213 214 source_account_id, atproto_did, atproto_handle, 214 - atproto_display_name, atproto_avatar, match_score, 215 - post_count, follower_count 215 + atproto_display_name, atproto_avatar, atproto_description, 216 + match_score, post_count, follower_count 216 217 ) 217 218 SELECT * FROM UNNEST( 218 219 ${sourceAccountId}::integer[], ··· 220 221 ${atprotoHandle}::text[], 221 222 ${atprotoDisplayName}::text[], 222 223 ${atprotoAvatar}::text[], 224 + ${atprotoDescription}::text[], 223 225 ${matchScore}::integer[], 224 226 ${postCount}::integer[], 225 227 ${followerCount}::integer[] ··· 232 234 atproto_handle = EXCLUDED.atproto_handle, 233 235 atproto_display_name = EXCLUDED.atproto_display_name, 234 236 atproto_avatar = EXCLUDED.atproto_avatar, 237 + atproto_description = EXCLUDED.atproto_description, 235 238 match_score = EXCLUDED.match_score, 236 239 post_count = EXCLUDED.post_count, 237 240 follower_count = EXCLUDED.follower_count,
+1
netlify/functions/db.ts
··· 100 100 atproto_handle TEXT NOT NULL, 101 101 atproto_display_name TEXT, 102 102 atproto_avatar TEXT, 103 + atproto_description TEXT, 103 104 post_count INTEGER, 104 105 follower_count INTEGER, 105 106 match_score INTEGER NOT NULL,
+2
netlify/functions/get-upload-details.ts
··· 83 83 am.atproto_handle, 84 84 am.atproto_display_name, 85 85 am.atproto_avatar, 86 + am.atproto_description, 86 87 am.match_score, 87 88 am.post_count, 88 89 am.follower_count, ··· 139 140 handle: row.atproto_handle, 140 141 displayName: row.atproto_display_name, 141 142 avatar: row.atproto_avatar, 143 + description: row.atproto_description, 142 144 matchScore: row.match_score, 143 145 postCount: row.post_count, 144 146 followerCount: row.follower_count,
+15 -2
netlify/functions/oauth-start.ts
··· 106 106 }; 107 107 } catch (error) { 108 108 console.error('OAuth start error:', error); 109 + 110 + // Provide user-friendly error messages 111 + let userMessage = 'Failed to start authentication'; 112 + 113 + if (error instanceof Error) { 114 + if (error.message.includes('resolve') || error.message.includes('not found')) { 115 + userMessage = 'Account not found. Please check your handle and try again.'; 116 + } else if (error.message.includes('network') || error.message.includes('timeout')) { 117 + userMessage = 'Network error. Please check your connection and try again.'; 118 + } else if (error.message.includes('Invalid identifier')) { 119 + userMessage = 'Invalid handle format. Please use the format: username.bsky.social'; 120 + } 121 + } 122 + 109 123 return { 110 124 statusCode: 500, 111 125 headers: { 'Content-Type': 'application/json' }, 112 126 body: JSON.stringify({ 113 - error: 'Failed to start OAuth flow', 127 + error: userMessage, 114 128 details: error instanceof Error ? error.message : 'Unknown error', 115 - stack: error instanceof Error ? error.stack : undefined 116 129 }), 117 130 }; 118 131 }
+3
netlify/functions/save-results.ts
··· 21 21 handle: string; 22 22 displayName?: string; 23 23 avatar?: string; 24 + description?: string; 24 25 matchScore: number; 25 26 postCount: number; 26 27 followerCount: number; ··· 134 135 atprotoHandle: string; 135 136 atprotoDisplayName?: string; 136 137 atprotoAvatar?: string; 138 + atprotoDescription?: string; 137 139 matchScore: number; 138 140 postCount: number; 139 141 followerCount: number; ··· 156 158 atprotoHandle: match.handle, 157 159 atprotoDisplayName: match.displayName, 158 160 atprotoAvatar: match.avatar, 161 + atprotoDescription: (match as any).description, 159 162 matchScore: match.matchScore, 160 163 postCount: match.postCount || 0, 161 164 followerCount: match.followerCount || 0,
+15 -14
src/components/SearchResultCard.tsx
··· 84 84 {match.displayName} 85 85 </div> 86 86 )} 87 - <div className="text-sm text-gray-800 dark:text-gray-200"> 87 + <a 88 + href={`https://bsky.app/profile/${match.handle}`} 89 + target="_blank" 90 + rel="noopener noreferrer" 91 + className="text-sm text-blue-600 dark:text-blue-400 hover:underline" 92 + > 88 93 @{match.handle} 89 - </div> 94 + </a> 90 95 {match.description && ( 91 96 <div className="text-sm text-gray-700 dark:text-gray-300 mt-1 line-clamp-2">{match.description}</div> 92 97 )} 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 - )} 98 - {match.followerCount && match.followerCount > 0 && ( 99 - <span>{match.followerCount.toLocaleString()} followers</span> 100 - )} 101 - </div> 102 - )} 103 - <div className="flex items-center space-x-3 mt-2"> 104 - <span className="text-xs bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-300 px-2 py-1 rounded-full font-medium"> 98 + <div className="flex items-center flex-wrap gap-x-3 gap-y-1 mt-2 text-xs text-gray-700 dark:text-gray-300"> 99 + {match.postCount && match.postCount > 0 && ( 100 + <span>{match.postCount.toLocaleString()} posts</span> 101 + )} 102 + {match.followerCount && match.followerCount > 0 && ( 103 + <span>{match.followerCount.toLocaleString()} followers</span> 104 + )} 105 + <span className="bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-300 px-2 py-1 rounded-full font-medium"> 105 106 {match.matchScore}% match 106 107 </span> 107 108 </div>
+1 -1
src/lib/parserLogic.ts
··· 15 15 const matches = [...content.matchAll(pattern)]; 16 16 17 17 // We map the results to the first captured group (match[1]), filtering out empty results. 18 - return matches.map(match => match[1]).filter(name => !!name); 18 + return matches.map(match => match[1].trim()).filter(name => !!name); 19 19 20 20 } catch (e) { 21 21 console.error(`ERROR: Invalid regex pattern '${regexPattern}':`, e);
+63 -65
src/pages/Home.tsx
··· 228 228 229 229 {/* History Tab */} 230 230 {activeTab === 'history' && ( 231 - <div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6"> 232 - <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"> 233 - <div className="flex items-center space-x-3 mb-6"> 234 - <Sparkles className="w-6 h-6 text-firefly-amber" /> 235 - <h2 className="text-xl font-bold text-slate-900 dark:text-slate-100"> 236 - Your Light Trail 237 - </h2> 238 - </div> 231 + <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"> 232 + <div className="flex items-center space-x-3 mb-6"> 233 + <Sparkles className="w-6 h-6 text-firefly-amber" /> 234 + <h2 className="text-xl font-bold text-slate-900 dark:text-slate-100"> 235 + Your Light Trail 236 + </h2> 237 + </div> 239 238 240 - {isLoading ? ( 241 - <div className="space-y-3"> 242 - {[...Array(3)].map((_, i) => ( 243 - <div key={i} className="animate-pulse flex items-center space-x-4 p-4 bg-slate-50 dark:bg-slate-700 rounded-xl"> 244 - <div className="w-12 h-12 bg-slate-200 dark:bg-slate-600 rounded-xl" /> 245 - <div className="flex-1 space-y-2"> 246 - <div className="h-4 bg-slate-200 dark:bg-slate-600 rounded w-3/4" /> 247 - <div className="h-3 bg-slate-200 dark:bg-slate-600 rounded w-1/2" /> 248 - </div> 239 + {isLoading ? ( 240 + <div className="space-y-3"> 241 + {[...Array(3)].map((_, i) => ( 242 + <div key={i} className="animate-pulse flex items-center space-x-4 p-4 bg-slate-50 dark:bg-slate-700 rounded-xl"> 243 + <div className="w-12 h-12 bg-slate-200 dark:bg-slate-600 rounded-xl" /> 244 + <div className="flex-1 space-y-2"> 245 + <div className="h-4 bg-slate-200 dark:bg-slate-600 rounded w-3/4" /> 246 + <div className="h-3 bg-slate-200 dark:bg-slate-600 rounded w-1/2" /> 249 247 </div> 250 - ))} 251 - </div> 252 - ) : uploads.length === 0 ? ( 253 - <div className="text-center py-12"> 254 - <Upload className="w-16 h-16 text-slate-300 dark:text-slate-600 mx-auto mb-4" /> 255 - <p className="text-slate-600 dark:text-slate-400 font-medium">No previous uploads yet</p> 256 - <p className="text-sm text-slate-500 dark:text-slate-500 mt-2"> 257 - Upload your first file to get started 258 - </p> 259 - </div> 260 - ) : ( 261 - <div className="space-y-3"> 262 - {uploads.map((upload) => { 263 - const destApp = ATPROTO_APPS[userSettings.platformDestinations[upload.sourcePlatform as keyof typeof userSettings.platformDestinations]]; 264 - return ( 265 - <button 266 - key={upload.uploadId} 267 - onClick={() => onLoadUpload(upload.uploadId)} 268 - 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" 269 - > 270 - <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`}> 271 - <Sparkles className="w-6 h-6 text-white" /> 272 - </div> 273 - <div className="flex-1 min-w-0"> 274 - <div className="flex flex-wrap items-start justify-between gap-x-4 gap-y-2 mb-1"> 275 - <div className="font-semibold text-slate-900 dark:text-slate-100 capitalize"> 276 - {upload.sourcePlatform} 277 - </div> 278 - <div className="flex items-center gap-2 flex-shrink-0"> 279 - <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"> 280 - {upload.matchedUsers} {upload.matchedUsers === 1 ? 'firefly' : 'fireflies'} 281 - </span> 282 - <div className="text-sm text-slate-600 dark:text-slate-400 font-medium whitespace-nowrap"> 283 - {Math.round((upload.matchedUsers / upload.totalUsers) * 100)}% 284 - </div> 285 - </div> 286 - </div> 287 - <div className="text-sm text-slate-700 dark:text-slate-300"> 288 - {upload.totalUsers} users • {formatDate(upload.createdAt)} 248 + </div> 249 + ))} 250 + </div> 251 + ) : uploads.length === 0 ? ( 252 + <div className="text-center py-12"> 253 + <Upload className="w-16 h-16 text-slate-300 dark:text-slate-600 mx-auto mb-4" /> 254 + <p className="text-slate-600 dark:text-slate-400 font-medium">No previous uploads yet</p> 255 + <p className="text-sm text-slate-500 dark:text-slate-500 mt-2"> 256 + Upload your first file to get started 257 + </p> 258 + </div> 259 + ) : ( 260 + <div className="space-y-3"> 261 + {uploads.map((upload) => { 262 + const destApp = ATPROTO_APPS[userSettings.platformDestinations[upload.sourcePlatform as keyof typeof userSettings.platformDestinations]]; 263 + return ( 264 + <button 265 + key={upload.uploadId} 266 + onClick={() => onLoadUpload(upload.uploadId)} 267 + 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" 268 + > 269 + <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`}> 270 + <Sparkles className="w-6 h-6 text-white" /> 271 + </div> 272 + <div className="flex-1 min-w-0"> 273 + <div className="flex flex-wrap items-start justify-between gap-x-4 gap-y-2 mb-1"> 274 + <div className="font-semibold text-slate-900 dark:text-slate-100 capitalize"> 275 + {upload.sourcePlatform} 289 276 </div> 290 - {destApp && ( 291 - <div className="text-xs text-gray-500 dark:text-gray-400 mt-1"> 292 - Sent to {destApp.icon} {destApp.name} 277 + <div className="flex items-center gap-2 flex-shrink-0"> 278 + <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"> 279 + {upload.matchedUsers} {upload.matchedUsers === 1 ? 'firefly' : 'fireflies'} 280 + </span> 281 + <div className="text-sm text-slate-600 dark:text-slate-400 font-medium whitespace-nowrap"> 282 + {Math.round((upload.matchedUsers / upload.totalUsers) * 100)}% 293 283 </div> 294 - )} 284 + </div> 295 285 </div> 296 - </button> 297 - ); 298 - })} 299 - </div> 300 - )} 286 + <div className="text-sm text-slate-700 dark:text-slate-300"> 287 + {upload.totalUsers} users • {formatDate(upload.createdAt)} 288 + </div> 289 + {destApp && ( 290 + <div className="text-xs text-gray-500 dark:text-gray-400 mt-1"> 291 + Sent to {destApp.icon} {destApp.name} 292 + </div> 293 + )} 294 + </div> 295 + </button> 296 + ); 297 + })} 301 298 </div> 299 + )} 302 300 </div> 303 301 )} 304 302
+4 -2
src/pages/Loading.tsx
··· 62 62 <div className="max-w-3xl mx-auto px-4 py-6"> 63 63 <div className="flex items-center justify-between"> 64 64 <div className="flex items-center space-x-4"> 65 - <div className="relative"> 65 + <div className="relative w-14 h-14"> 66 66 <PlatformIcon className="w-12 h-12" /> 67 - <Search className="w-6 h-6 absolute -bottom-1 -right-1 animate-pulse" aria-hidden="true" /> 67 + <div className="absolute -bottom-1 -right-1 w-7 h-7 bg-white dark:bg-slate-800 rounded-full flex items-center justify-center"> 68 + <Search className="w-4 h-4 animate-pulse" aria-hidden="true" /> 69 + </div> 68 70 </div> 69 71 <div> 70 72 <h2 className="text-xl font-bold">Finding Your Fireflies</h2>