tangled
alpha
login
or
join now
byarielm.fyi
/
atlast
16
fork
atom
ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
16
fork
atom
overview
issues
1
pulls
pipelines
more ui improvements bc it's still ugly
byarielm.fyi
4 months ago
d500a59e
e3123227
+136
-108
4 changed files
expand all
collapse all
unified
split
src
App.tsx
components
SearchResultCard.tsx
pages
Home.tsx
Login.tsx
+2
src/App.tsx
···
201
201
{currentStep === 'login' && (
202
202
<LoginPage
203
203
onSubmit={handleLogin}
204
204
+
session={session}
205
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
30
-
<div className="p-4 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
31
31
-
<div className="flex items-center space-x-3">
32
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
33
-
{result.tiktokUser.username.charAt(0).toUpperCase()}
34
34
-
</div>
35
35
-
<div className="flex-1">
36
36
-
<div className="font-bold text-gray-900 dark:text-gray-100">
30
30
+
<div className="px-4 py-3 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
31
31
+
<div className="flex items-center justify-between">
32
32
+
<div className="flex-1 min-w-0">
33
33
+
<div className="font-bold text-gray-900 dark:text-gray-100 truncate">
37
34
@{result.tiktokUser.username}
38
35
</div>
39
39
-
<div className="text-sm text-gray-500 dark:text-gray-400">
40
40
-
from {platform.name}
36
36
+
<div className="text-xs text-gray-500 dark:text-gray-400">
37
37
+
{platform.name}
41
38
</div>
42
39
</div>
43
43
-
<div className={`text-xs px-2 py-1 rounded-full ${platform.accentBg} text-white`}>
40
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
49
-
{/* Bluesky Matches */}
46
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
54
-
<p className="text-sm">Not found on Bluesky yet</p>
51
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
105
-
className={`flex items-center space-x-1 px-3 py-2 rounded-full font-medium transition-all flex-shrink-0 ${
102
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
109
+
title={isFollowed ? 'Already followed' : isSelected ? 'Selected to follow' : 'Select to follow'}
112
110
>
113
111
{isFollowed ? (
114
114
-
<>
115
115
-
<Check className="w-4 h-4" />
116
116
-
<span className="text-sm">Followed</span>
117
117
-
</>
112
112
+
<Check className="w-4 h-4" />
118
113
) : isSelected ? (
119
119
-
<>
120
120
-
<Check className="w-4 h-4" />
121
121
-
<span className="text-sm">Selected</span>
122
122
-
</>
114
114
+
<Check className="w-4 h-4" />
123
115
) : (
124
124
-
<>
125
125
-
<UserPlus className="w-4 h-4" />
126
126
-
<span className="text-sm">Select</span>
127
127
-
</>
116
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
166
-
<div className="flex items-center space-x-2 mb-1">
167
167
-
<span className="font-semibold text-gray-900 dark:text-gray-100 capitalize">
168
168
-
{upload.sourcePlatform}
169
169
-
</span>
170
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
171
-
{upload.matchedUsers} matches
166
166
+
<div className="font-semibold text-gray-900 dark:text-gray-100 capitalize mb-1">
167
167
+
{upload.sourcePlatform}
168
168
+
</div>
169
169
+
<div className="flex items-center space-x-2">
170
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
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
2
-
import { Heart, Upload, Search } from "lucide-react";
2
2
+
import { Heart, Upload, Search, ArrowRight } from "lucide-react";
3
3
4
4
interface LoginPageProps {
5
5
onSubmit: (handle: string) => void;
6
6
+
session?: { handle: string } | null;
7
7
+
onNavigate?: (step: 'home') => void;
6
8
}
7
9
8
8
-
export default function LoginPage({ onSubmit }: LoginPageProps) {
10
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
18
-
<div className="max-w-6xl mx-auto px-4 py-12">
19
19
-
{/* Welcome Section */}
20
20
-
<div className="text-center mb-16">
21
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
22
-
<Heart className="w-12 h-12 text-white" />
20
20
+
<div className="max-w-6xl mx-auto px-4 py-8 md:py-12">
21
21
+
22
22
+
{/* Hero Section - Side by side on desktop */}
23
23
+
<div className="grid md:grid-cols-2 gap-8 md:gap-12 items-start mb-12 md:mb-16">
24
24
+
{/* Left: Welcome */}
25
25
+
<div className="text-center md:text-left">
26
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
27
+
<Heart className="w-10 h-10 md:w-12 md:h-12 text-white" />
28
28
+
</div>
29
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
30
+
Welcome to ATlast
31
31
+
</h1>
32
32
+
<p className="text-lg md:text-xl lg:text-2xl text-gray-700 dark:text-gray-300 mb-6">
33
33
+
Reunite with your community on the ATmosphere
34
34
+
</p>
35
35
+
36
36
+
{/* Privacy Notice - visible on mobile */}
37
37
+
<div className="md:hidden mt-6">
38
38
+
<p className="text-sm text-gray-600 dark:text-gray-400">
39
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
40
+
</p>
41
41
+
</div>
42
42
+
</div>
43
43
+
44
44
+
{/* Right: Login Card or Dashboard Button */}
45
45
+
<div className="w-full">
46
46
+
{session ? (
47
47
+
<div className="bg-white dark:bg-gray-800 rounded-3xl shadow-2xl p-8 border border-gray-100 dark:border-gray-700">
48
48
+
<div className="text-center mb-6">
49
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
50
+
<Heart className="w-8 h-8 text-white" />
51
51
+
</div>
52
52
+
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-2">
53
53
+
You're logged in!
54
54
+
</h2>
55
55
+
<p className="text-gray-600 dark:text-gray-400">
56
56
+
Welcome back, @{session.handle}
57
57
+
</p>
58
58
+
</div>
59
59
+
60
60
+
<button
61
61
+
onClick={() => onNavigate?.('home')}
62
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
63
+
>
64
64
+
<span>Go to Dashboard</span>
65
65
+
<ArrowRight className="w-5 h-5" />
66
66
+
</button>
67
67
+
</div>
68
68
+
) : (
69
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
70
+
<h2 className="text-xl md:text-2xl font-bold text-gray-900 dark:text-gray-100 mb-2 text-center">
71
71
+
Get Started
72
72
+
</h2>
73
73
+
<p className="text-gray-600 dark:text-gray-400 text-center mb-6">
74
74
+
Connect your ATmosphere account to begin
75
75
+
</p>
76
76
+
77
77
+
<form onSubmit={handleSubmit} className="space-y-4" method="post">
78
78
+
<div>
79
79
+
<label htmlFor="atproto-handle" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
80
80
+
Your ATmosphere Handle
81
81
+
</label>
82
82
+
<input
83
83
+
id="atproto-handle"
84
84
+
type="text"
85
85
+
value={handle}
86
86
+
onChange={(e) => setHandle(e.target.value)}
87
87
+
placeholder="yourname.bsky.social"
88
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
89
+
aria-required="true"
90
90
+
aria-describedby="handle-description"
91
91
+
/>
92
92
+
<p id="handle-description" className="text-xs text-gray-500 dark:text-gray-400 mt-2">
93
93
+
Enter your full ATmosphere handle (e.g., username.bsky.social or yourname.com)
94
94
+
</p>
95
95
+
</div>
96
96
+
97
97
+
<button
98
98
+
type="submit"
99
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
100
+
aria-label="Connect to the ATmosphere"
101
101
+
>
102
102
+
Connect to the ATmosphere
103
103
+
</button>
104
104
+
</form>
105
105
+
106
106
+
<div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
107
107
+
<div className="flex items-start space-x-2 text-sm text-gray-600 dark:text-gray-400">
108
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
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
110
+
</svg>
111
111
+
<div>
112
112
+
<p className="font-medium text-gray-700 dark:text-gray-300">Secure OAuth Connection</p>
113
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
114
+
</div>
115
115
+
</div>
116
116
+
</div>
117
117
+
</div>
118
118
+
)}
23
119
</div>
24
24
-
<h1 className="text-5xl md:text-6xl font-bold text-gray-900 dark:text-gray-100 mb-4">
25
25
-
Welcome to ATlast
26
26
-
</h1>
27
27
-
<p className="text-xl md:text-2xl text-gray-700 dark:text-gray-300 mb-8 max-w-2xl mx-auto">
28
28
-
Reunite with your community on the ATmosphere
29
29
-
</p>
30
120
</div>
31
121
32
122
{/* Value Props */}
33
33
-
<div className="grid md:grid-cols-3 gap-6 mb-16 max-w-5xl mx-auto">
123
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
41
-
<p className="text-gray-600 dark:text-gray-400">
131
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
53
-
<p className="text-gray-600 dark:text-gray-400">
143
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
65
-
<p className="text-gray-600 dark:text-gray-400">
155
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
71
-
{/* Login Card */}
72
72
-
<div className="max-w-md mx-auto">
73
73
-
<div className="bg-white dark:bg-gray-800 rounded-3xl shadow-2xl p-8 border border-gray-100 dark:border-gray-700">
74
74
-
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-2 text-center">
75
75
-
Get Started
76
76
-
</h2>
77
77
-
<p className="text-gray-600 dark:text-gray-400 text-center mb-6">
78
78
-
Connect your ATmosphere account to begin finding your people
79
79
-
</p>
80
80
-
81
81
-
<form onSubmit={handleSubmit} className="space-y-4" method="post">
82
82
-
<div>
83
83
-
<label htmlFor="atproto-handle" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
84
84
-
Your ATmosphere Handle
85
85
-
</label>
86
86
-
<input
87
87
-
id="atproto-handle"
88
88
-
type="text"
89
89
-
value={handle}
90
90
-
onChange={(e) => setHandle(e.target.value)}
91
91
-
placeholder="yourname.bsky.social"
92
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
93
-
aria-required="true"
94
94
-
aria-describedby="handle-description"
95
95
-
/>
96
96
-
<p id="handle-description" className="text-xs text-gray-500 dark:text-gray-400 mt-2">
97
97
-
Enter your full ATmosphere handle (e.g., username.bsky.social)
98
98
-
</p>
99
99
-
</div>
100
100
-
101
101
-
<button
102
102
-
type="submit"
103
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
104
-
aria-label="Connect to the ATmosphere"
105
105
-
>
106
106
-
Connect to the ATmosphere
107
107
-
</button>
108
108
-
</form>
109
109
-
110
110
-
<div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
111
111
-
<div className="flex items-start space-x-2 text-sm text-gray-600 dark:text-gray-400">
112
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
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
114
-
</svg>
115
115
-
<div>
116
116
-
<p className="font-medium text-gray-700 dark:text-gray-300">Secure OAuth Connection</p>
117
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
118
-
</div>
119
119
-
</div>
120
120
-
</div>
121
121
-
</div>
122
122
-
123
123
-
{/* Privacy Notice */}
124
124
-
<div className="mt-8 text-center">
125
125
-
<p className="text-sm text-gray-600 dark:text-gray-400 max-w-md mx-auto">
126
126
-
Your data is processed locally and never stored on our servers. We only help you find matches and reconnect with your community.
127
127
-
</p>
128
128
-
</div>
161
161
+
{/* Privacy Notice - desktop only */}
162
162
+
<div className="hidden md:block text-center mb-8">
163
163
+
<p className="text-sm text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
164
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
165
+
</p>
129
166
</div>
130
167
131
168
{/* How It Works */}
132
132
-
<div className="mt-16 max-w-4xl mx-auto">
169
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
136
-
<div className="grid md:grid-cols-4 gap-4">
173
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