your personal website on atproto - mirror blento.app
at remove-extra-buttons 267 lines 7.9 kB view raw
1<script lang="ts" module> 2 export const loginModalState = $state({ 3 visible: false, 4 show: () => (loginModalState.visible = true), 5 hide: () => (loginModalState.visible = false) 6 }); 7</script> 8 9<script lang="ts"> 10 import { login, signup } from '$lib/atproto'; 11 import type { ActorIdentifier, Did } from '@atcute/lexicons'; 12 import Button from './Button.svelte'; 13 import { onMount, tick } from 'svelte'; 14 import SecondaryButton from './SecondaryButton.svelte'; 15 import HandleInput from './HandleInput.svelte'; 16 import { AppBskyActorDefs } from '@atcute/bluesky'; 17 import { Avatar } from '@foxui/core'; 18 19 let { signUp = true, loginOnSelect = true }: { signUp?: boolean; loginOnSelect?: boolean } = 20 $props(); 21 22 let value = $state(''); 23 let error: string | null = $state(null); 24 let loadingLogin = $state(false); 25 let loadingSignup = $state(false); 26 27 async function onSubmit(event?: Event) { 28 event?.preventDefault(); 29 if (loadingLogin) return; 30 31 error = null; 32 loadingLogin = true; 33 34 try { 35 await login(value as ActorIdentifier); 36 } catch (err) { 37 error = err instanceof Error ? err.message : String(err); 38 } finally { 39 loadingLogin = false; 40 } 41 } 42 43 let input: HTMLInputElement | null = $state(null); 44 let submitButton: HTMLButtonElement | null = $state(null); 45 46 $effect(() => { 47 if (!loginModalState.visible) { 48 error = null; 49 value = ''; 50 loadingLogin = false; 51 selectedActor = undefined; 52 } else { 53 focusInput(); 54 } 55 }); 56 57 function focusInput() { 58 tick().then(() => { 59 input?.focus(); 60 }); 61 } 62 function focusSubmit() { 63 tick().then(() => { 64 submitButton?.focus(); 65 }); 66 } 67 68 let selectedActor: AppBskyActorDefs.ProfileViewBasic | undefined = $state(); 69 70 let recentLogins: Record<Did, AppBskyActorDefs.ProfileViewBasic> = $state({}); 71 72 onMount(() => { 73 try { 74 recentLogins = JSON.parse(localStorage.getItem('recent-logins') || '{}'); 75 } catch { 76 console.error('Failed to load recent logins'); 77 } 78 }); 79 80 function removeRecentLogin(did: Did) { 81 try { 82 delete recentLogins[did]; 83 84 localStorage.setItem('recent-logins', JSON.stringify(recentLogins)); 85 } catch { 86 console.error('Failed to remove recent login'); 87 } 88 } 89 90 let recentLoginsView = $state(true); 91 92 let showRecentLogins = $derived( 93 Object.keys(recentLogins).length > 0 && !loadingLogin && !selectedActor && recentLoginsView 94 ); 95</script> 96 97{#if loginModalState.visible} 98 <div 99 class="fixed inset-0 z-100 w-screen overflow-y-auto" 100 aria-labelledby="modal-title" 101 role="dialog" 102 aria-modal="true" 103 > 104 <div 105 class="bg-base-50/90 dark:bg-base-950/90 fixed inset-0 backdrop-blur-sm transition-opacity" 106 onclick={() => (loginModalState.visible = false)} 107 aria-hidden="true" 108 ></div> 109 110 <div class="pointer-events-none fixed inset-0 isolate z-10 w-screen overflow-y-auto"> 111 <div 112 class="flex min-h-full w-screen items-end justify-center p-4 text-center sm:items-center sm:p-0" 113 > 114 <div 115 class="border-base-200 bg-base-100 dark:border-base-700 dark:bg-base-800 pointer-events-auto relative w-full transform overflow-hidden rounded-2xl border px-4 pt-4 pb-4 text-left shadow-xl transition-all sm:my-8 sm:max-w-sm sm:p-6" 116 > 117 <h3 class="text-base-900 dark:text-base-100 font-semibold" id="modal-title"> 118 Login with your internet handle 119 </h3> 120 121 <div class="text-base-800 dark:text-base-200 mt-2 mb-2 text-xs font-light"> 122 e.g. your bluesky account 123 </div> 124 125 <form onsubmit={onSubmit} class="mt-2 flex w-full flex-col gap-2"> 126 {#if showRecentLogins} 127 <div class="mt-2 mb-2 text-sm font-medium">Recent logins</div> 128 <div class="flex flex-col gap-2"> 129 {#each Object.values(recentLogins) 130 .filter((l) => l.handle && l.handle !== 'handle.invalid') 131 .slice(0, 4) as recentLogin (recentLogin.did)} 132 <div class="group"> 133 <div 134 class="group-hover:bg-base-300 bg-base-200 dark:bg-base-700 dark:hover:bg-base-600 dark:border-base-500/50 border-base-300 relative flex h-10 w-full items-center justify-between gap-2 rounded-full border px-2 font-semibold transition-colors duration-100" 135 > 136 <div class="flex items-center gap-2"> 137 <Avatar class="size-6" src={recentLogin.avatar} /> 138 {recentLogin.handle} 139 </div> 140 <button 141 class="z-20 cursor-pointer" 142 onclick={() => { 143 value = recentLogin.handle; 144 selectedActor = recentLogin; 145 if (loginOnSelect) onSubmit(); 146 else focusSubmit(); 147 }} 148 > 149 <div class="absolute inset-0 h-full w-full"></div> 150 <span class="sr-only">login</span> 151 </button> 152 153 <button 154 onclick={() => { 155 removeRecentLogin(recentLogin.did); 156 }} 157 class="z-30 cursor-pointer rounded-full p-0.5" 158 > 159 <svg 160 xmlns="http://www.w3.org/2000/svg" 161 fill="none" 162 viewBox="0 0 24 24" 163 stroke-width="1.5" 164 stroke="currentColor" 165 class="size-3" 166 > 167 <path 168 stroke-linecap="round" 169 stroke-linejoin="round" 170 d="M6 18 18 6M6 6l12 12" 171 /> 172 </svg> 173 <span class="sr-only">sign in with other account</span> 174 </button> 175 </div> 176 </div> 177 {/each} 178 </div> 179 {:else if !selectedActor} 180 <div class="mt-4 w-full"> 181 <HandleInput 182 bind:value 183 onselected={(a) => { 184 selectedActor = a; 185 value = a.handle; 186 if (loginOnSelect) onSubmit(); 187 else focusSubmit(); 188 }} 189 bind:ref={input} 190 /> 191 </div> 192 {:else} 193 <div 194 class="bg-base-200 dark:bg-base-700 border-base-300 dark:border-base-600 mt-4 flex h-10 w-full items-center justify-between gap-2 rounded-full border px-2 font-semibold" 195 > 196 <div class="flex items-center gap-2"> 197 <Avatar class="size-6" src={selectedActor.avatar} /> 198 {selectedActor.handle} 199 </div> 200 201 <button 202 onclick={() => { 203 selectedActor = undefined; 204 value = ''; 205 }} 206 class="cursor-pointer rounded-full p-0.5" 207 > 208 <svg 209 xmlns="http://www.w3.org/2000/svg" 210 fill="none" 211 viewBox="0 0 24 24" 212 stroke-width="1.5" 213 stroke="currentColor" 214 class="size-3" 215 > 216 <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /> 217 </svg> 218 <span class="sr-only">sign in with other account</span> 219 </button> 220 </div> 221 {/if} 222 223 {#if error} 224 <p class="text-accent-500 text-sm font-semibold">{error}</p> 225 {/if} 226 227 <div class="mt-4"> 228 {#if showRecentLogins} 229 <div class="mt-2 mb-4 text-sm font-medium">Or login with new handle</div> 230 231 <Button 232 onclick={() => { 233 recentLoginsView = false; 234 focusInput(); 235 }} 236 class="w-full">Login with new handle</Button 237 > 238 {:else} 239 <Button bind:ref={submitButton} type="submit" disabled={loadingLogin} class="w-full" 240 >{loadingLogin ? 'Loading...' : 'Login'}</Button 241 > 242 {/if} 243 </div> 244 245 {#if signUp} 246 <div 247 class="border-base-200 dark:border-base-700 text-base-800 dark:text-base-200 mt-4 border-t pt-4 text-sm leading-7" 248 > 249 Don't have an account? 250 <div class="mt-3"> 251 <SecondaryButton 252 onclick={async () => { 253 loadingSignup = true; 254 await signup(); 255 }} 256 disabled={loadingSignup} 257 class="w-full">{loadingSignup ? 'Loading...' : 'Sign Up'}</SecondaryButton 258 > 259 </div> 260 </div> 261 {/if} 262 </form> 263 </div> 264 </div> 265 </div> 266 </div> 267{/if}