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