your personal website on atproto - mirror blento.app
at custom-domains 146 lines 4.5 kB view raw
1<script lang="ts"> 2 import type { WebsiteData } from '$lib/types'; 3 import { getImage, compressImage, getProfilePosition } from '$lib/helper'; 4 import PlainTextEditor from '$lib/components/PlainTextEditor.svelte'; 5 import MarkdownTextEditor from '$lib/components/MarkdownTextEditor.svelte'; 6 import { Avatar, Button } from '@foxui/core'; 7 import { getIsMobile } from './context'; 8 import MadeWithBlento from './MadeWithBlento.svelte'; 9 import { SelectThemePopover } from '$lib/components/select-theme'; 10 11 let { data = $bindable(), hideBlento = false }: { data: WebsiteData; hideBlento?: boolean } = 12 $props(); 13 14 let fileInput: HTMLInputElement; 15 let isHoveringAvatar = $state(false); 16 17 async function handleAvatarChange(event: Event) { 18 const target = event.target as HTMLInputElement; 19 const file = target.files?.[0]; 20 if (!file) return; 21 22 try { 23 const compressedBlob = await compressImage(file); 24 const objectUrl = URL.createObjectURL(compressedBlob); 25 26 data.publication.icon = { 27 blob: compressedBlob, 28 objectUrl 29 } as any; 30 31 data = { ...data }; 32 } catch (error) { 33 console.error('Failed to process image:', error); 34 } 35 } 36 37 function getAvatarUrl(): string | undefined { 38 const customIcon = getImage(data.publication, data.did, 'icon'); 39 if (customIcon) return customIcon; 40 return data.profile.avatar; 41 } 42 43 function handleFileInputClick() { 44 fileInput.click(); 45 } 46 47 let profilePosition = $derived(getProfilePosition(data)); 48</script> 49 50<div 51 class={[ 52 'relative mx-auto flex max-w-lg flex-col justify-between px-8', 53 profilePosition === 'side' 54 ? '@5xl/wrapper:fixed @5xl/wrapper:h-screen @5xl/wrapper:w-1/4 @5xl/wrapper:max-w-none @5xl/wrapper:px-12' 55 : '@5xl/wrapper:max-w-4xl @5xl/wrapper:px-12' 56 ]} 57> 58 <div 59 class={[ 60 'flex flex-col gap-4 pt-16 pb-4', 61 profilePosition === 'side' && '@5xl/wrapper:h-screen @5xl/wrapper:pt-24' 62 ]} 63 > 64 <!-- Avatar with edit capability --> 65 <button 66 type="button" 67 class={[ 68 'group relative size-32 shrink-0 cursor-pointer overflow-hidden rounded-full', 69 profilePosition === 'side' && '@5xl/wrapper:size-44' 70 ]} 71 onmouseenter={() => (isHoveringAvatar = true)} 72 onmouseleave={() => (isHoveringAvatar = false)} 73 onclick={handleFileInputClick} 74 > 75 <Avatar 76 src={getAvatarUrl()} 77 class={[ 78 'border-base-400 dark:border-base-800 size-32 shrink-0 rounded-full border object-cover', 79 profilePosition === 'side' && '@5xl/wrapper:size-44' 80 ]} 81 /> 82 83 <!-- Hover overlay --> 84 <div 85 class={[ 86 'absolute inset-0 flex items-center justify-center rounded-full bg-black/50 transition-opacity duration-200', 87 isHoveringAvatar ? 'opacity-100' : 'opacity-0' 88 ]} 89 > 90 <div class="text-center text-sm text-white"> 91 <svg 92 xmlns="http://www.w3.org/2000/svg" 93 fill="none" 94 viewBox="0 0 24 24" 95 stroke-width="1.5" 96 stroke="currentColor" 97 class="mx-auto mb-1 size-6" 98 > 99 <path 100 stroke-linecap="round" 101 stroke-linejoin="round" 102 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" 103 /> 104 <path 105 stroke-linecap="round" 106 stroke-linejoin="round" 107 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" 108 /> 109 </svg> 110 <span class="font-medium">Click to change</span> 111 </div> 112 </div> 113 </button> 114 115 <input 116 bind:this={fileInput} 117 type="file" 118 accept="image/*" 119 class="hidden" 120 onchange={handleAvatarChange} 121 /> 122 123 <!-- Editable Name --> 124 {#if data.publication} 125 <div class="text-4xl font-bold wrap-anywhere"> 126 <PlainTextEditor bind:contentDict={data.publication} key="name" placeholder="Your name" /> 127 </div> 128 {/if} 129 130 <!-- Editable Description --> 131 <div class="scrollbar -mx-4 grow overflow-x-hidden overflow-y-scroll px-4"> 132 {#if data.publication} 133 <MarkdownTextEditor 134 bind:contentDict={data.publication} 135 key="description" 136 placeholder="Something about me..." 137 class="text-base-600 dark:text-base-400 prose dark:prose-invert prose-a:text-accent-500 prose-a:no-underline" 138 /> 139 {/if} 140 </div> 141 142 {#if !hideBlento} 143 <MadeWithBlento class="hidden {profilePosition === 'side' && '@5xl/wrapper:block'}" /> 144 {/if} 145 </div> 146</div>