replies timeline only, appview-less bluesky client
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}