your personal website on atproto - mirror
blento.app
1<script lang="ts">
2 import { onMount } from 'svelte';
3 import type { ContentComponentProps } from '../types';
4 import { getAdditionalUserData, getCanEdit, getIsMobile } from '$lib/website/context';
5 import { getBlentoOrBskyProfile } from '$lib/atproto/methods';
6 import type { FriendsProfile } from '.';
7 import type { Did } from '@atcute/lexicons';
8 import { Avatar } from '@foxui/core';
9
10 let { item }: ContentComponentProps = $props();
11
12 const isMobile = getIsMobile();
13 const canEdit = getCanEdit();
14 const additionalData = getAdditionalUserData();
15
16 let dids: string[] = $derived(item.cardData.friends ?? []);
17
18 let serverProfiles: FriendsProfile[] = $derived(
19 (additionalData[item.cardType] as FriendsProfile[]) ?? []
20 );
21
22 let clientProfiles: FriendsProfile[] = $state([]);
23
24 let profiles = $derived.by(() => {
25 if (serverProfiles.length > 0) {
26 return dids
27 .map((did) => serverProfiles.find((p) => p.did === did))
28 .filter((p): p is FriendsProfile => !!p);
29 }
30 return dids
31 .map((did) => clientProfiles.find((p) => p.did === did))
32 .filter((p): p is FriendsProfile => !!p);
33 });
34
35 onMount(() => {
36 if (serverProfiles.length === 0 && dids.length > 0) {
37 loadProfiles();
38 }
39 });
40
41 async function loadProfiles() {
42 const results = await Promise.all(
43 dids.map((did) => getBlentoOrBskyProfile({ did: did as Did }).catch(() => undefined))
44 );
45 clientProfiles = results.filter(
46 (p): p is FriendsProfile => !!p && p.handle !== 'handle.invalid'
47 );
48 }
49
50 // Reload when dids change in editing mode
51 $effect(() => {
52 if (canEdit() && dids.length > 0) {
53 loadProfiles();
54 }
55 });
56
57 let sizeClass = $derived.by(() => {
58 const w = isMobile() ? item.mobileW / 2 : item.w;
59 if (w < 3) return 'sm';
60 if (w < 5) return 'md';
61 return 'lg';
62 });
63
64 function getLink(profile: FriendsProfile): string {
65 if (profile.hasBlento && profile.handle && profile.handle !== 'handle.invalid') {
66 return `/${profile.handle}`;
67 }
68 if (profile.handle && profile.handle !== 'handle.invalid') {
69 return `https://bsky.app/profile/${profile.handle}`;
70 }
71 return `https://bsky.app/profile/${profile.did}`;
72 }
73</script>
74
75<div class="flex h-full w-full items-center justify-center overflow-hidden px-2">
76 {#if dids.length === 0}
77 {#if canEdit()}
78 <span class="text-base-400 dark:text-base-500 accent:text-accent-300 text-sm">
79 Add friends in settings
80 </span>
81 {/if}
82 {:else}
83 {@const olX = sizeClass === 'sm' ? 12 : sizeClass === 'md' ? 20 : 24}
84 {@const olY = sizeClass === 'sm' ? 8 : sizeClass === 'md' ? 12 : 16}
85 <div class="">
86 <div
87 class="flex flex-wrap items-center justify-center"
88 style="padding: {olY}px 0 0 {olX}px;"
89 >
90 {#each profiles as profile (profile.did)}
91 <a
92 href={getLink(profile)}
93 class="accent:ring-accent-500 relative block rounded-full ring-2 ring-white transition-transform hover:scale-110 dark:ring-neutral-900"
94 style="margin: -{olY}px 0 0 -{olX}px;"
95 >
96 <Avatar
97 src={profile.avatar}
98 alt={profile.handle}
99 class={sizeClass === 'sm' ? 'size-12' : sizeClass === 'md' ? 'size-16' : 'size-20'}
100 />
101 </a>
102 {/each}
103 </div>
104 </div>
105 {/if}
106</div>