+162
-79
frontend/src/routes/login/+page.svelte
+162
-79
frontend/src/routes/login/+page.svelte
···
1
1
<script lang="ts">
2
-
import { APP_NAME, APP_TAGLINE } from '$lib/branding';
2
+
import { APP_NAME } from '$lib/branding';
3
3
import { API_URL } from '$lib/config';
4
4
import HandleAutocomplete from '$lib/components/HandleAutocomplete.svelte';
5
5
6
6
let handle = $state('');
7
7
let loading = $state(false);
8
+
let showHandleInfo = $state(false);
9
+
let showPdsInfo = $state(false);
8
10
9
11
function startOAuth(e: SubmitEvent) {
10
12
e.preventDefault();
11
13
if (!handle.trim()) return;
12
14
loading = true;
13
-
// redirect to backend OAuth start endpoint
14
15
window.location.href = `${API_URL}/auth/start?handle=${encodeURIComponent(handle)}`;
15
16
}
16
17
···
21
22
22
23
<div class="container">
23
24
<div class="login-card">
24
-
<h1>{APP_NAME}</h1>
25
-
<p>{APP_TAGLINE}</p>
25
+
<h1>sign in to {APP_NAME}</h1>
26
26
27
27
<form onsubmit={startOAuth}>
28
28
<div class="input-group">
29
-
<div class="label-row">
30
-
<label for="handle">atproto handle</label>
31
-
<a
32
-
href="https://atproto.com/specs/handle"
33
-
target="_blank"
34
-
rel="noopener noreferrer"
35
-
class="help-link"
36
-
title="learn about ATProto handles"
37
-
>
38
-
what's this?
39
-
</a>
40
-
</div>
29
+
<label for="handle">internet handle</label>
41
30
<HandleAutocomplete
42
31
bind:value={handle}
43
32
onSelect={handleSelect}
44
-
placeholder="yourname.bsky.social"
33
+
placeholder="you.bsky.social"
45
34
disabled={loading}
46
35
/>
47
-
<p class="input-help">
48
-
don't have one?
49
-
<a href="https://bsky.app" target="_blank" rel="noopener noreferrer">create a free Bluesky account</a>
50
-
to get your ATProto identity
51
-
</p>
52
36
</div>
53
37
54
-
<button type="submit" disabled={loading || !handle.trim()}>
55
-
{loading ? 'redirecting...' : 'sign in with atproto'}
38
+
<button type="submit" class="primary" disabled={loading || !handle.trim()}>
39
+
{loading ? 'redirecting...' : 'sign in'}
56
40
</button>
57
41
</form>
42
+
43
+
<div class="faq">
44
+
<button
45
+
class="faq-toggle"
46
+
onclick={() => (showHandleInfo = !showHandleInfo)}
47
+
aria-expanded={showHandleInfo}
48
+
>
49
+
<span>what is an internet handle?</span>
50
+
<svg
51
+
class="chevron"
52
+
class:open={showHandleInfo}
53
+
width="16"
54
+
height="16"
55
+
viewBox="0 0 24 24"
56
+
fill="none"
57
+
stroke="currentColor"
58
+
stroke-width="2"
59
+
>
60
+
<polyline points="6 9 12 15 18 9"></polyline>
61
+
</svg>
62
+
</button>
63
+
{#if showHandleInfo}
64
+
<div class="faq-content">
65
+
<p>
66
+
your internet handle is a domain that identifies you across apps built on
67
+
<a href="https://atproto.com" target="_blank" rel="noopener">AT Protocol</a>.
68
+
if you signed up for Bluesky or another ATProto service, you already have one
69
+
(like <code>yourname.bsky.social</code>).
70
+
</p>
71
+
<p>
72
+
read more at <a href="https://internethandle.org" target="_blank" rel="noopener">internethandle.org</a>.
73
+
</p>
74
+
</div>
75
+
{/if}
76
+
77
+
<button
78
+
class="faq-toggle"
79
+
onclick={() => (showPdsInfo = !showPdsInfo)}
80
+
aria-expanded={showPdsInfo}
81
+
>
82
+
<span>don't have one?</span>
83
+
<svg
84
+
class="chevron"
85
+
class:open={showPdsInfo}
86
+
width="16"
87
+
height="16"
88
+
viewBox="0 0 24 24"
89
+
fill="none"
90
+
stroke="currentColor"
91
+
stroke-width="2"
92
+
>
93
+
<polyline points="6 9 12 15 18 9"></polyline>
94
+
</svg>
95
+
</button>
96
+
{#if showPdsInfo}
97
+
<div class="faq-content">
98
+
<p>
99
+
the easiest way to get one is to sign up for <a href="https://bsky.app" target="_blank" rel="noopener">Bluesky</a>.
100
+
once you have an account, you can use that handle here.
101
+
</p>
102
+
</div>
103
+
{/if}
104
+
</div>
58
105
</div>
59
106
</div>
60
107
···
71
118
.login-card {
72
119
background: var(--bg-tertiary);
73
120
border: 1px solid var(--border-subtle);
74
-
border-radius: 8px;
75
-
padding: 3rem;
76
-
max-width: 400px;
121
+
border-radius: 12px;
122
+
padding: 2.5rem;
123
+
max-width: 420px;
77
124
width: 100%;
78
125
}
79
126
80
127
h1 {
81
-
font-size: 2.5rem;
82
-
margin: 0 0 0.5rem 0;
128
+
font-size: 1.75rem;
129
+
margin: 0 0 2rem 0;
83
130
color: var(--text-primary);
84
131
text-align: center;
132
+
font-weight: 600;
133
+
white-space: nowrap;
85
134
}
86
135
87
-
p {
88
-
color: var(--text-tertiary);
89
-
text-align: center;
90
-
margin: 0 0 2rem 0;
136
+
form {
137
+
display: flex;
138
+
flex-direction: column;
139
+
gap: 1.5rem;
140
+
}
141
+
142
+
.input-group {
143
+
display: flex;
144
+
flex-direction: column;
145
+
gap: 0.5rem;
146
+
}
147
+
148
+
label {
149
+
color: var(--text-secondary);
150
+
font-size: 0.9rem;
151
+
}
152
+
153
+
button.primary {
154
+
width: 100%;
155
+
padding: 0.85rem;
156
+
background: var(--accent);
157
+
color: white;
158
+
border: none;
159
+
border-radius: 8px;
91
160
font-size: 0.95rem;
161
+
font-weight: 500;
162
+
font-family: inherit;
163
+
cursor: pointer;
164
+
transition: all 0.15s;
92
165
}
93
166
94
-
.input-group {
95
-
margin-bottom: 1.5rem;
167
+
button.primary:hover:not(:disabled) {
168
+
opacity: 0.9;
169
+
}
170
+
171
+
button.primary:disabled {
172
+
opacity: 0.5;
173
+
cursor: not-allowed;
96
174
}
97
175
98
-
.label-row {
176
+
.faq {
177
+
margin-top: 1.5rem;
178
+
border-top: 1px solid var(--border-subtle);
179
+
padding-top: 1rem;
180
+
}
181
+
182
+
.faq-toggle {
183
+
width: 100%;
99
184
display: flex;
100
185
justify-content: space-between;
101
186
align-items: center;
102
-
margin-bottom: 0.5rem;
103
-
}
104
-
105
-
label {
187
+
padding: 0.75rem 0;
188
+
background: none;
189
+
border: none;
106
190
color: var(--text-secondary);
191
+
font-family: inherit;
107
192
font-size: 0.9rem;
193
+
cursor: pointer;
194
+
text-align: left;
108
195
}
109
196
110
-
.help-link {
111
-
color: var(--accent);
112
-
text-decoration: none;
113
-
font-size: 0.85rem;
114
-
transition: color 0.2s;
197
+
.faq-toggle:hover {
198
+
color: var(--text-primary);
115
199
}
116
200
117
-
.help-link:hover {
118
-
color: var(--accent-hover);
119
-
text-decoration: underline;
201
+
.chevron {
202
+
transition: transform 0.2s;
203
+
flex-shrink: 0;
120
204
}
121
205
122
-
.input-help {
123
-
margin: 0.5rem 0 0 0;
206
+
.chevron.open {
207
+
transform: rotate(180deg);
208
+
}
209
+
210
+
.faq-content {
211
+
padding: 0 0 1rem 0;
212
+
color: var(--text-tertiary);
124
213
font-size: 0.85rem;
125
-
color: var(--text-tertiary);
214
+
line-height: 1.6;
126
215
}
127
216
128
-
.input-help a {
217
+
.faq-content p {
218
+
margin: 0 0 0.75rem 0;
219
+
text-align: left;
220
+
}
221
+
222
+
.faq-content p:last-child {
223
+
margin-bottom: 0;
224
+
}
225
+
226
+
.faq-content a {
129
227
color: var(--accent);
130
228
text-decoration: none;
131
-
transition: color 0.2s;
132
229
}
133
230
134
-
.input-help a:hover {
135
-
color: var(--accent-hover);
231
+
.faq-content a:hover {
136
232
text-decoration: underline;
137
233
}
138
234
139
-
button {
140
-
width: 100%;
141
-
padding: 0.75rem;
142
-
background: var(--accent);
143
-
color: white;
144
-
border: none;
235
+
.faq-content code {
236
+
background: var(--bg-secondary);
237
+
padding: 0.15rem 0.4rem;
145
238
border-radius: 4px;
146
-
font-size: 1rem;
147
-
font-weight: 600;
148
-
font-family: inherit;
149
-
cursor: pointer;
150
-
transition: all 0.2s;
151
-
}
152
-
153
-
button:hover:not(:disabled) {
154
-
background: var(--accent-hover);
155
-
transform: translateY(-1px);
156
-
box-shadow: 0 4px 12px color-mix(in srgb, var(--accent) 30%, transparent);
239
+
font-size: 0.85em;
157
240
}
158
241
159
-
button:disabled {
160
-
opacity: 0.5;
161
-
cursor: not-allowed;
162
-
transform: none;
163
-
}
242
+
@media (max-width: 480px) {
243
+
.login-card {
244
+
padding: 2rem 1.5rem;
245
+
}
164
246
165
-
button:active:not(:disabled) {
166
-
transform: translateY(0);
247
+
h1 {
248
+
font-size: 1.5rem;
249
+
}
167
250
}
168
251
</style>