1<script lang="ts">
2 import { generateColorForDid, type Account } from '$lib/accounts';
3 import { AtpClient } from '$lib/at/client';
4 import type { Did, Handle } from '@atcute/lexicons';
5 import ProfilePicture from './ProfilePicture.svelte';
6 import PfpPlaceholder from './PfpPlaceholder.svelte';
7
8 interface Props {
9 client: AtpClient;
10 accounts: Array<Account>;
11 selectedDid?: Did | null;
12 onAccountSelected: (did: Did) => void;
13 onLoginSucceed: (did: Did, handle: Handle, password: string) => void;
14 onLogout: (did: Did) => void;
15 }
16
17 let {
18 client,
19 accounts = [],
20 selectedDid = $bindable(null),
21 onAccountSelected,
22 onLoginSucceed,
23 onLogout
24 }: Props = $props();
25
26 let isDropdownOpen = $state(false);
27 let isLoginModalOpen = $state(false);
28 let loginHandle = $state('');
29 let loginPassword = $state('');
30 let loginError = $state('');
31 let isLoggingIn = $state(false);
32
33 const toggleDropdown = (e: MouseEvent) => {
34 e.stopPropagation();
35 isDropdownOpen = !isDropdownOpen;
36 };
37
38 const selectAccount = (did: Did) => {
39 onAccountSelected(did);
40 isDropdownOpen = false;
41 };
42
43 const openLoginModal = () => {
44 isLoginModalOpen = true;
45 isDropdownOpen = false;
46 loginHandle = '';
47 loginPassword = '';
48 loginError = '';
49 };
50
51 const closeLoginModal = () => {
52 isLoginModalOpen = false;
53 loginHandle = '';
54 loginPassword = '';
55 loginError = '';
56 };
57
58 const handleLogin = async () => {
59 if (!loginHandle || !loginPassword) {
60 loginError = 'please enter both handle and password';
61 return;
62 }
63
64 isLoggingIn = true;
65 loginError = '';
66
67 try {
68 const client = new AtpClient();
69 const result = await client.login(loginHandle as Handle, loginPassword);
70
71 if (!result.ok) {
72 loginError = result.error;
73 isLoggingIn = false;
74 return;
75 }
76
77 if (!client.didDoc) {
78 loginError = 'failed to get did document';
79 isLoggingIn = false;
80 return;
81 }
82
83 onLoginSucceed(client.didDoc.did, loginHandle as Handle, loginPassword);
84 closeLoginModal();
85 } catch (error) {
86 loginError = `login failed: ${error}`;
87 } finally {
88 isLoggingIn = false;
89 }
90 };
91
92 const handleKeydown = (event: KeyboardEvent) => {
93 if (event.key === 'Escape') {
94 closeLoginModal();
95 } else if (event.key === 'Enter' && !isLoggingIn) {
96 handleLogin();
97 }
98 };
99
100 const closeDropdown = () => {
101 isDropdownOpen = false;
102 };
103</script>
104
105<svelte:window onclick={closeDropdown} />
106
107<div class="relative">
108 <button
109 onclick={toggleDropdown}
110 class="flex h-16 w-16 items-center justify-center rounded-sm shadow-lg transition-all hover:scale-105 hover:shadow-xl"
111 >
112 {#if selectedDid}
113 <ProfilePicture {client} did={selectedDid} size={15} />
114 {:else}
115 <PfpPlaceholder color="var(--nucleus-accent)" size={15} />
116 {/if}
117 </button>
118
119 {#if isDropdownOpen}
120 <!-- svelte-ignore a11y_click_events_have_key_events -->
121 <!-- svelte-ignore a11y_no_static_element_interactions -->
122 <div
123 class="absolute left-0 z-10 mt-3 min-w-52 animate-fade-in-scale-fast overflow-hidden rounded-sm border-2 border-(--nucleus-accent) bg-(--nucleus-bg)/94 shadow-2xl backdrop-blur-lg transition-all"
124 onclick={(e) => e.stopPropagation()}
125 >
126 {#if accounts.length > 0}
127 <div class="p-2">
128 {#each accounts as account (account.did)}
129 {@const color = generateColorForDid(account.did)}
130 <button
131 onclick={() => selectAccount(account.did)}
132 class="
133 group flex w-full items-center gap-3 rounded-sm p-2 text-left text-sm font-medium transition-all
134 {account.did === selectedDid ? 'shadow-lg' : ''}
135 "
136 style="color: {color}; background: {account.did === selectedDid
137 ? `linear-gradient(135deg, color-mix(in srgb, var(--nucleus-accent) 20%, transparent), color-mix(in srgb, var(--nucleus-accent2) 20%, transparent))`
138 : 'transparent'};"
139 >
140 <span>@{account.handle}</span>
141 <svg
142 xmlns="http://www.w3.org/2000/svg"
143 onclick={() => onLogout(account.did)}
144 class="ml-auto hidden h-5 w-5 text-(--nucleus-accent) transition-all group-hover:[display:block] hover:scale-[1.2] hover:shadow-md"
145 width="24"
146 height="24"
147 viewBox="0 0 20 20"
148 ><path
149 fill="currentColor"
150 fill-rule="evenodd"
151 d="M8.75 1A2.75 2.75 0 0 0 6 3.75v.443q-1.193.115-2.365.298a.75.75 0 1 0 .23 1.482l.149-.022l.841 10.518A2.75 2.75 0 0 0 7.596 19h4.807a2.75 2.75 0 0 0 2.742-2.53l.841-10.52l.149.023a.75.75 0 0 0 .23-1.482A41 41 0 0 0 14 4.193V3.75A2.75 2.75 0 0 0 11.25 1zM10 4q1.26 0 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325Q8.74 4 10 4M8.58 7.72a.75.75 0 0 0-1.5.06l.3 7.5a.75.75 0 1 0 1.5-.06zm4.34.06a.75.75 0 1 0-1.5-.06l-.3 7.5a.75.75 0 1 0 1.5.06z"
152 clip-rule="evenodd"
153 /></svg
154 >
155
156 {#if account.did === selectedDid}
157 <svg
158 xmlns="http://www.w3.org/2000/svg"
159 class="ml-auto h-5 w-5 text-(--nucleus-accent) group-hover:hidden"
160 width="24"
161 height="24"
162 viewBox="0 0 24 24"
163 ><path
164 fill="currentColor"
165 fill-rule="evenodd"
166 d="M19.916 4.626a.75.75 0 0 1 .208 1.04l-9 13.5a.75.75 0 0 1-1.154.114l-6-6a.75.75 0 0 1 1.06-1.06l5.353 5.353l8.493-12.74a.75.75 0 0 1 1.04-.207"
167 clip-rule="evenodd"
168 stroke-width="1.5"
169 stroke="currentColor"
170 /></svg
171 >
172 {/if}
173 </button>
174 {/each}
175 </div>
176 <div
177 class="mx-2 h-px bg-gradient-to-r from-(--nucleus-accent) to-(--nucleus-accent2)"
178 ></div>
179 {/if}
180 <button
181 onclick={openLoginModal}
182 class="group flex w-full origin-left items-center gap-3 p-3 text-left text-sm font-semibold text-(--nucleus-accent) transition-all hover:scale-[1.1]"
183 >
184 <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
185 <path
186 stroke-linecap="round"
187 stroke-linejoin="round"
188 stroke-width="2.5"
189 d="M12 4v16m8-8H4"
190 />
191 </svg>
192 <span>add account</span>
193 </button>
194 </div>
195 {/if}
196</div>
197
198{#if isLoginModalOpen}
199 <div
200 class="fixed inset-0 z-50 flex items-center justify-center bg-(--nucleus-bg)/80 backdrop-blur-sm"
201 onclick={closeLoginModal}
202 onkeydown={handleKeydown}
203 role="button"
204 tabindex="-1"
205 >
206 <!-- svelte-ignore a11y_interactive_supports_focus -->
207 <!-- svelte-ignore a11y_click_events_have_key_events -->
208 <div
209 class="w-full max-w-md animate-fade-in-scale rounded-sm border-2 border-(--nucleus-accent) bg-(--nucleus-bg) p-4 shadow-2xl transition-all"
210 onclick={(e) => e.stopPropagation()}
211 role="dialog"
212 >
213 <div class="mb-6 flex items-center justify-between">
214 <div>
215 <h2 class="text-2xl font-bold">add account</h2>
216 <div class="mt-2 flex gap-2">
217 <div class="h-1 w-10 rounded-full bg-(--nucleus-accent)"></div>
218 <div class="h-1 w-9 rounded-full bg-(--nucleus-accent2)"></div>
219 </div>
220 </div>
221 <!-- svelte-ignore a11y_consider_explicit_label -->
222 <button
223 onclick={closeLoginModal}
224 class="rounded-xl p-2 text-(--nucleus-fg)/40 transition-all hover:scale-110 hover:text-(--nucleus-fg)"
225 >
226 <svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
227 <path
228 stroke-linecap="round"
229 stroke-linejoin="round"
230 stroke-width="2.5"
231 d="M6 18L18 6M6 6l12 12"
232 />
233 </svg>
234 </button>
235 </div>
236
237 <div class="space-y-5">
238 <div>
239 <label for="handle" class="mb-2 block text-sm font-semibold text-(--nucleus-fg)/80">
240 handle
241 </label>
242 <input
243 id="handle"
244 type="text"
245 bind:value={loginHandle}
246 placeholder="example.bsky.social"
247 class="single-line-input border-(--nucleus-accent)/40 bg-(--nucleus-accent)/3"
248 disabled={isLoggingIn}
249 />
250 </div>
251
252 <div>
253 <label for="password" class="mb-2 block text-sm font-semibold text-(--nucleus-fg)/80">
254 app password
255 </label>
256 <input
257 id="password"
258 type="password"
259 bind:value={loginPassword}
260 placeholder="xxxx-xxxx-xxxx-xxxx"
261 class="single-line-input border-(--nucleus-accent)/40 bg-(--nucleus-accent)/3"
262 disabled={isLoggingIn}
263 />
264 </div>
265
266 {#if loginError}
267 <div
268 class="rounded-sm border-2 p-4"
269 style="background: #ef444422; border-color: #ef4444;"
270 >
271 <p class="text-sm font-medium" style="color: #fca5a5;">{loginError}</p>
272 </div>
273 {/if}
274
275 <div class="flex gap-3 pt-3">
276 <button onclick={closeLoginModal} class="flex-1 action-button" disabled={isLoggingIn}>
277 cancel
278 </button>
279 <button
280 onclick={handleLogin}
281 class="flex-1 action-button border-transparent text-(--nucleus-fg)"
282 style="background: linear-gradient(135deg, var(--nucleus-accent), var(--nucleus-accent2));"
283 disabled={isLoggingIn}
284 >
285 {isLoggingIn ? 'logging in...' : 'login'}
286 </button>
287 </div>
288 </div>
289 </div>
290 </div>
291{/if}