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</script>
47
48<div
49 class={[
50 'relative mx-auto flex max-w-lg flex-col justify-between px-8',
51 profilePosition === 'side'
52 ? '@5xl/wrapper:fixed @5xl/wrapper:h-screen @5xl/wrapper:w-1/4 @5xl/wrapper:max-w-none @5xl/wrapper:px-12'
53 : '@5xl/wrapper:max-w-4xl @5xl/wrapper:px-12'
54 ]}
55>
56 <div
57 class={[
58 'flex flex-col gap-4 pt-16 pb-4',
59 profilePosition === 'side' && '@5xl/wrapper:h-screen @5xl/wrapper:pt-24'
60 ]}
61 >
62 <!-- Avatar with edit capability -->
63 <button
64 type="button"
65 class={[
66 'group relative size-32 shrink-0 cursor-pointer overflow-hidden rounded-full',
67 profilePosition === 'side' && '@5xl/wrapper:size-44'
68 ]}
69 onmouseenter={() => (isHoveringAvatar = true)}
70 onmouseleave={() => (isHoveringAvatar = false)}
71 onclick={handleFileInputClick}
72 >
73 <Avatar
74 src={getAvatarUrl()}
75 class={[
76 'border-base-400 dark:border-base-800 size-32 shrink-0 rounded-full border object-cover',
77 profilePosition === 'side' && '@5xl/wrapper:size-44'
78 ]}
79 />
80
81 <!-- Hover overlay -->
82 <div
83 class={[
84 'absolute inset-0 flex items-center justify-center rounded-full bg-black/50 transition-opacity duration-200',
85 isHoveringAvatar ? 'opacity-100' : 'opacity-0'
86 ]}
87 >
88 <div class="text-center text-sm text-white">
89 <svg
90 xmlns="http://www.w3.org/2000/svg"
91 fill="none"
92 viewBox="0 0 24 24"
93 stroke-width="1.5"
94 stroke="currentColor"
95 class="mx-auto mb-1 size-6"
96 >
97 <path
98 stroke-linecap="round"
99 stroke-linejoin="round"
100 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"
101 />
102 <path
103 stroke-linecap="round"
104 stroke-linejoin="round"
105 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"
106 />
107 </svg>
108 <span class="font-medium">Click to change</span>
109 </div>
110 </div>
111 </button>
112
113 <input
114 bind:this={fileInput}
115 type="file"
116 accept="image/*"
117 class="hidden"
118 onchange={handleAvatarChange}
119 />
120
121 <!-- Editable Name -->
122 {#if data.publication}
123 <div class="text-4xl font-bold wrap-anywhere">
124 <PlainTextEditor bind:contentDict={data.publication} key="name" placeholder="Your name" />
125 </div>
126 {/if}
127
128 <!-- Editable Description -->
129 <div class="scrollbar -mx-4 grow overflow-x-hidden overflow-y-scroll px-4">
130 {#if data.publication}
131 <MarkdownTextEditor
132 bind:contentDict={data.publication}
133 key="description"
134 placeholder="Something about me..."
135 class="text-base-600 dark:text-base-400 prose dark:prose-invert prose-a:text-accent-500 prose-a:no-underline"
136 />
137 {/if}
138 </div>
139
140 {#if !hideBlento}
141 <MadeWithBlento class="hidden {profilePosition === 'side' && '@5xl/wrapper:block'}" />
142 {/if}
143 </div>
144</div>