your personal website on atproto - mirror blento.app

v0.1

+50 -90
+4 -4
src/lib/cards/BaseCard/BaseEditingCard.svelte
··· 57 58 const cardDef = $derived(CardDefinitionsByType[item.cardType]); 59 60 - const minW = $derived(cardDef?.minW ?? (isMobile() ? 2 : 2)); 61 - const minH = $derived(cardDef?.minH ?? (isMobile() ? 2 : 2)); 62 63 - const maxW = $derived(cardDef?.maxW ?? COLUMNS); 64 - const maxH = $derived(cardDef?.maxH ?? (isMobile() ? 12 : 6)); 65 66 // Resize handle state 67 let isResizing = $state(false);
··· 57 58 const cardDef = $derived(CardDefinitionsByType[item.cardType]); 59 60 + const minW = $derived(cardDef.minW ?? (isMobile() ? 2 : 2)); 61 + const minH = $derived(cardDef.minH ?? (isMobile() ? 2 : 2)); 62 63 + const maxW = $derived(cardDef.maxW ?? COLUMNS); 64 + const maxH = $derived(cardDef.maxH ?? (isMobile() ? 12 : 6)); 65 66 // Resize handle state 67 let isResizing = $state(false);
+24 -38
src/lib/components/qr/QRCodeDisplay.svelte
··· 1 <script lang="ts"> 2 - import { browser } from '$app/environment'; 3 4 - let { url, size = 280, logo }: { url: string; size?: number; logo?: string } = $props(); 5 6 let container: HTMLDivElement | undefined = $state(); 7 8 - $effect(() => { 9 - if (!browser || !container) return; 10 - 11 - const render = async () => { 12 - const QRCodeStylingModule = await import('qr-code-styling'); 13 - const QRCodeStyling = QRCodeStylingModule.default; 14 - 15 - container!.innerHTML = ''; 16 17 - const options: ConstructorParameters<typeof QRCodeStyling>[0] = { 18 - width: size, 19 - height: size, 20 - data: url, 21 - dotsOptions: { 22 - color: '#000', 23 - type: 'rounded' 24 - }, 25 - backgroundOptions: { 26 - color: '#fff' 27 - }, 28 - cornersSquareOptions: { 29 - type: 'extra-rounded' 30 - }, 31 - cornersDotOptions: { 32 - type: 'dot' 33 - } 34 - }; 35 36 - if (logo) { 37 - options.image = logo; 38 - options.imageOptions = { 39 - crossOrigin: 'anonymous', 40 - margin: 4 41 - }; 42 } 43 - 44 - const qrCode = new QRCodeStyling(options); 45 - qrCode.append(container!); 46 }; 47 48 - render(); 49 }); 50 </script> 51
··· 1 <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 4 + let { url, size = 280 }: { url: string; size?: number } = $props(); 5 6 let container: HTMLDivElement | undefined = $state(); 7 8 + onMount(async () => { 9 + if (!container) return; 10 11 + const module = await import('qr-code-styling'); 12 + const QRCodeStyling = module.default; 13 14 + const options: ConstructorParameters<typeof QRCodeStyling>[0] = { 15 + width: size, 16 + height: size, 17 + data: url, 18 + dotsOptions: { 19 + color: '#000', 20 + type: 'rounded' 21 + }, 22 + backgroundOptions: { 23 + color: '#fff' 24 + }, 25 + cornersSquareOptions: { 26 + type: 'extra-rounded' 27 + }, 28 + cornersDotOptions: { 29 + type: 'dot' 30 } 31 }; 32 33 + const qrCode = new QRCodeStyling(options); 34 + qrCode.append(container); 35 }); 36 </script> 37
+1 -3
src/lib/components/qr/QRCodeModal.svelte
··· 28 toast.error('Failed to copy'); 29 } 30 } 31 - 32 - const logoUrl = $derived(context.avatar || context.favicon); 33 </script> 34 35 <Modal bind:open closeButton={true} class="max-w-sm"> ··· 54 {/if} 55 56 <div class="overflow-hidden rounded-2xl"> 57 - <QRCodeDisplay url={href} size={280} logo={logoUrl} /> 58 </div> 59 60 <div class="flex w-full items-center gap-2">
··· 28 toast.error('Failed to copy'); 29 } 30 } 31 </script> 32 33 <Modal bind:open closeButton={true} class="max-w-sm"> ··· 52 {/if} 53 54 <div class="overflow-hidden rounded-2xl"> 55 + <QRCodeDisplay url={href} size={280} /> 56 </div> 57 58 <div class="flex w-full items-center gap-2">
+9 -5
src/lib/components/qr/qrOverlay.svelte.ts
··· 12 } 13 14 export function qrOverlay( 15 - node: HTMLAnchorElement, 16 - params: { context?: QRContext; disabled?: boolean } = {} 17 ) { 18 const LONG_PRESS_DURATION = 500; 19 let longPressTimer: ReturnType<typeof setTimeout> | null = null; 20 let isLongPress = false; 21 22 function startLongPress() { 23 if (params.disabled) return; 24 isLongPress = false; 25 longPressTimer = setTimeout(() => { 26 isLongPress = true; 27 - openModal?.(node.href, params.context ?? {}); 28 }, LONG_PRESS_DURATION); 29 } 30 ··· 45 function handleContextMenu(e: MouseEvent) { 46 if (params.disabled) return; 47 e.preventDefault(); 48 - openModal?.(node.href, params.context ?? {}); 49 } 50 51 node.addEventListener('pointerdown', startLongPress); ··· 56 node.addEventListener('contextmenu', handleContextMenu); 57 58 return { 59 - update(newParams: { context?: QRContext; disabled?: boolean }) { 60 params = newParams; 61 }, 62 destroy() {
··· 12 } 13 14 export function qrOverlay( 15 + node: HTMLElement, 16 + params: { href?: string; context?: QRContext; disabled?: boolean } = {} 17 ) { 18 const LONG_PRESS_DURATION = 500; 19 let longPressTimer: ReturnType<typeof setTimeout> | null = null; 20 let isLongPress = false; 21 22 + function getHref() { 23 + return params.href || (node as HTMLAnchorElement).href || ''; 24 + } 25 + 26 function startLongPress() { 27 if (params.disabled) return; 28 isLongPress = false; 29 longPressTimer = setTimeout(() => { 30 isLongPress = true; 31 + openModal?.(getHref(), params.context ?? {}); 32 }, LONG_PRESS_DURATION); 33 } 34 ··· 49 function handleContextMenu(e: MouseEvent) { 50 if (params.disabled) return; 51 e.preventDefault(); 52 + openModal?.(getHref(), params.context ?? {}); 53 } 54 55 node.addEventListener('pointerdown', startLongPress); ··· 60 node.addEventListener('contextmenu', handleContextMenu); 61 62 return { 63 + update(newParams: { href?: string; context?: QRContext; disabled?: boolean }) { 64 params = newParams; 65 }, 66 destroy() {
+12 -40
src/lib/website/Profile.svelte
··· 8 import { getDescription, getName } from '$lib/helper'; 9 import { page } from '$app/state'; 10 import type { ActorIdentifier } from '@atcute/lexicons'; 11 - import QRCodeModal from '$lib/components/qr/QRCodeModal.svelte'; 12 13 let { 14 data, ··· 22 renderer.link = ({ href, title, text }) => 23 `<a target="_blank" href="${href}" title="${title}">${text}</a>`; 24 25 - let qrOpen = $state(false); 26 - let longPressTimer: ReturnType<typeof setTimeout> | null = null; 27 - 28 - const profileUrl = $derived(`${page.url}/${data.handle}`); 29 - 30 - function startLongPress() { 31 - longPressTimer = setTimeout(() => { 32 - qrOpen = true; 33 - }, 500); 34 - } 35 - 36 - function cancelLongPress() { 37 - if (longPressTimer) { 38 - clearTimeout(longPressTimer); 39 - longPressTimer = null; 40 - } 41 - } 42 - 43 - function handleContextMenu(e: MouseEvent) { 44 - e.preventDefault(); 45 - qrOpen = true; 46 - } 47 </script> 48 49 <!-- lg:fixed lg:h-screen lg:w-1/4 lg:max-w-none lg:px-12 lg:pt-24 xl:w-1/3 --> ··· 51 class="mx-auto flex max-w-lg flex-col justify-between px-8 @5xl/wrapper:fixed @5xl/wrapper:h-screen @5xl/wrapper:w-1/4 @5xl/wrapper:max-w-none @5xl/wrapper:px-12" 52 > 53 <div class="flex flex-col gap-4 pt-16 pb-8 @5xl/wrapper:h-screen @5xl/wrapper:pt-24"> 54 - <!-- svelte-ignore a11y_no_static_element_interactions --> 55 - <div 56 - class="w-fit cursor-pointer" 57 - onpointerdown={startLongPress} 58 - onpointerup={cancelLongPress} 59 - onpointercancel={cancelLongPress} 60 - onpointerleave={cancelLongPress} 61 - oncontextmenu={handleContextMenu} 62 > 63 {#if data.profile.avatar} 64 <img ··· 69 {:else} 70 <div class="bg-base-300 dark:bg-base-700 size-32 rounded-full @5xl/wrapper:size-44"></div> 71 {/if} 72 - </div> 73 - <QRCodeModal 74 - bind:open={qrOpen} 75 - href={profileUrl} 76 - context={{ 77 - title: getName(data), 78 - avatar: data.profile.avatar 79 - }} 80 - /> 81 82 <div class="text-4xl font-bold wrap-anywhere"> 83 {getName(data)}
··· 8 import { getDescription, getName } from '$lib/helper'; 9 import { page } from '$app/state'; 10 import type { ActorIdentifier } from '@atcute/lexicons'; 11 + import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte'; 12 13 let { 14 data, ··· 22 renderer.link = ({ href, title, text }) => 23 `<a target="_blank" href="${href}" title="${title}">${text}</a>`; 24 25 + const profileUrl = $derived(`${page.url.origin}/${data.handle}`); 26 </script> 27 28 <!-- lg:fixed lg:h-screen lg:w-1/4 lg:max-w-none lg:px-12 lg:pt-24 xl:w-1/3 --> ··· 30 class="mx-auto flex max-w-lg flex-col justify-between px-8 @5xl/wrapper:fixed @5xl/wrapper:h-screen @5xl/wrapper:w-1/4 @5xl/wrapper:max-w-none @5xl/wrapper:px-12" 31 > 32 <div class="flex flex-col gap-4 pt-16 pb-8 @5xl/wrapper:h-screen @5xl/wrapper:pt-24"> 33 + <a 34 + href={profileUrl} 35 + class="w-fit" 36 + use:qrOverlay={{ 37 + context: { 38 + title: getName(data), 39 + avatar: data.profile.avatar 40 + } 41 + }} 42 > 43 {#if data.profile.avatar} 44 <img ··· 49 {:else} 50 <div class="bg-base-300 dark:bg-base-700 size-32 rounded-full @5xl/wrapper:size-44"></div> 51 {/if} 52 + </a> 53 54 <div class="text-4xl font-bold wrap-anywhere"> 55 {getName(data)}