Monorepo for Tangled tangled.org
1

Configure Feed

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

core / web / src / lib / components / profile / ProfileCard.svelte
4.6 kB 157 lines
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 UserRound from "$icon/user-round"; 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 avatar?: string; 18 } 19 20 interface Props { 21 identity: Identity; 22 profile: ProfileRecord | null; 23 followers: number; 24 following: number; 25 viewerFollowRkey?: string | null; 26 } 27 28 let { identity, profile, followers, following, viewerFollowRkey }: Props = $props(); 29 30 const auth = getAuth(); 31 const isSelf = $derived(auth.currentDid === identity.did); 32 33 let editing = $state(false); 34 let saved = $state<ProfileRecord | null>(null); 35 const shown = $derived(saved ?? profile); 36 37 const links = $derived((shown?.links ?? []).filter((l) => l.trim().length > 0)); 38 const blueskyUrl = $derived(shown?.bluesky ? `https://bsky.app/profile/${identity.did}` : null); 39</script> 40 41<div class="grid grid-cols-3 items-center gap-1 md:grid-cols-1"> 42 <div class="col-span-1 flex items-center justify-center"> 43 <div class="relative aspect-square w-3/4"> 44 {#if identity.avatar} 45 <img 46 class="absolute inset-0 h-full w-full rounded-full object-cover p-2" 47 src={identity.avatar} 48 alt={identity.handle} 49 /> 50 {:else} 51 <span class="absolute inset-0 h-full w-full p-2" aria-hidden="true"> 52 <span 53 class="flex h-full w-full items-center justify-center rounded-full bg-background-inset text-foreground-placeholder" 54 > 55 <UserRound class="size-1/2" /> 56 </span> 57 </span> 58 {/if} 59 </div> 60 </div> 61 62 <div class="col-span-2"> 63 <div class="flex flex-row flex-nowrap items-center gap-2"> 64 <h1 65 title={identity.handle} 66 class="overflow-hidden text-lg font-bold text-ellipsis whitespace-nowrap" 67 > 68 {identity.handle} 69 </h1> 70 {#if shown?.pronouns} 71 <p class="text-foreground-subtle">{shown.pronouns}</p> 72 {/if} 73 </div> 74 <div class="md:hidden"> 75 <FollowerFollowing handle={identity.handle} {followers} {following} /> 76 </div> 77 </div> 78 79 <div class="col-span-3 md:col-span-full"> 80 {#if editing} 81 <ProfileEditForm 82 profile={shown} 83 onSaved={(record) => { 84 saved = record; 85 editing = false; 86 }} 87 onCancel={() => (editing = false)} 88 /> 89 {:else} 90 <div class="space-y-2 text-sm"> 91 {#if shown?.description} 92 <p class="pb-4 text-base md:pb-2">{shown.description}</p> 93 {/if} 94 95 <div class="hidden md:block"> 96 <FollowerFollowing handle={identity.handle} {followers} {following} /> 97 </div> 98 99 {#if shown?.location || links.length > 0 || blueskyUrl} 100 <div 101 class="mb-2 flex max-w-full flex-col gap-2 overflow-hidden text-ellipsis whitespace-nowrap" 102 > 103 {#if shown?.location} 104 <div class="flex items-center gap-2"> 105 <span class="flex-shrink-0"><MapPin class="size-4" aria-hidden="true" /></span> 106 <span>{shown.location}</span> 107 </div> 108 {/if} 109 {#if blueskyUrl} 110 <div class="flex items-center gap-2"> 111 <span class="flex-shrink-0"><Link class="size-4" aria-hidden="true" /></span> 112 <a 113 href={blueskyUrl} 114 target="_blank" 115 rel="external noopener noreferrer" 116 class="truncate no-underline hover:underline">{identity.handle}</a 117 > 118 </div> 119 {/if} 120 {#each links as link, index (index)} 121 <div class="flex items-center gap-2"> 122 <span class="flex-shrink-0"><Link class="size-4" aria-hidden="true" /></span> 123 <a 124 href={link} 125 target="_blank" 126 rel="external nofollow me noopener noreferrer" 127 class="truncate no-underline hover:underline">{link}</a 128 > 129 </div> 130 {/each} 131 </div> 132 {/if} 133 134 <div class="my-2 flex items-center gap-2"> 135 {#if isSelf} 136 <Button variant="default" class="w-full gap-2" onclick={() => (editing = true)}> 137 <Pencil class="size-4" aria-hidden="true" /> 138 Edit 139 </Button> 140 {:else} 141 <div class="w-full"> 142 <FollowButton profileDid={identity.did} initialRkey={viewerFollowRkey} /> 143 </div> 144 {/if} 145 <Button 146 href={`/${identity.handle}/feed.atom`} 147 variant="default" 148 class="px-3" 149 aria-label="RSS feed" 150 > 151 <Rss class="size-4" aria-hidden="true" /> 152 </Button> 153 </div> 154 </div> 155 {/if} 156 </div> 157</div>