Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
Svelte
at icy/tmrrpn
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195<script lang="ts"> import FollowButton from "./FollowButton.svelte"; import VouchButton from "./VouchButton.svelte"; import NetworkVouches from "./NetworkVouches.svelte"; import ProfileEditForm from "./ProfileEditForm.svelte"; import FollowerFollowing from "./FollowerFollowing.svelte"; import MapPin from "$icon/map-pin"; import Link from "$icon/link"; import Pencil from "$icon/pencil"; import Rss from "$icon/rss"; import ReportItems from "$lib/components/moderation/ReportItems.svelte"; import Avatar from "$lib/components/ui/Avatar.svelte"; import Button from "$lib/components/ui/Button.svelte"; import DropdownItem from "$lib/components/ui/DropdownItem.svelte"; import MoreMenu from "$lib/components/ui/MoreMenu.svelte"; import { getAuth } from "$lib/auth.svelte"; import type { ProfileRecord } from "$lib/api/records"; import type { DirectVouch, NetworkVouch } from "./types";
interface Identity { did: string; handle: string; }
interface Props { identity: Identity; profile: ProfileRecord | null; followers: number; following: number; viewerFollowRkey?: string | null; viewerVouch?: DirectVouch | null; networkVouches?: NetworkVouch[]; }
let { identity, profile, followers, following, viewerFollowRkey, viewerVouch = null, networkVouches = [] }: Props = $props();
const auth = getAuth(); const isSelf = $derived(auth.currentDid === identity.did); const signedIn = $derived(Boolean(auth.currentDid));
let editing = $state(false); let saved = $state<ProfileRecord | null>(null); const shown = $derived(saved ?? profile);
const links = $derived((shown?.links ?? []).filter((l) => l.trim().length > 0)); const blueskyUrl = $derived(shown?.bluesky ? `https://bsky.app/profile/${identity.did}` : null);</script>
<div class="grid grid-cols-3 items-center gap-1 md:grid-cols-1"> <div class="col-span-1 flex items-center justify-center"> <div class="relative aspect-square w-3/4"> <Avatar did={identity.did} handle={identity.handle} size="full" class="absolute inset-0" /> </div> </div>
<div class="col-span-2"> <div class="flex flex-row flex-nowrap items-center gap-2"> <h1 title={identity.handle} class="min-w-0 truncate typography-heading-4 font-bold"> {identity.handle} </h1> {#if shown?.pronouns} <p class="shrink-0 whitespace-nowrap text-foreground-muted">{shown.pronouns}</p> {/if} </div> <div class="md:hidden"> <FollowerFollowing handle={identity.handle} {followers} {following} /> </div> </div>
<div class="col-span-3 md:col-span-full"> {#if editing} <ProfileEditForm profile={shown} onSaved={(record) => { saved = record; editing = false; }} onCancel={() => (editing = false)} /> {:else} <div class="space-y-2 typography-paragraph-small"> {#if shown?.description} <p class="pb-4 typography-paragraph-large md:pb-2">{shown.description}</p> {/if}
<div class="hidden md:block"> <FollowerFollowing handle={identity.handle} {followers} {following} /> </div>
{#if shown?.location || links.length > 0 || blueskyUrl} <div class="mb-2 flex max-w-full flex-col gap-2 truncate"> {#if shown?.location} <div class="flex items-center gap-2"> <span class="shrink-0" ><MapPin class="size-4" aria-hidden="true" /></span > <span>{shown.location}</span> </div> {/if} {#if blueskyUrl} <div class="flex items-center gap-2"> <span class="shrink-0" ><Link class="size-4" aria-hidden="true" /></span > <a href={blueskyUrl} target="_blank" rel="external noopener noreferrer" class="truncate no-underline hover:underline" >{identity.handle}</a > </div> {/if} {#each links as link, index (index)} <div class="flex items-center gap-2"> <span class="shrink-0" ><Link class="size-4" aria-hidden="true" /></span > <a href={link} target="_blank" rel="external nofollow me noopener noreferrer" class="truncate no-underline hover:underline">{link}</a > </div> {/each} </div> {/if}
<div class="my-2 flex items-center gap-2"> {#if isSelf} <Button variant="default" class="w-full gap-2" onclick={() => (editing = true)} > <Pencil class="size-3" aria-hidden="true" /> Edit </Button> {:else} <div class="w-full"> <FollowButton profileDid={identity.did} initialRkey={viewerFollowRkey} /> </div> {/if} <MoreMenu label={`More actions for @${identity.handle}`} buttonVariant="default" insetShadow={true} > <DropdownItem icon={Rss} href={`/${identity.handle}/feed.atom`}> Atom feed </DropdownItem> <ReportItems authorDid={identity.did} authorHandle={identity.handle} /> </MoreMenu> </div>
{#if (!isSelf && signedIn) || networkVouches.length > 0} <div class="my-2 flex flex-col gap-2"> {#if !isSelf} <VouchButton profileDid={identity.did} profileLabel={identity.handle} initialVouch={viewerVouch} {networkVouches} /> {/if} {#if networkVouches.length > 0} <NetworkVouches subjectHandle={identity.handle} vouches={networkVouches} /> {/if} </div> {/if} </div> {/if} </div></div>