your personal website on atproto - mirror blento.app
at signup 265 lines 7.8 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).slice(0, 4) as recentLogin (recentLogin.did)} 130 <div class="group"> 131 <div 132 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" 133 > 134 <div class="flex items-center gap-2"> 135 <Avatar class="size-6" src={recentLogin.avatar} /> 136 {recentLogin.handle} 137 </div> 138 <button 139 class="z-20 cursor-pointer" 140 onclick={() => { 141 value = recentLogin.handle; 142 selectedActor = recentLogin; 143 if (loginOnSelect) onSubmit(); 144 else focusSubmit(); 145 }} 146 > 147 <div class="absolute inset-0 h-full w-full"></div> 148 <span class="sr-only">login</span> 149 </button> 150 151 <button 152 onclick={() => { 153 removeRecentLogin(recentLogin.did); 154 }} 155 class="z-30 cursor-pointer rounded-full p-0.5" 156 > 157 <svg 158 xmlns="http://www.w3.org/2000/svg" 159 fill="none" 160 viewBox="0 0 24 24" 161 stroke-width="1.5" 162 stroke="currentColor" 163 class="size-3" 164 > 165 <path 166 stroke-linecap="round" 167 stroke-linejoin="round" 168 d="M6 18 18 6M6 6l12 12" 169 /> 170 </svg> 171 <span class="sr-only">sign in with other account</span> 172 </button> 173 </div> 174 </div> 175 {/each} 176 </div> 177 {:else if !selectedActor} 178 <div class="mt-4 w-full"> 179 <HandleInput 180 bind:value 181 onselected={(a) => { 182 selectedActor = a; 183 value = a.handle; 184 if (loginOnSelect) onSubmit(); 185 else focusSubmit(); 186 }} 187 bind:ref={input} 188 /> 189 </div> 190 {:else} 191 <div 192 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" 193 > 194 <div class="flex items-center gap-2"> 195 <Avatar class="size-6" src={selectedActor.avatar} /> 196 {selectedActor.handle} 197 </div> 198 199 <button 200 onclick={() => { 201 selectedActor = undefined; 202 value = ''; 203 }} 204 class="cursor-pointer rounded-full p-0.5" 205 > 206 <svg 207 xmlns="http://www.w3.org/2000/svg" 208 fill="none" 209 viewBox="0 0 24 24" 210 stroke-width="1.5" 211 stroke="currentColor" 212 class="size-3" 213 > 214 <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /> 215 </svg> 216 <span class="sr-only">sign in with other account</span> 217 </button> 218 </div> 219 {/if} 220 221 {#if error} 222 <p class="text-accent-500 text-sm font-semibold">{error}</p> 223 {/if} 224 225 <div class="mt-4"> 226 {#if showRecentLogins} 227 <div class="mt-2 mb-4 text-sm font-medium">Or login with new handle</div> 228 229 <Button 230 onclick={() => { 231 recentLoginsView = false; 232 focusInput(); 233 }} 234 class="w-full">Login with new handle</Button 235 > 236 {:else} 237 <Button bind:ref={submitButton} type="submit" disabled={loadingLogin} class="w-full" 238 >{loadingLogin ? 'Loading...' : 'Login'}</Button 239 > 240 {/if} 241 </div> 242 243 {#if signUp} 244 <div 245 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" 246 > 247 Don't have an account? 248 <div class="mt-3"> 249 <SecondaryButton 250 onclick={async () => { 251 loadingSignup = true; 252 await signup(); 253 }} 254 disabled={loadingSignup} 255 class="w-full">{loadingSignup ? 'Loading...' : 'Sign Up'}</SecondaryButton 256 > 257 </div> 258 </div> 259 {/if} 260 </form> 261 </div> 262 </div> 263 </div> 264 </div> 265{/if}