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