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 removeFriend(did: string) {
65 item.cardData.friends = item.cardData.friends.filter((d: string) => d !== did);
66 }
67
68 function getLink(profile: FriendsProfile): string {
69 if (profile.hasBlento && profile.handle && profile.handle !== 'handle.invalid') {
70 return `/${profile.handle}`;
71 }
72 if (profile.handle && profile.handle !== 'handle.invalid') {
73 return `https://bsky.app/profile/${profile.handle}`;
74 }
75 return `https://bsky.app/profile/${profile.did}`;
76 }
77</script>
78
79<div class="flex h-full w-full items-center justify-center overflow-hidden px-2">
80 {#if dids.length === 0}
81 {#if canEdit()}
82 <span class="text-base-400 dark:text-base-500 accent:text-accent-300 text-sm">
83 Add friends in settings
84 </span>
85 {/if}
86 {:else}
87 {@const olX = sizeClass === 'sm' ? 12 : sizeClass === 'md' ? 20 : 24}
88 {@const olY = sizeClass === 'sm' ? 8 : sizeClass === 'md' ? 12 : 16}
89 <div class="">
90 <div class="flex flex-wrap items-center justify-center" style="padding: {olY}px 0 0 {olX}px;">
91 {#each profiles as profile (profile.did)}
92 <div class="group relative" style="margin: -{olY}px 0 0 -{olX}px;">
93 <a
94 href={getLink(profile)}
95 class="accent:ring-accent-500 relative block rounded-full ring-2 ring-white transition-transform hover:scale-110 dark:ring-neutral-900"
96 >
97 <Avatar
98 src={profile.avatar}
99 alt={profile.handle}
100 class={sizeClass === 'sm' ? 'size-12' : sizeClass === 'md' ? 'size-16' : 'size-20'}
101 />
102 </a>
103 {#if canEdit()}
104 <button
105 aria-label="Remove friend"
106 class="absolute inset-0 flex cursor-pointer items-center justify-center rounded-full bg-black/50 text-white opacity-0 transition-opacity group-hover:opacity-100"
107 onclick={(e) => {
108 e.preventDefault();
109 e.stopPropagation();
110 removeFriend(profile.did);
111 }}
112 >
113 <svg
114 xmlns="http://www.w3.org/2000/svg"
115 fill="none"
116 viewBox="0 0 24 24"
117 stroke-width="2.5"
118 stroke="currentColor"
119 class="size-4"
120 >
121 <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
122 </svg>
123 </button>
124 {/if}
125 </div>
126 {/each}
127 </div>
128 </div>
129 {/if}
130</div>