your personal website on atproto - mirror blento.app
at main 124 lines 3.5 kB view raw
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>