+7
-4
netlify/functions/db-helpers.ts
+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
+1
netlify/functions/db.ts
+2
netlify/functions/get-upload-details.ts
+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
+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
+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
+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
+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
+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
+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>