This repository has no description
0

Configure Feed

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

core / web / src / lib / components / profile / ProfileEditForm.svelte
3.9 kB 130 lines
1<script lang="ts"> 2 import { untrack } from "svelte"; 3 import Check from "$icon/check"; 4 import Link from "$icon/link"; 5 import MapPin from "$icon/map-pin"; 6 import X from "$icon/x"; 7 import Button from "$lib/components/ui/Button.svelte"; 8 import { getAuth } from "$lib/auth.svelte"; 9 import { putProfile } from "$lib/api/profile"; 10 import type { ProfileRecord } from "$lib/api/records"; 11 12 interface Props { 13 profile: ProfileRecord | null; 14 onSaved: (record: ProfileRecord) => void; 15 onCancel: () => void; 16 } 17 18 let { profile, onSaved, onCancel }: Props = $props(); 19 20 const auth = getAuth(); 21 22 // seed once; the profile prop can't change while the form is open. 23 const seed = untrack(() => profile); 24 let description = $state(seed?.description ?? ""); 25 let pronouns = $state(seed?.pronouns ?? ""); 26 let location = $state(seed?.location ?? ""); 27 let bluesky = $state(seed?.bluesky ?? false); 28 let links = $state( 29 Array.from({ length: 5 }, (_, index) => ({ 30 value: (seed?.links?.[index] as string | undefined) ?? "" 31 })) 32 ); 33 let busy = $state(false); 34 let error = $state<string | null>(null); 35 36 const inputClass = 37 "w-full rounded border border-border-default bg-background-default px-2 py-1 outline-none focus:border-border-strong focus:ring-1 focus:ring-border-strong"; 38 39 const save = async (event: SubmitEvent) => { 40 event.preventDefault(); 41 const agent = auth.agent; 42 if (!agent || busy) return; 43 busy = true; 44 error = null; 45 const cleanLinks = links.map((link) => link.value.trim()).filter(Boolean); 46 // put replaces the whole record, so carry unedited fields (avatar, pins, stats). 47 const record: ProfileRecord = { 48 ...profile, 49 $type: "sh.tangled.actor.profile", 50 bluesky, 51 description: description.trim() || undefined, 52 pronouns: pronouns.trim() || undefined, 53 location: location.trim() || undefined, 54 links: cleanLinks.length > 0 ? (cleanLinks as ProfileRecord["links"]) : undefined 55 }; 56 try { 57 await putProfile(agent, record); 58 onSaved(record); 59 } catch { 60 error = "Could not save profile. Try again."; 61 } finally { 62 busy = false; 63 } 64 }; 65</script> 66 67<form class="my-2 flex max-w-full flex-col gap-4 text-sm" onsubmit={save}> 68 <div class="flex flex-col gap-1"> 69 <label for="profile-bio">Bio</label> 70 <textarea 71 id="profile-bio" 72 class={inputClass} 73 rows="3" 74 placeholder="Write a bio" 75 bind:value={description}></textarea> 76 </div> 77 78 <div class="flex flex-col gap-1"> 79 <label for="profile-pronouns">Pronouns</label> 80 <input 81 id="profile-pronouns" 82 type="text" 83 class={inputClass} 84 placeholder="they/them" 85 bind:value={pronouns} 86 /> 87 </div> 88 89 <div class="flex flex-col gap-1"> 90 <label for="profile-location">Location</label> 91 <div class="flex w-full items-center gap-2"> 92 <MapPin class="size-4 shrink-0" aria-hidden="true" /> 93 <input id="profile-location" type="text" class={inputClass} bind:value={location} /> 94 </div> 95 </div> 96 97 <div class="flex flex-col gap-1"> 98 <span>Social links</span> 99 <label class="flex items-center gap-2 py-1"> 100 <input type="checkbox" bind:checked={bluesky} /> 101 Link to Bluesky account 102 </label> 103 {#each links as link, index (index)} 104 <div class="flex w-full items-center gap-2"> 105 <Link class="size-4 shrink-0" aria-hidden="true" /> 106 <input 107 type="text" 108 class={inputClass} 109 placeholder={`Social link ${index + 1}`} 110 bind:value={link.value} 111 /> 112 </div> 113 {/each} 114 </div> 115 116 {#if error} 117 <p class="typography-paragraph-small text-foreground-danger">{error}</p> 118 {/if} 119 120 <div class="flex items-center justify-between gap-2"> 121 <Button type="submit" variant="default" class="w-full gap-2" loading={busy}> 122 <Check class="size-4" aria-hidden="true" /> 123 Save 124 </Button> 125 <Button variant="default" class="w-full gap-2" disabled={busy} onclick={onCancel}> 126 <X class="size-4" aria-hidden="true" /> 127 Cancel 128 </Button> 129 </div> 130</form>