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 } 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 canEdit = getCanEdit();
13 const additionalData = getAdditionalUserData();
14
15 let dids: string[] = $derived(item.cardData.friends ?? []);
16
17 let serverProfiles: FriendsProfile[] = $derived(
18 (additionalData[item.cardType] as FriendsProfile[]) ?? []
19 );
20
21 let clientProfiles: FriendsProfile[] = $state([]);
22
23 let profiles = $derived.by(() => {
24 if (serverProfiles.length > 0) {
25 return dids
26 .map((did) => serverProfiles.find((p) => p.did === did))
27 .filter((p): p is FriendsProfile => !!p);
28 }
29 return dids
30 .map((did) => clientProfiles.find((p) => p.did === did))
31 .filter((p): p is FriendsProfile => !!p);
32 });
33
34 onMount(() => {
35 if (serverProfiles.length === 0 && dids.length > 0) {
36 loadProfiles();
37 }
38 });
39
40 async function loadProfiles() {
41 const results = await Promise.all(
42 dids.map((did) => getBlentoOrBskyProfile({ did: did as Did }).catch(() => undefined))
43 );
44 clientProfiles = results.filter(
45 (p): p is FriendsProfile => !!p && p.handle !== 'handle.invalid'
46 );
47 }
48
49 // Reload when dids change in editing mode
50 $effect(() => {
51 if (canEdit() && dids.length > 0) {
52 loadProfiles();
53 }
54 });
55
56 function removeFriend(did: string) {
57 item.cardData.friends = item.cardData.friends.filter((d: string) => d !== did);
58 }
59
60 function getLink(profile: FriendsProfile): string {
61 if (profile.hasBlento && profile.url) {
62 return profile.url;
63 }
64 if (profile.handle && profile.handle !== 'handle.invalid') {
65 return `https://bsky.app/profile/${profile.handle}`;
66 }
67 return `https://bsky.app/profile/${profile.did}`;
68 }
69</script>
70
71<div class="flex h-full w-full items-center justify-center overflow-hidden px-2">
72 {#if dids.length === 0}
73 {#if canEdit()}
74 <span class="text-base-400 dark:text-base-500 accent:text-accent-300 text-sm">
75 Add friends in settings
76 </span>
77 {/if}
78 {:else}
79 <div class="friends-card">
80 <div class="friends-grid flex flex-wrap items-center justify-center">
81 {#each profiles as profile (profile.did)}
82 <div class="friends-avatar group relative">
83 <a
84 href={getLink(profile)}
85 class="accent:ring-accent-500 relative block rounded-full ring-2 ring-white transition-transform hover:scale-110 dark:ring-neutral-900"
86 >
87 <Avatar src={profile.avatar} alt={profile.handle} class="friends-avatar-image" />
88 </a>
89 {#if canEdit()}
90 <button
91 aria-label="Remove friend"
92 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"
93 onclick={(e) => {
94 e.preventDefault();
95 e.stopPropagation();
96 removeFriend(profile.did);
97 }}
98 >
99 <svg
100 xmlns="http://www.w3.org/2000/svg"
101 fill="none"
102 viewBox="0 0 24 24"
103 stroke-width="2.5"
104 stroke="currentColor"
105 class="size-4"
106 >
107 <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
108 </svg>
109 </button>
110 {/if}
111 </div>
112 {/each}
113 </div>
114 </div>
115 {/if}
116</div>
117
118<style>
119 .friends-card {
120 --friends-overlap-x: 12px;
121 --friends-overlap-y: 8px;
122 --friends-avatar-size: 48px;
123 }
124
125 .friends-grid {
126 padding: var(--friends-overlap-y) 0 0 var(--friends-overlap-x);
127 }
128
129 .friends-avatar {
130 margin: calc(var(--friends-overlap-y) * -1) 0 0 calc(var(--friends-overlap-x) * -1);
131 }
132
133 :global(.friends-avatar-image) {
134 width: var(--friends-avatar-size);
135 height: var(--friends-avatar-size);
136 }
137
138 @container card (width >= 18rem) {
139 .friends-card {
140 --friends-overlap-x: 20px;
141 --friends-overlap-y: 12px;
142 --friends-avatar-size: 64px;
143 }
144 }
145
146 @container card (width >= 26rem) {
147 .friends-card {
148 --friends-overlap-x: 24px;
149 --friends-overlap-y: 16px;
150 --friends-avatar-size: 80px;
151 }
152 }
153</style>