your personal website on atproto - mirror blento.app
at fix-favicon 240 lines 7.1 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 accentColor = $derived(data.publication?.preferences?.accentColor ?? 'pink'); 15 let baseColor = $derived(data.publication?.preferences?.baseColor ?? 'stone'); 16 17 function updateTheme(newAccent: string, newBase: string) { 18 data.publication.preferences ??= {}; 19 data.publication.preferences.accentColor = newAccent; 20 data.publication.preferences.baseColor = newBase; 21 data = { ...data }; 22 } 23 24 let profilePosition = $derived(getProfilePosition(data)); 25 26 function toggleProfilePosition() { 27 data.publication.preferences ??= {}; 28 data.publication.preferences.profilePosition = profilePosition === 'side' ? 'top' : 'side'; 29 data = { ...data }; 30 } 31 32 let fileInput: HTMLInputElement; 33 let isHoveringAvatar = $state(false); 34 35 async function handleAvatarChange(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 data.publication.icon = { 45 blob: compressedBlob, 46 objectUrl 47 } as any; 48 49 data = { ...data }; 50 } catch (error) { 51 console.error('Failed to process image:', error); 52 } 53 } 54 55 function getAvatarUrl(): string | undefined { 56 const customIcon = getImage(data.publication, data.did, 'icon'); 57 if (customIcon) return customIcon; 58 return data.profile.avatar; 59 } 60 61 function handleFileInputClick() { 62 fileInput.click(); 63 } 64 65 let isMobile = getIsMobile(); 66</script> 67 68<div 69 class={[ 70 'relative mx-auto flex max-w-lg flex-col justify-between px-8', 71 profilePosition === 'side' 72 ? '@5xl/wrapper:fixed @5xl/wrapper:h-screen @5xl/wrapper:w-1/4 @5xl/wrapper:max-w-none @5xl/wrapper:px-12' 73 : '@5xl/wrapper:max-w-4xl @5xl/wrapper:px-12' 74 ]} 75> 76 <div 77 class={[ 78 'absolute left-2 z-20 flex gap-2', 79 profilePosition === 'side' ? 'top-2 left-14' : 'top-2' 80 ]} 81 > 82 <Button 83 size="icon" 84 onclick={() => { 85 data.publication.preferences ??= {}; 86 data.publication.preferences.hideProfileSection = true; 87 data = { ...data }; 88 }} 89 variant="ghost" 90 > 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="size-6" 98 > 99 <path 100 stroke-linecap="round" 101 stroke-linejoin="round" 102 d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88" 103 /> 104 </svg> 105 </Button> 106 107 <!-- Position toggle button (desktop only) --> 108 {#if !isMobile()} 109 <Button size="icon" type="button" onclick={toggleProfilePosition} variant="ghost"> 110 {#if profilePosition === 'side'} 111 <svg 112 xmlns="http://www.w3.org/2000/svg" 113 fill="none" 114 viewBox="0 0 24 24" 115 stroke-width="1.5" 116 stroke="currentColor" 117 class="size-6" 118 > 119 <path 120 stroke-linecap="round" 121 stroke-linejoin="round" 122 d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25" 123 /> 124 </svg> 125 {:else} 126 <svg 127 xmlns="http://www.w3.org/2000/svg" 128 fill="none" 129 viewBox="0 0 24 24" 130 stroke-width="1.5" 131 stroke="currentColor" 132 class="size-6" 133 > 134 <path 135 stroke-linecap="round" 136 stroke-linejoin="round" 137 d="m19.5 4.5-15 15m0 0h11.25m-11.25 0V8.25" 138 /> 139 </svg> 140 {/if} 141 </Button> 142 {/if} 143 144 <!-- Theme selection --> 145 <SelectThemePopover 146 {accentColor} 147 {baseColor} 148 onchanged={(newAccent, newBase) => updateTheme(newAccent, newBase)} 149 /> 150 </div> 151 152 <div 153 class={[ 154 'flex flex-col gap-4 pt-16 pb-4', 155 profilePosition === 'side' && '@5xl/wrapper:h-screen @5xl/wrapper:pt-24' 156 ]} 157 > 158 <!-- Avatar with edit capability --> 159 <button 160 type="button" 161 class={[ 162 'group relative size-32 shrink-0 cursor-pointer overflow-hidden rounded-full', 163 profilePosition === 'side' && '@5xl/wrapper:size-44' 164 ]} 165 onmouseenter={() => (isHoveringAvatar = true)} 166 onmouseleave={() => (isHoveringAvatar = false)} 167 onclick={handleFileInputClick} 168 > 169 <Avatar 170 src={getAvatarUrl()} 171 class={[ 172 'border-base-400 dark:border-base-800 size-32 shrink-0 rounded-full border object-cover', 173 profilePosition === 'side' && '@5xl/wrapper:size-44' 174 ]} 175 /> 176 177 <!-- Hover overlay --> 178 <div 179 class={[ 180 'absolute inset-0 flex items-center justify-center rounded-full bg-black/50 transition-opacity duration-200', 181 isHoveringAvatar ? 'opacity-100' : 'opacity-0' 182 ]} 183 > 184 <div class="text-center text-sm text-white"> 185 <svg 186 xmlns="http://www.w3.org/2000/svg" 187 fill="none" 188 viewBox="0 0 24 24" 189 stroke-width="1.5" 190 stroke="currentColor" 191 class="mx-auto mb-1 size-6" 192 > 193 <path 194 stroke-linecap="round" 195 stroke-linejoin="round" 196 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" 197 /> 198 <path 199 stroke-linecap="round" 200 stroke-linejoin="round" 201 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" 202 /> 203 </svg> 204 <span class="font-medium">Click to change</span> 205 </div> 206 </div> 207 </button> 208 209 <input 210 bind:this={fileInput} 211 type="file" 212 accept="image/*" 213 class="hidden" 214 onchange={handleAvatarChange} 215 /> 216 217 <!-- Editable Name --> 218 {#if data.publication} 219 <div class="text-4xl font-bold wrap-anywhere"> 220 <PlainTextEditor bind:contentDict={data.publication} key="name" placeholder="Your name" /> 221 </div> 222 {/if} 223 224 <!-- Editable Description --> 225 <div class="scrollbar -mx-4 grow overflow-x-hidden overflow-y-scroll px-4"> 226 {#if data.publication} 227 <MarkdownTextEditor 228 bind:contentDict={data.publication} 229 key="description" 230 placeholder="Something about me..." 231 class="text-base-600 dark:text-base-400 prose dark:prose-invert prose-a:text-accent-500 prose-a:no-underline" 232 /> 233 {/if} 234 </div> 235 236 {#if !hideBlento} 237 <MadeWithBlento class="hidden {profilePosition === 'side' && '@5xl/wrapper:block'}" /> 238 {/if} 239 </div> 240</div>