your personal website on atproto - mirror blento.app
25
fork

Configure Feed

Select the types of activity you want to include in your feed.

update login in modal

Florian c869b909 97e0064d

+28 -428
-22
src/lib/atproto/UI/Button.svelte
··· 1 - <script lang="ts"> 2 - import type { HTMLButtonAttributes } from 'svelte/elements'; 3 - 4 - type Props = HTMLButtonAttributes & { 5 - children: () => any; 6 - ref?: HTMLButtonElement | null; 7 - }; 8 - 9 - let { children, ref = $bindable(), class: className, ...props }: Props = $props(); 10 - </script> 11 - 12 - <button 13 - bind:this={ref} 14 - class={[ 15 - 'bg-accent-600 hover:bg-accent-500 focus-visible:outline-accent-600 text-white', 16 - 'inline-flex cursor-pointer justify-center rounded-full px-3 py-2 text-sm font-semibold shadow-sm focus-visible:outline focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-50', 17 - className 18 - ]} 19 - {...props} 20 - > 21 - {@render children()} 22 - </button>
-91
src/lib/atproto/UI/HandleInput.svelte
··· 1 - <script lang="ts"> 2 - import { AppBskyActorDefs } from '@atcute/bluesky'; 3 - import { Combobox } from 'bits-ui'; 4 - import { searchActorsTypeahead } from '$lib/atproto'; 5 - import { Avatar } from '@foxui/core'; 6 - 7 - let results: AppBskyActorDefs.ProfileViewBasic[] = $state([]); 8 - 9 - async function search(q: string) { 10 - if (!q || q.length < 2) { 11 - results = []; 12 - return; 13 - } 14 - results = (await searchActorsTypeahead(q, 5)).actors; 15 - } 16 - let open = $state(false); 17 - 18 - let { 19 - value = $bindable(), 20 - onselected, 21 - ref = $bindable() 22 - }: { 23 - value: string; 24 - onselected: (actor: AppBskyActorDefs.ProfileViewBasic) => void; 25 - ref?: HTMLInputElement | null; 26 - } = $props(); 27 - </script> 28 - 29 - <Combobox.Root 30 - type="single" 31 - onOpenChangeComplete={(o) => { 32 - if (!o) results = []; 33 - }} 34 - bind:value={ 35 - () => { 36 - return value; 37 - }, 38 - (val) => { 39 - const profile = results.find((v) => v.handle === val); 40 - if (profile) onselected?.(profile); 41 - // Only update if val has content - prevents Combobox from clearing on Enter 42 - if (val) value = val; 43 - } 44 - } 45 - bind:open={ 46 - () => { 47 - return open && results.length > 0; 48 - }, 49 - (val) => { 50 - open = val; 51 - } 52 - } 53 - > 54 - <Combobox.Input 55 - bind:ref 56 - oninput={(e) => { 57 - value = e.currentTarget.value; 58 - search(e.currentTarget.value); 59 - }} 60 - onkeydown={(e) => { 61 - if (e.key === 'Enter') e.currentTarget.form?.requestSubmit(); 62 - }} 63 - class="focus-within:outline-accent-600 dark:focus-within:outline-accent-500 dark:placeholder:text-base-400 w-full touch-none rounded-full border-0 bg-white ring-0 outline-1 -outline-offset-1 outline-gray-300 focus-within:outline-2 focus-within:-outline-offset-2 dark:bg-white/5 dark:outline-white/10" 64 - placeholder="handle" 65 - id="" 66 - aria-label="enter your handle" 67 - /> 68 - <Combobox.Content 69 - class="border-base-300 bg-base-50 dark:bg-base-900 dark:border-base-800 z-100 max-h-[30dvh] w-full rounded-2xl border shadow-lg" 70 - sideOffset={10} 71 - align="start" 72 - side="top" 73 - > 74 - <Combobox.Viewport class="w-full p-1"> 75 - {#each results as actor (actor.did)} 76 - <Combobox.Item 77 - class="rounded-button data-highlighted:bg-accent-100 dark:data-highlighted:bg-accent-600/30 my-0.5 flex w-full cursor-pointer items-center gap-2 rounded-xl p-2 px-2" 78 - value={actor.handle} 79 - label={actor.handle} 80 - > 81 - <Avatar 82 - src={actor.avatar?.replace('avatar', 'avatar_thumbnail')} 83 - alt="" 84 - class="size-6 rounded-full" 85 - /> 86 - {actor.handle} 87 - </Combobox.Item> 88 - {/each} 89 - </Combobox.Viewport> 90 - </Combobox.Content> 91 - </Combobox.Root>
-267
src/lib/atproto/UI/LoginModal.svelte
··· 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}
-20
src/lib/atproto/UI/SecondaryButton.svelte
··· 1 - <script lang="ts"> 2 - import type { HTMLButtonAttributes } from 'svelte/elements'; 3 - 4 - type Props = HTMLButtonAttributes & { 5 - children: () => any; 6 - }; 7 - 8 - let { children, class: className, ...props }: Props = $props(); 9 - </script> 10 - 11 - <button 12 - class={[ 13 - 'bg-base-300 dark:bg-base-700 dark:text-base-50 dark:hover:bg-base-600 hover:bg-base-200 focus-visible:outline-base-600 text-black transition-colors duration-100', 14 - 'inline-flex cursor-pointer justify-center rounded-full px-3 py-2 text-sm font-semibold shadow-sm focus-visible:outline focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-50', 15 - className 16 - ]} 17 - {...props} 18 - > 19 - {@render children()} 20 - </button>
+3 -14
src/lib/cards/social/FriendsCard/FriendsCardSettings.svelte
··· 1 1 <script lang="ts"> 2 - import type { Item } from '$lib/types'; 3 2 import type { SettingsComponentProps } from '../../types'; 4 - import type { AppBskyActorDefs } from '@atcute/bluesky'; 5 3 import { AtprotoHandlePopup } from '@foxui/social'; 6 4 7 - let { item = $bindable<Item>() }: SettingsComponentProps = $props(); 5 + let { item = $bindable() }: SettingsComponentProps = $props(); 8 6 9 - let handleValue = $state(''); 10 - let inputRef: HTMLInputElement | null = $state(null); 11 - 12 - function addFriend(actor: AppBskyActorDefs.ProfileViewBasic) { 7 + function addFriend(actor: { did: string; handle: string }) { 13 8 if (!item.cardData.friends) item.cardData.friends = []; 14 9 if (item.cardData.friends.includes(actor.did)) return; 15 10 item.cardData.friends = [...item.cardData.friends, actor.did]; 16 - requestAnimationFrame(() => { 17 - handleValue = ''; 18 - if (inputRef) inputRef.value = ''; 19 - }); 20 11 } 21 12 </script> 22 13 23 - <!-- <HandleInput bind:value={handleValue} onselected={addFriend} bind:ref={inputRef} /> --> 24 - 25 - <AtprotoHandlePopup onselected={addFriend} /> 14 + <AtprotoHandlePopup onselected={addFriend} />
+2 -2
src/lib/cards/utilities/ButtonCard/ButtonCard.svelte
··· 2 2 import { goto } from '$app/navigation'; 3 3 import { user } from '$lib/atproto'; 4 4 import { getHandleOrDid } from '$lib/atproto/methods'; 5 - import { loginModalState } from '$lib/atproto/UI/LoginModal.svelte'; 5 + import { atProtoLoginModalState } from '@foxui/social'; 6 6 import { cn } from '@foxui/core'; 7 7 import type { ContentComponentProps } from '../../types'; 8 8 ··· 28 28 if (user.isLoggedIn && user.profile) { 29 29 goto('/' + getHandleOrDid(user.profile) + '/edit', {}); 30 30 } else { 31 - loginModalState.show(); 31 + atProtoLoginModalState.show(); 32 32 } 33 33 }} 34 34 class={[
+2 -2
src/lib/website/FloatingEditButton.svelte
··· 5 5 import type { WebsiteData } from '$lib/types'; 6 6 import { page } from '$app/state'; 7 7 import type { ActorIdentifier } from '@atcute/lexicons'; 8 - import { loginModalState } from '$lib/atproto/UI/LoginModal.svelte'; 8 + import { atProtoLoginModalState } from '@foxui/social'; 9 9 import { getHandleOrDid } from '$lib/atproto/methods'; 10 10 11 11 let { data }: { data: WebsiteData } = $props(); ··· 69 69 </div> 70 70 {:else if showLoginOnBlento} 71 71 <div class="fixed bottom-6 left-6 z-49"> 72 - <Button size="lg" onclick={() => loginModalState.show()}>Login</Button> 72 + <Button size="lg" onclick={() => atProtoLoginModalState.show()}>Login</Button> 73 73 </div> 74 74 {:else if showEditBlentoButton} 75 75 <div class="fixed bottom-6 left-6 z-49">
+13 -2
src/routes/+layout.svelte
··· 8 8 import YoutubeVideoPlayer, { videoPlayer } from '$lib/components/YoutubeVideoPlayer.svelte'; 9 9 import { page } from '$app/state'; 10 10 import { goto } from '$app/navigation'; 11 - import LoginModal from '$lib/atproto/UI/LoginModal.svelte'; 11 + import { AtprotoLoginModal } from '@foxui/social'; 12 + import { login, signup } from '$lib/atproto'; 13 + import type { ActorIdentifier } from '@atcute/lexicons'; 12 14 13 15 let { children, data } = $props(); 14 16 let showThemeToggle = $derived( ··· 38 40 <YoutubeVideoPlayer /> 39 41 {/if} 40 42 41 - <LoginModal /> 43 + <AtprotoLoginModal 44 + login={async (handle) => { 45 + await login(handle as ActorIdentifier); 46 + return true; 47 + }} 48 + signup={async () => { 49 + await signup(); 50 + return true; 51 + }} 52 + />
+2 -2
src/routes/[[actor=actor]]/(pages)/p/[[page]]/copy/+page.svelte
··· 10 10 import { goto } from '$app/navigation'; 11 11 import * as TID from '@atcute/tid'; 12 12 import { Button } from '@foxui/core'; 13 - import { loginModalState } from '$lib/atproto/UI/LoginModal.svelte'; 13 + import { atProtoLoginModalState } from '@foxui/social'; 14 14 15 15 let { data } = $props(); 16 16 ··· 245 245 </h1> 246 246 247 247 <div class="flex w-full justify-center"> 248 - <Button size="lg" onclick={() => loginModalState.show()}>Login</Button> 248 + <Button size="lg" onclick={() => atProtoLoginModalState.show()}>Login</Button> 249 249 </div> 250 250 {/if} 251 251 </div>
+2 -2
src/routes/[[actor=actor]]/blog/new/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { user } from '$lib/atproto/auth.svelte'; 3 - import { loginModalState } from '$lib/atproto/UI/LoginModal.svelte'; 3 + import { atProtoLoginModalState } from '@foxui/social'; 4 4 import { uploadBlob, createTID } from '$lib/atproto/methods'; 5 5 import { compressImage } from '$lib/atproto/image-helper'; 6 6 import { Badge, Button } from '@foxui/core'; ··· 384 384 class="border-base-200 dark:border-base-800 bg-base-100 dark:bg-base-900/50 rounded-2xl border p-8 text-center" 385 385 > 386 386 <p class="text-base-600 dark:text-base-400 mb-4">Log in to create a blog post.</p> 387 - <Button onclick={() => loginModalState.show()}>Log in</Button> 387 + <Button onclick={() => atProtoLoginModalState.show()}>Log in</Button> 388 388 </div> 389 389 {:else} 390 390 <!-- Draft badge -->
+2 -2
src/routes/[[actor=actor]]/events/[rkey]/EventRsvp.svelte
··· 1 1 <script lang="ts"> 2 2 import { user } from '$lib/atproto/auth.svelte'; 3 3 import { getRecord, putRecord, deleteRecord, createTID } from '$lib/atproto/methods'; 4 - import { loginModalState } from '$lib/atproto/UI/LoginModal.svelte'; 4 + import { atProtoLoginModalState } from '@foxui/social'; 5 5 import { Avatar, Button } from '@foxui/core'; 6 6 import type { Did } from '@atcute/lexicons'; 7 7 ··· 145 145 <div class="flex items-center justify-between gap-4"> 146 146 <p class="text-base-600 dark:text-base-400 text-sm">Log in to RSVP to this event</p> 147 147 148 - <Button onclick={() => loginModalState.show()}>Log in to RSVP</Button> 148 + <Button onclick={() => atProtoLoginModalState.show()}>Log in to RSVP</Button> 149 149 </div> 150 150 {:else if rsvpStatus === 'going'} 151 151 <div class="flex items-center justify-between">
+2 -2
src/routes/[[actor=actor]]/events/[rkey]/edit/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { user } from '$lib/atproto/auth.svelte'; 3 - import { loginModalState } from '$lib/atproto/UI/LoginModal.svelte'; 3 + import { atProtoLoginModalState } from '@foxui/social'; 4 4 import { uploadBlob, putRecord, deleteRecord, resolveHandle } from '$lib/atproto/methods'; 5 5 import { getCDNImageBlobUrl } from '$lib/atproto'; 6 6 import { compressImage } from '$lib/atproto/image-helper'; ··· 624 624 <p class="text-base-600 dark:text-base-400 mb-4"> 625 625 Log in to {isNew ? 'create an event' : 'edit this event'}. 626 626 </p> 627 - <Button onclick={() => loginModalState.show()}>Log in</Button> 627 + <Button onclick={() => atProtoLoginModalState.show()}>Log in</Button> 628 628 </div> 629 629 {:else} 630 630 <form