your personal website on atproto - mirror blento.app
at signup 225 lines 6.6 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 type { Editor } from '@tiptap/core'; 9 import MadeWithBlento from './MadeWithBlento.svelte'; 10 11 let { data = $bindable(), hideBlento = false }: { data: WebsiteData; hideBlento?: boolean } = 12 $props(); 13 14 let profilePosition = $derived(getProfilePosition(data)); 15 16 function toggleProfilePosition() { 17 data.publication.preferences ??= {}; 18 data.publication.preferences.profilePosition = profilePosition === 'side' ? 'top' : 'side'; 19 data = { ...data }; 20 } 21 22 let fileInput: HTMLInputElement; 23 let isHoveringAvatar = $state(false); 24 25 async function handleAvatarChange(event: Event) { 26 const target = event.target as HTMLInputElement; 27 const file = target.files?.[0]; 28 if (!file) return; 29 30 try { 31 const compressedBlob = await compressImage(file); 32 const objectUrl = URL.createObjectURL(compressedBlob); 33 34 data.publication.icon = { 35 blob: compressedBlob, 36 objectUrl 37 } as any; 38 39 data = { ...data }; 40 } catch (error) { 41 console.error('Failed to process image:', error); 42 } 43 } 44 45 function getAvatarUrl(): string | undefined { 46 const customIcon = getImage(data.publication, data.did, 'icon'); 47 if (customIcon) return customIcon; 48 return data.profile.avatar; 49 } 50 51 function handleFileInputClick() { 52 fileInput.click(); 53 } 54 55 let isMobile = getIsMobile(); 56</script> 57 58<div 59 class={[ 60 'relative mx-auto flex max-w-lg flex-col justify-between px-8', 61 profilePosition === 'side' 62 ? '@5xl/wrapper:fixed @5xl/wrapper:h-screen @5xl/wrapper:w-1/4 @5xl/wrapper:max-w-none @5xl/wrapper:px-12' 63 : '@5xl/wrapper:max-w-4xl @5xl/wrapper:px-12' 64 ]} 65> 66 <div 67 class={[ 68 'absolute left-2 z-20 flex gap-2', 69 profilePosition === 'side' ? 'top-2 left-14' : 'top-2' 70 ]} 71 > 72 <Button 73 size="icon" 74 onclick={() => { 75 data.publication.preferences ??= {}; 76 data.publication.preferences.hideProfileSection = true; 77 data = { ...data }; 78 }} 79 variant="ghost" 80 > 81 <svg 82 xmlns="http://www.w3.org/2000/svg" 83 fill="none" 84 viewBox="0 0 24 24" 85 stroke-width="1.5" 86 stroke="currentColor" 87 class="size-6" 88 > 89 <path 90 stroke-linecap="round" 91 stroke-linejoin="round" 92 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" 93 /> 94 </svg> 95 </Button> 96 97 <!-- Position toggle button (desktop only) --> 98 {#if !isMobile()} 99 <Button size="icon" type="button" onclick={toggleProfilePosition} variant="ghost"> 100 {#if profilePosition === 'side'} 101 <svg 102 xmlns="http://www.w3.org/2000/svg" 103 fill="none" 104 viewBox="0 0 24 24" 105 stroke-width="1.5" 106 stroke="currentColor" 107 class="size-6" 108 > 109 <path 110 stroke-linecap="round" 111 stroke-linejoin="round" 112 d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25" 113 /> 114 </svg> 115 {:else} 116 <svg 117 xmlns="http://www.w3.org/2000/svg" 118 fill="none" 119 viewBox="0 0 24 24" 120 stroke-width="1.5" 121 stroke="currentColor" 122 class="size-6" 123 > 124 <path 125 stroke-linecap="round" 126 stroke-linejoin="round" 127 d="m19.5 4.5-15 15m0 0h11.25m-11.25 0V8.25" 128 /> 129 </svg> 130 {/if} 131 </Button> 132 {/if} 133 </div> 134 135 <div 136 class={[ 137 'flex flex-col gap-4 pt-16 pb-8', 138 profilePosition === 'side' && '@5xl/wrapper:h-screen @5xl/wrapper:pt-24' 139 ]} 140 > 141 <!-- Avatar with edit capability --> 142 <button 143 type="button" 144 class={[ 145 'group relative size-32 shrink-0 cursor-pointer overflow-hidden rounded-full', 146 profilePosition === 'side' && '@5xl/wrapper:size-44' 147 ]} 148 onmouseenter={() => (isHoveringAvatar = true)} 149 onmouseleave={() => (isHoveringAvatar = false)} 150 onclick={handleFileInputClick} 151 > 152 <Avatar 153 src={getAvatarUrl()} 154 class={[ 155 'border-base-400 dark:border-base-800 size-32 shrink-0 rounded-full border object-cover', 156 profilePosition === 'side' && '@5xl/wrapper:size-44' 157 ]} 158 /> 159 160 <!-- Hover overlay --> 161 <div 162 class={[ 163 'absolute inset-0 flex items-center justify-center rounded-full bg-black/50 transition-opacity duration-200', 164 isHoveringAvatar ? 'opacity-100' : 'opacity-0' 165 ]} 166 > 167 <div class="text-center text-sm text-white"> 168 <svg 169 xmlns="http://www.w3.org/2000/svg" 170 fill="none" 171 viewBox="0 0 24 24" 172 stroke-width="1.5" 173 stroke="currentColor" 174 class="mx-auto mb-1 size-6" 175 > 176 <path 177 stroke-linecap="round" 178 stroke-linejoin="round" 179 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" 180 /> 181 <path 182 stroke-linecap="round" 183 stroke-linejoin="round" 184 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" 185 /> 186 </svg> 187 <span class="font-medium">Click to change</span> 188 </div> 189 </div> 190 </button> 191 192 <input 193 bind:this={fileInput} 194 type="file" 195 accept="image/*" 196 class="hidden" 197 onchange={handleAvatarChange} 198 /> 199 200 <!-- Editable Name --> 201 {#if data.publication} 202 <div class="text-4xl font-bold wrap-anywhere"> 203 <PlainTextEditor bind:contentDict={data.publication} key="name" placeholder="Your name" /> 204 </div> 205 {/if} 206 207 <!-- Editable Description --> 208 <div class="scrollbar -mx-4 grow overflow-x-hidden overflow-y-scroll px-4"> 209 {#if data.publication} 210 <MarkdownTextEditor 211 bind:contentDict={data.publication} 212 key="description" 213 placeholder="Something about me..." 214 class="text-base-600 dark:text-base-400 prose dark:prose-invert prose-a:text-accent-500 prose-a:no-underline" 215 /> 216 {/if} 217 </div> 218 219 <div class={['h-10.5 w-1', profilePosition === 'side' && '@5xl/wrapper:hidden']}></div> 220 221 {#if !hideBlento} 222 <MadeWithBlento class="hidden {profilePosition === 'side' && '@5xl/wrapper:block'}" /> 223 {/if} 224 </div> 225</div>