your personal website on atproto - mirror blento.app
at qr-codes 131 lines 3.7 kB view raw
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>