your personal website on atproto - mirror blento.app

custom favicon, og image

Florian 53c2b9fd 4ff93c7a

+154 -12
+151 -10
src/lib/cards/LinkCard/EditingLinkCard.svelte
··· 1 1 <script lang="ts"> 2 2 import { browser } from '$app/environment'; 3 - import { getImage } from '$lib/helper'; 3 + import { getImage, compressImage } from '$lib/helper'; 4 4 import { getDidContext, getIsMobile } from '$lib/website/context'; 5 5 import type { ContentComponentProps } from '../types'; 6 6 import PlainTextEditor from '$lib/components/PlainTextEditor.svelte'; 7 7 8 8 let { item = $bindable() }: ContentComponentProps = $props(); 9 9 10 + let faviconInputRef: HTMLInputElement; 11 + let imageInputRef: HTMLInputElement; 12 + let isHoveringFavicon = $state(false); 13 + let isHoveringImage = $state(false); 14 + 15 + async function handleFaviconChange(event: Event) { 16 + const target = event.target as HTMLInputElement; 17 + const file = target.files?.[0]; 18 + if (!file) return; 19 + 20 + try { 21 + const compressedBlob = await compressImage(file, 128); 22 + const objectUrl = URL.createObjectURL(compressedBlob); 23 + 24 + item.cardData.favicon = { 25 + blob: compressedBlob, 26 + objectUrl 27 + } as any; 28 + 29 + faviconHasError = false; 30 + } catch (error) { 31 + console.error('Failed to process image:', error); 32 + } 33 + } 34 + 35 + async function handleImageChange(event: Event) { 36 + const target = event.target as HTMLInputElement; 37 + const file = target.files?.[0]; 38 + if (!file) return; 39 + 40 + try { 41 + const compressedBlob = await compressImage(file); 42 + const objectUrl = URL.createObjectURL(compressedBlob); 43 + 44 + item.cardData.image = { 45 + blob: compressedBlob, 46 + objectUrl 47 + } as any; 48 + } catch (error) { 49 + console.error('Failed to process image:', error); 50 + } 51 + } 52 + 10 53 let isMobile = getIsMobile(); 11 54 12 55 let faviconHasError = $state(false); ··· 55 98 let did = getDidContext(); 56 99 </script> 57 100 101 + <input 102 + type="file" 103 + accept="image/*" 104 + class="hidden" 105 + bind:this={faviconInputRef} 106 + onchange={handleFaviconChange} 107 + /> 108 + <input 109 + type="file" 110 + accept="image/*" 111 + class="hidden" 112 + bind:this={imageInputRef} 113 + onchange={handleImageChange} 114 + /> 115 + 58 116 <div class="relative flex h-full flex-col justify-between p-4"> 59 117 <div 60 118 class={[ ··· 64 122 ></div> 65 123 66 124 <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" 125 + <button 126 + type="button" 127 + class="bg-base-100 border-base-300 accent:bg-accent-100/50 accent:border-accent-200 dark:border-base-800 dark:bg-base-900 hover:ring-accent-500 relative mb-2 inline-flex size-8 cursor-pointer items-center justify-center rounded-xl border transition-all duration-200 hover:ring-2" 128 + onclick={() => faviconInputRef?.click()} 129 + onmouseenter={() => (isHoveringFavicon = true)} 130 + onmouseleave={() => (isHoveringFavicon = false)} 69 131 > 70 132 {#if hasFetched && item.cardData.favicon && !faviconHasError} 71 133 <img ··· 90 152 /> 91 153 </svg> 92 154 {/if} 93 - </div> 155 + <!-- Hover overlay --> 156 + <div 157 + class={[ 158 + 'absolute inset-0 flex items-center justify-center rounded-xl bg-black/50 transition-opacity duration-200', 159 + isHoveringFavicon ? 'opacity-100' : 'opacity-0' 160 + ]} 161 + > 162 + <svg 163 + xmlns="http://www.w3.org/2000/svg" 164 + fill="none" 165 + viewBox="0 0 24 24" 166 + stroke-width="2" 167 + stroke="currentColor" 168 + class="size-4 text-white" 169 + > 170 + <path 171 + stroke-linecap="round" 172 + stroke-linejoin="round" 173 + d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Z" 174 + /> 175 + </svg> 176 + </div> 177 + </button> 94 178 95 179 <div 96 180 class={[ ··· 121 205 </div> 122 206 </div> 123 207 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 - /> 208 + {#if hasFetched && browser && ((isMobile() && item.mobileH >= 8) || (!isMobile() && item.h >= 4))} 209 + <button 210 + type="button" 211 + class="hover:ring-accent-500 relative mb-2 aspect-2/1 w-full cursor-pointer overflow-hidden rounded-xl transition-all duration-200 hover:ring-2" 212 + onclick={() => imageInputRef?.click()} 213 + onmouseenter={() => (isHoveringImage = true)} 214 + onmouseleave={() => (isHoveringImage = false)} 215 + > 216 + {#if item.cardData.image} 217 + <img 218 + class="h-full w-full object-cover opacity-100 transition-opacity duration-100 starting:opacity-0" 219 + src={getImage(item.cardData, did)} 220 + alt="" 221 + /> 222 + {:else} 223 + <div class="bg-base-200 dark:bg-base-800 flex h-full w-full items-center justify-center"> 224 + <svg 225 + xmlns="http://www.w3.org/2000/svg" 226 + fill="none" 227 + viewBox="0 0 24 24" 228 + stroke-width="1.5" 229 + stroke="currentColor" 230 + class="text-base-400 dark:text-base-600 size-8" 231 + > 232 + <path 233 + stroke-linecap="round" 234 + stroke-linejoin="round" 235 + d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" 236 + /> 237 + </svg> 238 + </div> 239 + {/if} 240 + <!-- Hover overlay --> 241 + <div 242 + class={[ 243 + 'absolute inset-0 flex items-center justify-center rounded-xl bg-black/50 transition-opacity duration-200', 244 + isHoveringImage ? 'opacity-100' : 'opacity-0' 245 + ]} 246 + > 247 + <div class="text-center text-sm text-white"> 248 + <svg 249 + xmlns="http://www.w3.org/2000/svg" 250 + fill="none" 251 + viewBox="0 0 24 24" 252 + stroke-width="1.5" 253 + stroke="currentColor" 254 + class="mx-auto mb-1 size-6" 255 + > 256 + <path 257 + stroke-linecap="round" 258 + stroke-linejoin="round" 259 + d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z" 260 + /> 261 + <path 262 + stroke-linecap="round" 263 + stroke-linejoin="round" 264 + d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0ZM18.75 10.5h.008v.008h-.008V10.5Z" 265 + /> 266 + </svg> 267 + <span class="font-medium">{item.cardData.image ? 'Change' : 'Add image'}</span> 268 + </div> 269 + </div> 270 + </button> 130 271 {/if} 131 272 </div>
+1 -1
src/lib/cards/LinkCard/LinkCard.svelte
··· 23 23 <img 24 24 class="size-6 rounded-lg object-cover" 25 25 onerror={() => (faviconHasError = true)} 26 - src={item.cardData.favicon} 26 + src={getImage(item.cardData, did, 'favicon')} 27 27 alt="" 28 28 /> 29 29 {:else}
+2 -1
src/lib/website/EditableProfile.svelte
··· 8 8 import type { Editor } from '@tiptap/core'; 9 9 import MadeWithBlento from './MadeWithBlento.svelte'; 10 10 11 - let { data = $bindable() }: { data: WebsiteData } = $props(); 11 + let { data = $bindable(), hideBlento = false }: { data: WebsiteData; hideBlento?: boolean } = 12 + $props(); 12 13 13 14 let profilePosition = $derived(getProfilePosition(data)); 14 15