your personal website on atproto - mirror
blento.app
1<script lang="ts">
2 import type { WebsiteData } from '$lib/types';
3 import { getDescription, getName, getImage, compressImage } from '$lib/helper';
4 import PlainTextEditor from '$lib/components/PlainTextEditor.svelte';
5 import MarkdownTextEditor from '$lib/components/MarkdownTextEditor.svelte';
6 import type { Editor } from '@tiptap/core';
7
8 let { data = $bindable() }: { data: WebsiteData } = $props();
9
10 let fileInput: HTMLInputElement;
11 let isHoveringAvatar = $state(false);
12 let descriptionEditor: Editor | null = $state(null);
13
14 // Initialize publication if needed
15 $effect(() => {
16 if (!data.publication) {
17 data.publication = {
18 name: getName(data),
19 description: getDescription(data)
20 };
21 } else {
22 if (data.publication.name === undefined) {
23 data.publication.name = getName(data);
24 }
25 if (data.publication.description === undefined) {
26 data.publication.description = getDescription(data);
27 }
28 }
29 });
30
31 async function handleAvatarChange(event: Event) {
32 const target = event.target as HTMLInputElement;
33 const file = target.files?.[0];
34 if (!file) return;
35
36 try {
37 const compressedBlob = await compressImage(file);
38 const objectUrl = URL.createObjectURL(compressedBlob);
39
40 data.publication ??= {};
41 data.publication.icon = {
42 blob: compressedBlob,
43 objectUrl
44 } as any;
45
46 data = { ...data };
47 } catch (error) {
48 console.error('Failed to process image:', error);
49 }
50 }
51
52 function getAvatarUrl(): string | undefined {
53 const customIcon = getImage(data.publication ?? {}, data.did, 'icon');
54 if (customIcon) return customIcon;
55 return data.profile.avatar;
56 }
57
58 function handleFileInputClick() {
59 fileInput.click();
60 }
61</script>
62
63<div
64 class="mx-auto flex max-w-lg flex-col justify-between px-8 @5xl/wrapper:fixed @5xl/wrapper:h-screen @5xl/wrapper:w-1/4 @5xl/wrapper:max-w-none @5xl/wrapper:px-12"
65>
66 <div class="flex flex-col gap-4 pt-16 pb-8 @5xl/wrapper:h-screen @5xl/wrapper:pt-24">
67 <!-- Avatar with edit capability -->
68 <button
69 type="button"
70 class="group relative size-32 cursor-pointer overflow-hidden rounded-full @5xl/wrapper:size-44"
71 onmouseenter={() => (isHoveringAvatar = true)}
72 onmouseleave={() => (isHoveringAvatar = false)}
73 onclick={handleFileInputClick}
74 >
75 {#if getAvatarUrl()}
76 <img
77 class="border-base-400 dark:border-base-800 size-full rounded-full border object-cover"
78 src={getAvatarUrl()}
79 alt=""
80 />
81 {:else}
82 <div class="bg-base-300 dark:bg-base-700 size-full rounded-full"></div>
83 {/if}
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>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 bind:contentDict={data.publication} key="name" placeholder="Your name" />
129 </div>
130 {/if}
131
132 <!-- Editable Description -->
133 <div class="scrollbar -mx-4 grow overflow-x-hidden overflow-y-scroll px-4">
134 {#if data.publication}
135 <MarkdownTextEditor
136 bind:editor={descriptionEditor}
137 bind:contentDict={data.publication}
138 key="description"
139 placeholder="Add a description... (supports markdown)"
140 class=""
141 />
142 {/if}
143 </div>
144
145 <div class="h-10.5 w-1 @5xl/wrapper:hidden"></div>
146
147 <div class="hidden text-xs font-light @5xl/wrapper:block">
148 made with <a
149 href="https://blento.app"
150 target="_blank"
151 class="hover:text-accent-600 dark:hover:text-accent-400 font-medium transition-colors duration-200"
152 >blento</a
153 >
154 </div>
155 </div>
156</div>