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