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