tangled
alpha
login
or
join now
margin.at
/
margin
86
fork
atom
Write on the margins of the internet. Powered by the AT Protocol.
margin.at
extension
web
atproto
comments
86
fork
atom
overview
issues
4
pulls
1
pipelines
refactor login page
scanash.com
2 months ago
d9406cd5
01e5086e
+192
-176
3 changed files
expand all
collapse all
unified
split
web
src
components
RightSidebar.jsx
css
login.css
pages
Login.jsx
-3
web/src/components/RightSidebar.jsx
···
124
124
<Link to="/url" className="right-link">
125
125
Browse by URL
126
126
</Link>
127
127
-
<Link to="/highlights" className="right-link">
128
128
-
Public Highlights
129
129
-
</Link>
130
127
</nav>
131
128
</div>
132
129
)}
+116
-125
web/src/css/login.css
···
3
3
flex-direction: column;
4
4
align-items: center;
5
5
justify-content: center;
6
6
-
min-height: 70vh;
7
7
-
padding: 60px 20px;
6
6
+
min-height: 80vh;
7
7
+
padding: 40px 20px;
8
8
width: 100%;
9
9
-
max-width: 500px;
10
10
-
margin: 0 auto;
11
9
}
12
10
13
13
-
@media (max-width: 600px) {
14
14
-
.login-page {
15
15
-
padding: 40px 16px;
16
16
-
}
11
11
+
.login-header-group {
12
12
+
display: flex;
13
13
+
flex-direction: row;
14
14
+
align-items: center;
15
15
+
justify-content: center;
16
16
+
gap: 24px;
17
17
+
margin-bottom: 48px;
18
18
+
width: auto;
19
19
+
}
17
20
18
18
-
.login-at-logo {
19
19
-
font-size: 4rem;
20
20
-
}
21
21
-
22
22
-
.login-brand-name {
23
23
-
font-size: 1.25rem;
24
24
-
}
25
25
-
26
26
-
.login-brand-icon {
27
27
-
width: 40px;
28
28
-
height: 40px;
29
29
-
font-size: 1.5rem;
30
30
-
}
21
21
+
.login-logo-img {
22
22
+
width: 60px;
23
23
+
height: 60px;
24
24
+
object-fit: contain;
25
25
+
display: block;
31
26
}
32
27
33
33
-
.login-at-logo {
34
34
-
font-size: 5rem;
35
35
-
font-weight: 800;
36
36
-
color: var(--accent);
37
37
-
margin-bottom: 24px;
28
28
+
.login-x {
29
29
+
font-size: 2rem;
30
30
+
color: var(--text-tertiary);
31
31
+
font-weight: 300;
38
32
line-height: 1;
33
33
+
padding-bottom: 4px;
39
34
}
40
35
41
41
-
.login-logo-img {
42
42
-
width: 80px;
43
43
-
height: 80px;
44
44
-
margin-bottom: 24px;
45
45
-
object-fit: contain;
36
36
+
.login-atproto-icon {
37
37
+
color: #3b83f6 !important;
38
38
+
display: flex;
39
39
+
align-items: center;
40
40
+
justify-content: center;
46
41
}
47
42
48
43
.login-heading {
49
44
font-size: 1.5rem;
50
50
-
font-weight: 600;
45
45
+
font-weight: 700;
51
46
margin-bottom: 32px;
52
47
display: flex;
53
48
align-items: center;
54
54
-
gap: 10px;
49
49
+
justify-content: center;
50
50
+
gap: 8px;
55
51
text-align: center;
56
56
-
line-height: 1.4;
52
52
+
line-height: 1.3;
53
53
+
color: var(--text-primary);
57
54
}
58
55
59
56
.login-help-btn {
···
73
70
}
74
71
75
72
.login-help-text {
76
76
-
background: var(--bg-elevated);
73
73
+
background: var(--bg-tertiary);
77
74
border: 1px solid var(--border);
78
75
border-radius: var(--radius-md);
79
79
-
padding: 16px 20px;
76
76
+
padding: 16px;
80
77
margin-bottom: 24px;
81
81
-
font-size: 0.95rem;
78
78
+
font-size: 0.9rem;
82
79
color: var(--text-secondary);
83
83
-
line-height: 1.6;
80
80
+
line-height: 1.5;
84
81
text-align: center;
82
82
+
width: 100%;
85
83
}
86
84
87
85
.login-help-text code {
88
88
-
background: var(--bg-tertiary);
89
89
-
padding: 2px 8px;
86
86
+
background: rgba(255, 255, 255, 0.05);
87
87
+
padding: 2px 6px;
90
88
border-radius: var(--radius-sm);
91
91
-
font-size: 0.9rem;
89
89
+
font-size: 0.85rem;
90
90
+
font-family: var(--font-mono);
92
91
}
93
92
94
93
.login-form {
95
94
display: flex;
96
95
flex-direction: column;
97
97
-
gap: 16px;
96
96
+
gap: 20px;
98
97
width: 100%;
99
98
}
100
99
···
105
104
.login-input {
106
105
width: 100%;
107
106
padding: 14px 16px;
108
108
-
background: var(--bg-elevated);
107
107
+
background: var(--bg-secondary);
109
108
border: 1px solid var(--border);
110
109
border-radius: var(--radius-md);
111
110
color: var(--text-primary);
112
111
font-size: 1rem;
113
113
-
transition:
114
114
-
border-color 0.15s,
115
115
-
box-shadow 0.15s;
112
112
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
113
113
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
116
114
}
117
115
118
116
.login-input:focus {
119
117
outline: none;
120
118
border-color: var(--accent);
121
121
-
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
119
119
+
box-shadow: 0 0 0 4px var(--accent-subtle);
120
120
+
background: var(--bg-primary);
122
121
}
123
122
124
123
.login-input::placeholder {
···
127
126
128
127
.login-suggestions {
129
128
position: absolute;
130
130
-
top: calc(100% + 4px);
129
129
+
top: calc(100% + 8px);
131
130
left: 0;
132
131
right: 0;
133
133
-
background: var(--bg-card);
132
132
+
background: var(--bg-elevated);
134
133
border: 1px solid var(--border);
135
134
border-radius: var(--radius-md);
136
135
box-shadow: var(--shadow-lg);
137
136
overflow: hidden;
138
137
z-index: 100;
138
138
+
max-height: 300px;
139
139
+
overflow-y: auto;
139
140
}
140
141
141
142
.login-suggestion {
···
149
150
cursor: pointer;
150
151
text-align: left;
151
152
transition: background 0.1s;
153
153
+
border-bottom: 1px solid var(--border);
154
154
+
}
155
155
+
156
156
+
.login-suggestion:last-child {
157
157
+
border-bottom: none;
152
158
}
153
159
154
160
.login-suggestion:hover,
155
161
.login-suggestion.selected {
156
156
-
background: var(--bg-elevated);
162
162
+
background: var(--bg-tertiary);
157
163
}
158
164
159
165
.login-suggestion-avatar {
160
160
-
width: 40px;
161
161
-
height: 40px;
166
166
+
width: 36px;
167
167
+
height: 36px;
162
168
border-radius: var(--radius-full);
163
169
background: linear-gradient(135deg, var(--accent), #a855f7);
164
170
display: flex;
···
166
172
justify-content: center;
167
173
flex-shrink: 0;
168
174
overflow: hidden;
169
169
-
font-size: 0.875rem;
175
175
+
font-size: 0.8rem;
170
176
font-weight: 600;
171
177
color: white;
172
178
}
···
181
187
display: flex;
182
188
flex-direction: column;
183
189
min-width: 0;
190
190
+
gap: 2px;
184
191
}
185
192
186
193
.login-suggestion-name {
187
194
font-weight: 600;
195
195
+
font-size: 0.95rem;
188
196
color: var(--text-primary);
189
197
white-space: nowrap;
190
198
overflow: hidden;
···
192
200
}
193
201
194
202
.login-suggestion-handle {
195
195
-
font-size: 0.875rem;
203
203
+
font-size: 0.85rem;
196
204
color: var(--text-secondary);
197
205
white-space: nowrap;
198
206
overflow: hidden;
···
202
210
.login-error {
203
211
padding: 12px 16px;
204
212
background: rgba(239, 68, 68, 0.1);
205
205
-
border: 1px solid rgba(239, 68, 68, 0.3);
213
213
+
border: 1px solid rgba(239, 68, 68, 0.2);
206
214
border-radius: var(--radius-md);
207
207
-
color: #ef4444;
215
215
+
color: var(--error);
208
216
font-size: 0.875rem;
217
217
+
text-align: center;
209
218
}
210
219
211
211
-
.login-legal {
212
212
-
font-size: 0.75rem;
213
213
-
color: var(--text-tertiary);
214
214
-
line-height: 1.5;
215
215
-
margin-top: 16px;
216
216
-
}
217
217
-
218
218
-
.login-brand {
219
219
-
display: flex;
220
220
-
align-items: center;
221
221
-
justify-content: center;
222
222
-
gap: 12px;
223
223
-
margin-bottom: 24px;
224
224
-
}
225
225
-
226
226
-
.login-brand-icon {
227
227
-
width: 48px;
228
228
-
height: 48px;
229
229
-
background: linear-gradient(135deg, var(--accent), #a855f7);
230
230
-
border-radius: var(--radius-lg);
231
231
-
display: flex;
232
232
-
align-items: center;
233
233
-
justify-content: center;
234
234
-
font-size: 1.75rem;
235
235
-
font-weight: 800;
236
236
-
color: white;
237
237
-
}
238
238
-
239
239
-
.login-brand-name {
240
240
-
font-size: 1.75rem;
241
241
-
font-weight: 700;
242
242
-
}
243
243
-
244
244
-
.login-avatar {
245
245
-
width: 72px;
246
246
-
height: 72px;
247
247
-
border-radius: var(--radius-full);
248
248
-
background: linear-gradient(135deg, var(--accent), #a855f7);
249
249
-
display: flex;
250
250
-
align-items: center;
251
251
-
justify-content: center;
252
252
-
margin: 0 auto 16px;
253
253
-
font-weight: 700;
254
254
-
font-size: 1.5rem;
255
255
-
color: white;
256
256
-
overflow: hidden;
257
257
-
}
258
258
-
259
259
-
.login-avatar img {
220
220
+
.login-submit {
221
221
+
padding: 14px 24px;
222
222
+
font-size: 1rem;
223
223
+
font-weight: 600;
260
224
width: 100%;
261
261
-
height: 100%;
262
262
-
object-fit: cover;
225
225
+
justify-content: center;
263
226
}
264
227
265
228
.login-avatar-large {
266
266
-
width: 100px;
267
267
-
height: 100px;
229
229
+
width: 80px;
230
230
+
height: 80px;
268
231
border-radius: var(--radius-full);
269
232
background: linear-gradient(135deg, var(--accent), #a855f7);
270
233
display: flex;
···
275
238
font-size: 2rem;
276
239
color: white;
277
240
overflow: hidden;
241
241
+
box-shadow: var(--shadow-md);
278
242
}
279
243
280
244
.login-avatar-large img {
···
284
248
}
285
249
286
250
.login-welcome {
287
287
-
font-size: 1.5rem;
251
251
+
font-size: 1.25rem;
288
252
font-weight: 600;
289
253
margin-bottom: 32px;
290
254
text-align: center;
291
291
-
}
292
292
-
293
293
-
.login-welcome-name {
294
294
-
font-size: 1.25rem;
295
295
-
font-weight: 600;
296
296
-
margin-bottom: 24px;
255
255
+
color: var(--text-primary);
297
256
}
298
257
299
258
.login-actions {
···
303
262
width: 100%;
304
263
}
305
264
306
306
-
.login-btn {
307
307
-
width: 100%;
308
308
-
padding: 14px 24px;
309
309
-
font-size: 1rem;
310
310
-
font-weight: 600;
265
265
+
.morph-container {
266
266
+
display: inline-block;
267
267
+
color: var(--text-primary);
268
268
+
font-weight: 700;
269
269
+
transition:
270
270
+
opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1),
271
271
+
transform 0.4s cubic-bezier(0.4, 0, 0.2, 1),
272
272
+
filter 0.4s cubic-bezier(0.4, 0, 0.2, 1);
273
273
+
white-space: nowrap;
274
274
+
vertical-align: bottom;
311
275
}
312
276
313
313
-
.login-submit {
314
314
-
padding: 18px 32px;
315
315
-
font-size: 1.1rem;
316
316
-
font-weight: 600;
277
277
+
.morph-out {
278
278
+
opacity: 0;
279
279
+
transform: translateY(8px) scale(0.95);
280
280
+
filter: blur(4px);
281
281
+
}
282
282
+
283
283
+
.morph-in {
284
284
+
opacity: 1;
285
285
+
transform: translateY(0) scale(1);
286
286
+
filter: blur(0);
287
287
+
}
288
288
+
289
289
+
.login-legal {
290
290
+
margin-top: 24px;
291
291
+
font-size: 0.85rem;
292
292
+
color: var(--text-tertiary);
293
293
+
text-align: center;
294
294
+
line-height: 1.5;
295
295
+
}
296
296
+
297
297
+
.login-legal a {
298
298
+
color: var(--accent);
299
299
+
text-decoration: underline;
300
300
+
text-decoration-color: var(--accent);
301
301
+
text-underline-offset: 4px;
302
302
+
font-weight: 500;
303
303
+
}
304
304
+
305
305
+
.login-legal a:hover {
306
306
+
text-decoration-thickness: 2px;
307
307
+
opacity: 0.8;
317
308
}
+76
-48
web/src/pages/Login.jsx
···
2
2
import { Link } from "react-router-dom";
3
3
import { useAuth } from "../context/AuthContext";
4
4
import { searchActors, startLogin } from "../api/client";
5
5
-
import { HelpCircle } from "lucide-react";
5
5
+
import { AtSign } from "lucide-react";
6
6
import logo from "../assets/logo.svg";
7
7
8
8
export default function Login() {
···
12
12
const [showInviteInput, setShowInviteInput] = useState(false);
13
13
const [suggestions, setSuggestions] = useState([]);
14
14
const [showSuggestions, setShowSuggestions] = useState(false);
15
15
-
const [showHelp, setShowHelp] = useState(false);
16
15
const [loading, setLoading] = useState(false);
17
16
const [error, setError] = useState(null);
18
17
const [selectedIndex, setSelectedIndex] = useState(-1);
19
18
const inputRef = useRef(null);
20
19
const inviteRef = useRef(null);
21
20
const suggestionsRef = useRef(null);
21
21
+
22
22
+
const [providerIndex, setProviderIndex] = useState(0);
23
23
+
const [morphClass, setMorphClass] = useState("morph-in");
24
24
+
const providers = [
25
25
+
"AT Protocol",
26
26
+
"Bluesky",
27
27
+
"Blacksky",
28
28
+
"Tangled",
29
29
+
"selfhosted.social",
30
30
+
"Northsky",
31
31
+
"witchcraft.systems",
32
32
+
"topphie.social",
33
33
+
"altq.net",
34
34
+
];
35
35
+
36
36
+
useEffect(() => {
37
37
+
const cycleText = () => {
38
38
+
setMorphClass("morph-out");
39
39
+
40
40
+
setTimeout(() => {
41
41
+
setProviderIndex((prev) => (prev + 1) % providers.length);
42
42
+
setMorphClass("morph-in");
43
43
+
}, 400);
44
44
+
};
45
45
+
46
46
+
const interval = setInterval(cycleText, 3000);
47
47
+
return () => clearInterval(interval);
48
48
+
}, [providers.length]);
22
49
23
50
const isSelectionRef = useRef(false);
24
51
···
58
85
return () => document.removeEventListener("mousedown", handleClickOutside);
59
86
}, []);
60
87
88
88
+
if (isAuthenticated) {
89
89
+
return (
90
90
+
<div className="login-page">
91
91
+
<div className="login-avatar-large">
92
92
+
{user?.avatar ? (
93
93
+
<img src={user.avatar} alt={user.displayName || user.handle} />
94
94
+
) : (
95
95
+
<span>
96
96
+
{(user?.displayName || user?.handle || "??")
97
97
+
.substring(0, 2)
98
98
+
.toUpperCase()}
99
99
+
</span>
100
100
+
)}
101
101
+
</div>
102
102
+
<h1 className="login-welcome">
103
103
+
Welcome back, {user?.displayName || user?.handle}
104
104
+
</h1>
105
105
+
<div className="login-actions">
106
106
+
<Link to={`/profile/${user?.did}`} className="btn btn-primary">
107
107
+
View Profile
108
108
+
</Link>
109
109
+
<button onClick={logout} className="btn btn-ghost">
110
110
+
Sign out
111
111
+
</button>
112
112
+
</div>
113
113
+
</div>
114
114
+
);
115
115
+
}
116
116
+
61
117
const handleKeyDown = (e) => {
62
118
if (!showSuggestions || suggestions.length === 0) return;
63
119
···
113
169
}
114
170
};
115
171
116
116
-
if (isAuthenticated) {
117
117
-
return (
118
118
-
<div className="login-page">
119
119
-
<div className="login-avatar-large">
120
120
-
{user?.avatar ? (
121
121
-
<img src={user.avatar} alt={user.displayName || user.handle} />
122
122
-
) : (
123
123
-
<span>
124
124
-
{(user?.displayName || user?.handle || "??")
125
125
-
.substring(0, 2)
126
126
-
.toUpperCase()}
127
127
-
</span>
128
128
-
)}
129
129
-
</div>
130
130
-
<h1 className="login-welcome">
131
131
-
Welcome back, {user?.displayName || user?.handle}
132
132
-
</h1>
133
133
-
<div className="login-actions">
134
134
-
<Link to={`/profile/${user?.did}`} className="btn btn-primary">
135
135
-
View Profile
136
136
-
</Link>
137
137
-
<button onClick={logout} className="btn btn-ghost">
138
138
-
Sign out
139
139
-
</button>
140
140
-
</div>
141
141
-
</div>
142
142
-
);
143
143
-
}
144
144
-
145
172
return (
146
173
<div className="login-page">
147
147
-
<img src={logo} alt="Margin Logo" className="login-logo-img" />
174
174
+
<div className="login-header-group">
175
175
+
<img src={logo} alt="Margin Logo" className="login-logo-img" />
176
176
+
<span className="login-x">X</span>
177
177
+
<div className="login-atproto-icon">
178
178
+
<AtSign size={64} strokeWidth={2.4} />
179
179
+
</div>
180
180
+
</div>
148
181
149
182
<h1 className="login-heading">
150
150
-
Use the AT Protocol to login to Margin
151
151
-
<button
152
152
-
className="login-help-btn"
153
153
-
onClick={() => setShowHelp(!showHelp)}
154
154
-
type="button"
155
155
-
>
156
156
-
<HelpCircle size={20} />
157
157
-
</button>
183
183
+
Sign in with your{" "}
184
184
+
<span className={`morph-container ${morphClass}`}>
185
185
+
{providers[providerIndex]}
186
186
+
</span>{" "}
187
187
+
handle
158
188
</h1>
159
159
-
160
160
-
{showHelp && (
161
161
-
<p className="login-help-text">
162
162
-
The AT Protocol is an open, decentralized network for social apps.
163
163
-
Your handle looks like <code>name.bsky.social</code> or your own
164
164
-
domain.
165
165
-
</p>
166
166
-
)}
167
189
168
190
<form onSubmit={handleSubmit} className="login-form">
169
191
<div className="login-input-wrapper">
···
263
285
? "Submit Code"
264
286
: "Continue"}
265
287
</button>
288
288
+
289
289
+
<p className="login-legal">
290
290
+
By signing in, you agree to our{" "}
291
291
+
<Link to="/terms">Terms of Service</Link> and{" "}
292
292
+
<Link to="/privacy">Privacy Policy</Link>.
293
293
+
</p>
266
294
</form>
267
295
</div>
268
296
);