This repository has no description
1<script lang="ts">
2 import FollowButton from "./FollowButton.svelte";
3 import ProfileEditForm from "./ProfileEditForm.svelte";
4 import FollowerFollowing from "./FollowerFollowing.svelte";
5 import MapPin from "$icon/map-pin";
6 import Link from "$icon/link";
7 import Pencil from "$icon/pencil";
8 import Rss from "$icon/rss";
9 import Avatar from "$lib/components/ui/Avatar.svelte";
10 import Button from "$lib/components/ui/Button.svelte";
11 import { getAuth } from "$lib/auth.svelte";
12 import type { ProfileRecord } from "$lib/api/records";
13
14 interface Identity {
15 did: string;
16 handle: string;
17 }
18
19 interface Props {
20 identity: Identity;
21 profile: ProfileRecord | null;
22 followers: number;
23 following: number;
24 viewerFollowRkey?: string | null;
25 }
26
27 let { identity, profile, followers, following, viewerFollowRkey }: Props = $props();
28
29 const auth = getAuth();
30 const isSelf = $derived(auth.currentDid === identity.did);
31
32 let editing = $state(false);
33 let saved = $state<ProfileRecord | null>(null);
34 const shown = $derived(saved ?? profile);
35
36 const links = $derived((shown?.links ?? []).filter((l) => l.trim().length > 0));
37 const blueskyUrl = $derived(shown?.bluesky ? `https://bsky.app/profile/${identity.did}` : null);
38</script>
39
40<div class="grid grid-cols-3 items-center gap-1 md:grid-cols-1">
41 <div class="col-span-1 flex items-center justify-center">
42 <div class="relative aspect-square w-3/4">
43 <Avatar
44 did={identity.did}
45 handle={identity.handle}
46 size="h-full w-full"
47 class="absolute inset-0 p-2"
48 />
49 </div>
50 </div>
51
52 <div class="col-span-2">
53 <div class="flex flex-row flex-nowrap items-center gap-2">
54 <h1 title={identity.handle} class="truncate typography-heading-4 font-bold">
55 {identity.handle}
56 </h1>
57 {#if shown?.pronouns}
58 <p class="text-foreground-subtle">{shown.pronouns}</p>
59 {/if}
60 </div>
61 <div class="md:hidden">
62 <FollowerFollowing handle={identity.handle} {followers} {following} />
63 </div>
64 </div>
65
66 <div class="col-span-3 md:col-span-full">
67 {#if editing}
68 <ProfileEditForm
69 profile={shown}
70 onSaved={(record) => {
71 saved = record;
72 editing = false;
73 }}
74 onCancel={() => (editing = false)}
75 />
76 {:else}
77 <div class="space-y-2 text-sm">
78 {#if shown?.description}
79 <p class="pb-4 typography-paragraph-large md:pb-2">{shown.description}</p>
80 {/if}
81
82 <div class="hidden md:block">
83 <FollowerFollowing handle={identity.handle} {followers} {following} />
84 </div>
85
86 {#if shown?.location || links.length > 0 || blueskyUrl}
87 <div class="mb-2 flex max-w-full flex-col gap-2 truncate">
88 {#if shown?.location}
89 <div class="flex items-center gap-2">
90 <span class="shrink-0"><MapPin class="size-4" aria-hidden="true" /></span>
91 <span>{shown.location}</span>
92 </div>
93 {/if}
94 {#if blueskyUrl}
95 <div class="flex items-center gap-2">
96 <span class="shrink-0"><Link class="size-4" aria-hidden="true" /></span>
97 <a
98 href={blueskyUrl}
99 target="_blank"
100 rel="external noopener noreferrer"
101 class="truncate no-underline hover:underline">{identity.handle}</a
102 >
103 </div>
104 {/if}
105 {#each links as link, index (index)}
106 <div class="flex items-center gap-2">
107 <span class="shrink-0"><Link class="size-4" aria-hidden="true" /></span>
108 <a
109 href={link}
110 target="_blank"
111 rel="external nofollow me noopener noreferrer"
112 class="truncate no-underline hover:underline">{link}</a
113 >
114 </div>
115 {/each}
116 </div>
117 {/if}
118
119 <div class="my-2 flex items-center gap-2">
120 {#if isSelf}
121 <Button variant="default" class="w-full gap-2" onclick={() => (editing = true)}>
122 <Pencil class="size-3" aria-hidden="true" />
123 Edit
124 </Button>
125 {:else}
126 <div class="w-full">
127 <FollowButton profileDid={identity.did} initialRkey={viewerFollowRkey} />
128 </div>
129 {/if}
130 <Button href={`/${identity.handle}/feed.atom`} variant="default" aria-label="RSS feed">
131 <Rss class="size-4" aria-hidden="true" />
132 </Button>
133 </div>
134 </div>
135 {/if}
136 </div>
137</div>