your personal website on atproto - mirror
blento.app
1<script lang="ts">
2 import { browser } from '$app/environment';
3 import { getImage } from '$lib/helper';
4 import { getDidContext, getIsMobile } from '$lib/website/context';
5 import type { ContentComponentProps } from '../types';
6 import PlainTextEditor from '../utils/PlainTextEditor.svelte';
7
8 let { item = $bindable() }: ContentComponentProps = $props();
9
10 let isMobile = getIsMobile();
11
12 let faviconHasError = $state(false);
13 let isFetchingMetadata = $state(false);
14
15 let hasFetched = $derived(item.cardData.hasFetched !== false);
16
17 async function fetchMetadata() {
18 let domain: string;
19 try {
20 domain = new URL(item.cardData.href).hostname;
21 } catch {
22 return;
23 }
24 item.cardData.domain = domain;
25 faviconHasError = false;
26
27 try {
28 const response = await fetch('/api/links?link=' + encodeURIComponent(item.cardData.href));
29 if (!response.ok) {
30 throw new Error();
31 }
32 const data = await response.json();
33 item.cardData.description = data.description || '';
34 item.cardData.title = data.title || '';
35 item.cardData.image = data.images?.[0] || '';
36 item.cardData.favicon = data.favicons?.[0] || undefined;
37 } catch {
38 return;
39 }
40 }
41
42 $effect(() => {
43 if (hasFetched !== false || isFetchingMetadata) {
44 return;
45 }
46
47 isFetchingMetadata = true;
48
49 fetchMetadata().then(() => {
50 item.cardData.hasFetched = true;
51 isFetchingMetadata = false;
52 });
53 });
54
55 let did = getDidContext();
56</script>
57
58<div class="relative flex h-full flex-col justify-between p-4">
59 <div
60 class={[
61 'accent:bg-accent-500/50 absolute inset-0 z-20 bg-white/50 dark:bg-black/50',
62 !hasFetched ? 'animate-pulse' : 'hidden'
63 ]}
64 ></div>
65
66 <div class={isFetchingMetadata ? 'pointer-events-none' : ''}>
67 <div
68 class="bg-base-100 border-base-300 accent:bg-accent-100/50 accent:border-accent-200 dark:border-base-800 dark:bg-base-900 mb-2 inline-flex size-8 items-center justify-center rounded-xl border"
69 >
70 {#if hasFetched && item.cardData.favicon && !faviconHasError}
71 <img
72 class="size-6 rounded-lg object-cover"
73 onerror={() => (faviconHasError = true)}
74 src={getImage(item.cardData, did, 'favicon')}
75 alt=""
76 />
77 {:else}
78 <svg
79 xmlns="http://www.w3.org/2000/svg"
80 fill="none"
81 viewBox="0 0 24 24"
82 stroke-width="1.5"
83 stroke="currentColor"
84 class="size-4"
85 >
86 <path
87 stroke-linecap="round"
88 stroke-linejoin="round"
89 d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 1 1.242 7.244"
90 />
91 </svg>
92 {/if}
93 </div>
94
95 <div
96 class={[
97 '-m-1 rounded-md p-1 transition-colors duration-200',
98 hasFetched
99 ? 'hover:bg-base-200/70 dark:hover:bg-base-800/70 accent:hover:bg-accent-200/30'
100 : ''
101 ]}
102 >
103 {#if hasFetched}
104 <PlainTextEditor
105 class="text-base-900 dark:text-base-50 line-clamp-2 text-lg font-bold"
106 key="title"
107 bind:item
108 placeholder="Title here"
109 />
110 {:else}
111 <span class="text-base-900 dark:text-base-50 line-clamp-2 text-lg font-bold">
112 Loading data...
113 </span>
114 {/if}
115 </div>
116 <!-- <div class="text-base-800 dark:text-base-100 mt-2 text-xs">{item.cardData.description}</div> -->
117 <div
118 class="text-accent-600 accent:text-accent-950 dark:text-accent-400 mt-2 text-xs font-semibold"
119 >
120 {item.cardData.domain}
121 </div>
122 </div>
123
124 {#if hasFetched && browser && ((isMobile() && item.mobileH >= 8) || (!isMobile() && item.h >= 4)) && item.cardData.image}
125 <img
126 class="mb-2 aspect-2/1 w-full rounded-xl object-cover opacity-100 transition-opacity duration-100 starting:opacity-0"
127 src={getImage(item.cardData, did)}
128 alt=""
129 />
130 {/if}
131</div>