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