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