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

Configure Feed

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

some styling updates, move editing into modal

Florian 24db54d6 c85e23fc

+152 -142
+2 -1
src/lib/atproto/methods.ts
··· 247 247 } 248 248 }); 249 249 250 - if (!record.ok) throw new Error((record.data as { message?: string })?.message ?? 'Record not found'); 250 + if (!record.ok) 251 + throw new Error((record.data as { message?: string })?.message ?? 'Record not found'); 251 252 252 253 return JSON.parse(JSON.stringify(record.data)); 253 254 }
+4 -137
src/lib/website/EditableProfile.svelte
··· 1 1 <script lang="ts"> 2 - import type { WebsiteData, PronounSet } from '$lib/types'; 2 + import type { WebsiteData } from '$lib/types'; 3 3 import { getImage, compressImage, getProfilePosition } from '$lib/helper'; 4 4 import PlainTextEditor from '$lib/components/PlainTextEditor.svelte'; 5 5 import MarkdownTextEditor from '$lib/components/MarkdownTextEditor.svelte'; 6 - import { Avatar, Switch, Label } from '@foxui/core'; 6 + import { Avatar } from '@foxui/core'; 7 7 import MadeWithBlento from './MadeWithBlento.svelte'; 8 + import Pronouns from './Pronouns.svelte'; 8 9 9 10 let { data = $bindable(), hideBlento = false }: { data: WebsiteData; hideBlento?: boolean } = 10 11 $props(); 11 12 12 13 let fileInput: HTMLInputElement; 13 - let setsContainer: HTMLDivElement; 14 14 let isHoveringAvatar = $state(false); 15 - let editingPronouns = $state(false); 16 - let pronounSets: PronounSet[] = $state(getInitialPronounSets()); 17 - let displayMode: 'all' | 'firstOnly' = $state( 18 - data.pronounsRecord?.value?.displayMode === 'firstOnly' ? 'firstOnly' : 'all' 19 - ); 20 - 21 - function getInitialPronounSets(): PronounSet[] { 22 - if (data.pronounsRecord?.value?.sets?.length) { 23 - return structuredClone(data.pronounsRecord.value.sets); 24 - } 25 - if (data.pronouns) { 26 - return [{ forms: data.pronouns.split('/').map((s) => s.trim()) }]; 27 - } 28 - return [{ forms: [''] }]; 29 - } 30 15 31 16 async function handleAvatarChange(event: Event) { 32 17 const target = event.target as HTMLInputElement; ··· 63 48 function onTextUpdate() { 64 49 data = { ...data }; 65 50 } 66 - 67 - function addSet() { 68 - pronounSets = [...pronounSets, { forms: [''] }]; 69 - requestAnimationFrame(() => { 70 - setsContainer?.scrollTo({ top: setsContainer.scrollHeight, behavior: 'smooth' }); 71 - }); 72 - } 73 - 74 - function removeSet(index: number) { 75 - pronounSets = pronounSets.filter((_, i) => i !== index); 76 - updatePronouns(); 77 - } 78 - 79 - function updateSetInput(index: number, value: string) { 80 - pronounSets[index] = { forms: value.split('/').map((s) => s.trim()) }; 81 - updatePronouns(); 82 - } 83 - 84 - function updatePronouns() { 85 - const validSets = pronounSets.filter((set) => set.forms.some((form) => form.length > 0)); 86 - if (validSets.length > 0) { 87 - const setsToShow = displayMode === 'firstOnly' ? validSets.slice(0, 1) : validSets; 88 - data.pronouns = setsToShow.map((set) => set.forms.join('/')).join(' · '); 89 - data.pronounsRecord = { 90 - value: { 91 - sets: validSets, 92 - displayMode 93 - } 94 - }; 95 - } else { 96 - data.pronouns = undefined; 97 - data.pronounsRecord = undefined; 98 - } 99 - data = { ...data }; 100 - } 101 51 </script> 102 52 103 53 <div ··· 185 135 </div> 186 136 {/if} 187 137 188 - {#if editingPronouns} 189 - <div class="-mt-2 flex flex-col gap-2"> 190 - <div bind:this={setsContainer} class="flex max-h-48 flex-col gap-2 overflow-y-auto"> 191 - {#each pronounSets as set, i (i)} 192 - <div 193 - class={[ 194 - 'flex items-center gap-1', 195 - displayMode === 'firstOnly' && i > 0 && 'opacity-40' 196 - ]} 197 - > 198 - <input 199 - type="text" 200 - value={set.forms.join('/')} 201 - oninput={(e) => updateSetInput(i, e.currentTarget.value)} 202 - placeholder="e.g. she/her" 203 - class="bg-base-100 dark:bg-base-800 text-base-600 dark:text-base-300 w-full rounded px-2 py-1 text-sm outline-none" 204 - /> 205 - {#if pronounSets.length > 1} 206 - <button 207 - type="button" 208 - class="text-base-400 hover:text-red-500 cursor-pointer text-sm transition-colors" 209 - onclick={() => removeSet(i)} 210 - > 211 - &times; 212 - </button> 213 - {/if} 214 - </div> 215 - {/each} 216 - </div> 217 - {#if pronounSets.length > 1} 218 - <div class="flex items-center gap-1.5"> 219 - <Switch 220 - checked={displayMode === 'firstOnly'} 221 - onCheckedChange={(checked) => { 222 - displayMode = checked ? 'firstOnly' : 'all'; 223 - updatePronouns(); 224 - }} 225 - class="scale-75" 226 - /> 227 - <Label class="text-base-500 text-xs">show first only</Label> 228 - </div> 229 - {/if} 230 - <div class="flex gap-2"> 231 - {#if pronounSets.length < 10} 232 - <button 233 - type="button" 234 - class="text-base-500 hover:text-base-300 cursor-pointer text-xs" 235 - onclick={addSet} 236 - > 237 - + add set 238 - </button> 239 - {/if} 240 - <button 241 - type="button" 242 - class="text-base-500 hover:text-base-300 cursor-pointer text-xs" 243 - onclick={() => (editingPronouns = false)} 244 - > 245 - preview 246 - </button> 247 - </div> 248 - </div> 249 - {:else} 250 - <button 251 - type="button" 252 - class="border-base-500/30 hover:border-base-400 text-base-500 dark:text-base-400 hover:text-base-300 -mt-2 flex cursor-pointer items-center gap-1 rounded border border-dashed px-2 py-0.5 text-left text-sm transition-colors" 253 - onclick={() => (editingPronouns = true)} 254 - > 255 - <svg 256 - xmlns="http://www.w3.org/2000/svg" 257 - fill="none" 258 - viewBox="0 0 24 24" 259 - stroke-width="1.5" 260 - stroke="currentColor" 261 - class="size-3" 262 - > 263 - <path 264 - stroke-linecap="round" 265 - stroke-linejoin="round" 266 - d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" 267 - /> 268 - </svg> 269 - {data.pronouns || 'add pronouns'} 270 - </button> 271 - {/if} 138 + <Pronouns bind:data editing /> 272 139 273 140 <!-- Editable Description --> 274 141 <div class="scrollbar -mx-4 grow overflow-x-hidden overflow-y-scroll px-4">
+3 -4
src/lib/website/Profile.svelte
··· 7 7 import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte'; 8 8 import MadeWithBlento from './MadeWithBlento.svelte'; 9 9 import { Avatar } from '@foxui/core'; 10 + import Pronouns from './Pronouns.svelte'; 10 11 11 12 let { 12 13 data, ··· 61 62 {getName(data)} 62 63 </div> 63 64 64 - {#if data.pronouns} 65 - <div class="text-base-500 dark:text-base-400 -mt-2 text-sm"> 66 - {data.pronouns} 67 - </div> 65 + {#if data.pronounsRecord?.value?.sets?.length} 66 + <Pronouns {data} /> 68 67 {/if} 69 68 70 69 <div class="scrollbar -mx-4 grow overflow-x-hidden overflow-y-scroll px-4">
+143
src/lib/website/Pronouns.svelte
··· 1 + <script lang="ts"> 2 + import type { WebsiteData, PronounSet } from '$lib/types'; 3 + import { Badge, Button, Input, Switch, Label } from '@foxui/core'; 4 + import Modal from '$lib/components/modal/Modal.svelte'; 5 + 6 + let { 7 + data = $bindable(), 8 + editing = false 9 + }: { 10 + data: WebsiteData; 11 + editing?: boolean; 12 + } = $props(); 13 + 14 + let modalOpen = $state(false); 15 + let editSets: PronounSet[] = $state([]); 16 + let editDisplayMode: 'all' | 'firstOnly' = $state('all'); 17 + 18 + function openModal() { 19 + if (data.pronounsRecord?.value?.sets?.length) { 20 + editSets = JSON.parse(JSON.stringify(data.pronounsRecord.value.sets)); 21 + editDisplayMode = data.pronounsRecord.value.displayMode === 'firstOnly' ? 'firstOnly' : 'all'; 22 + } else { 23 + editSets = [{ forms: [''] }]; 24 + editDisplayMode = 'all'; 25 + } 26 + modalOpen = true; 27 + } 28 + 29 + function save() { 30 + const validSets = editSets.filter((set) => set.forms.some((form) => form.length > 0)); 31 + if (validSets.length > 0) { 32 + data.pronounsRecord = { 33 + value: { 34 + sets: validSets, 35 + displayMode: editDisplayMode 36 + } 37 + }; 38 + } else { 39 + data.pronounsRecord = undefined; 40 + } 41 + data = { ...data }; 42 + modalOpen = false; 43 + } 44 + 45 + function addSet() { 46 + editSets = [...editSets, { forms: [''] }]; 47 + } 48 + 49 + function removeSet(index: number) { 50 + editSets = editSets.filter((_, i) => i !== index); 51 + } 52 + 53 + function updateSetInput(index: number, value: string) { 54 + editSets[index] = { forms: value.split('/').map((s) => s.trim()) }; 55 + } 56 + 57 + let allSets = $derived(data.pronounsRecord?.value?.sets ?? []); 58 + let sets = $derived( 59 + data.pronounsRecord?.value?.displayMode === 'firstOnly' ? allSets.slice(0, 1) : allSets 60 + ); 61 + </script> 62 + 63 + {#if sets.length} 64 + <div class="flex flex-wrap gap-1"> 65 + {#each sets as set, i (i)} 66 + <Badge>{set.forms.join('/')}</Badge> 67 + {/each} 68 + </div> 69 + {/if} 70 + 71 + {#if editing} 72 + <Button size="sm" onclick={openModal}> 73 + <svg 74 + xmlns="http://www.w3.org/2000/svg" 75 + fill="none" 76 + viewBox="0 0 24 24" 77 + stroke-width="1.5" 78 + stroke="currentColor" 79 + class="size-3" 80 + > 81 + <path 82 + stroke-linecap="round" 83 + stroke-linejoin="round" 84 + d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" 85 + /> 86 + </svg> 87 + {sets.length ? 'edit' : 'add'} pronouns 88 + </Button> 89 + 90 + <Modal open={modalOpen} onOpenChange={(v) => (modalOpen = v)} closeButton> 91 + <div class="flex flex-col gap-4"> 92 + <h3 class="text-base-900 dark:text-base-100 text-lg font-semibold">Edit pronouns</h3> 93 + 94 + <div class="flex flex-col gap-2"> 95 + {#each editSets as set, i (i)} 96 + <div class="flex items-center gap-2"> 97 + <Input 98 + value={set.forms.join('/')} 99 + oninput={(e) => updateSetInput(i, e.currentTarget.value)} 100 + placeholder="e.g. she/her" 101 + variant="secondary" 102 + sizeVariant="sm" 103 + class="grow" 104 + /> 105 + {#if editSets.length > 1} 106 + <Button size="iconSm" variant="ghost" onclick={() => removeSet(i)}>&times;</Button> 107 + {/if} 108 + </div> 109 + {/each} 110 + </div> 111 + 112 + {#if editSets.length < 10} 113 + <Button 114 + size="sm" 115 + variant="secondary" 116 + onclick={addSet} 117 + disabled={!editSets.at(-1)?.forms.some((f) => f.length > 0)} 118 + class="w-fit" 119 + > 120 + + add more pronouns 121 + </Button> 122 + {/if} 123 + 124 + {#if editSets.length > 1} 125 + <div class="flex items-center gap-1.5"> 126 + <Switch 127 + id="pronouns-display-mode" 128 + checked={editDisplayMode === 'firstOnly'} 129 + onCheckedChange={(checked) => { 130 + editDisplayMode = checked ? 'firstOnly' : 'all'; 131 + }} 132 + /> 133 + <Label for="pronouns-display-mode">show first only</Label> 134 + </div> 135 + {/if} 136 + 137 + <div class="flex justify-end gap-2"> 138 + <Button variant="secondary" onclick={() => (modalOpen = false)}>Cancel</Button> 139 + <Button onclick={save}>Save</Button> 140 + </div> 141 + </div> 142 + </Modal> 143 + {/if}