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