tangled
alpha
login
or
join now
flo-bit.dev
/
blento
your personal website on atproto - mirror
blento.app
20
fork
atom
overview
issues
pulls
pipelines
custom favicon, og image
Florian
2 weeks ago
53c2b9fd
4ff93c7a
+154
-12
3 changed files
expand all
collapse all
unified
split
src
lib
cards
LinkCard
EditingLinkCard.svelte
LinkCard.svelte
website
EditableProfile.svelte
+151
-10
src/lib/cards/LinkCard/EditingLinkCard.svelte
···
1
1
<script lang="ts">
2
2
import { browser } from '$app/environment';
3
3
-
import { getImage } from '$lib/helper';
3
3
+
import { getImage, compressImage } from '$lib/helper';
4
4
import { getDidContext, getIsMobile } from '$lib/website/context';
5
5
import type { ContentComponentProps } from '../types';
6
6
import PlainTextEditor from '$lib/components/PlainTextEditor.svelte';
7
7
8
8
let { item = $bindable() }: ContentComponentProps = $props();
9
9
10
10
+
let faviconInputRef: HTMLInputElement;
11
11
+
let imageInputRef: HTMLInputElement;
12
12
+
let isHoveringFavicon = $state(false);
13
13
+
let isHoveringImage = $state(false);
14
14
+
15
15
+
async function handleFaviconChange(event: Event) {
16
16
+
const target = event.target as HTMLInputElement;
17
17
+
const file = target.files?.[0];
18
18
+
if (!file) return;
19
19
+
20
20
+
try {
21
21
+
const compressedBlob = await compressImage(file, 128);
22
22
+
const objectUrl = URL.createObjectURL(compressedBlob);
23
23
+
24
24
+
item.cardData.favicon = {
25
25
+
blob: compressedBlob,
26
26
+
objectUrl
27
27
+
} as any;
28
28
+
29
29
+
faviconHasError = false;
30
30
+
} catch (error) {
31
31
+
console.error('Failed to process image:', error);
32
32
+
}
33
33
+
}
34
34
+
35
35
+
async function handleImageChange(event: Event) {
36
36
+
const target = event.target as HTMLInputElement;
37
37
+
const file = target.files?.[0];
38
38
+
if (!file) return;
39
39
+
40
40
+
try {
41
41
+
const compressedBlob = await compressImage(file);
42
42
+
const objectUrl = URL.createObjectURL(compressedBlob);
43
43
+
44
44
+
item.cardData.image = {
45
45
+
blob: compressedBlob,
46
46
+
objectUrl
47
47
+
} as any;
48
48
+
} catch (error) {
49
49
+
console.error('Failed to process image:', error);
50
50
+
}
51
51
+
}
52
52
+
10
53
let isMobile = getIsMobile();
11
54
12
55
let faviconHasError = $state(false);
···
55
98
let did = getDidContext();
56
99
</script>
57
100
101
101
+
<input
102
102
+
type="file"
103
103
+
accept="image/*"
104
104
+
class="hidden"
105
105
+
bind:this={faviconInputRef}
106
106
+
onchange={handleFaviconChange}
107
107
+
/>
108
108
+
<input
109
109
+
type="file"
110
110
+
accept="image/*"
111
111
+
class="hidden"
112
112
+
bind:this={imageInputRef}
113
113
+
onchange={handleImageChange}
114
114
+
/>
115
115
+
58
116
<div class="relative flex h-full flex-col justify-between p-4">
59
117
<div
60
118
class={[
···
64
122
></div>
65
123
66
124
<div class={isFetchingMetadata ? 'pointer-events-none' : ''}>
67
67
-
<div
68
68
-
class="bg-base-100 border-base-300 accent:bg-accent-100/50 accent:border-accent-200 dark:border-base-800 dark:bg-base-900 mb-2 inline-flex size-8 items-center justify-center rounded-xl border"
125
125
+
<button
126
126
+
type="button"
127
127
+
class="bg-base-100 border-base-300 accent:bg-accent-100/50 accent:border-accent-200 dark:border-base-800 dark:bg-base-900 hover:ring-accent-500 relative mb-2 inline-flex size-8 cursor-pointer items-center justify-center rounded-xl border transition-all duration-200 hover:ring-2"
128
128
+
onclick={() => faviconInputRef?.click()}
129
129
+
onmouseenter={() => (isHoveringFavicon = true)}
130
130
+
onmouseleave={() => (isHoveringFavicon = false)}
69
131
>
70
132
{#if hasFetched && item.cardData.favicon && !faviconHasError}
71
133
<img
···
90
152
/>
91
153
</svg>
92
154
{/if}
93
93
-
</div>
155
155
+
<!-- Hover overlay -->
156
156
+
<div
157
157
+
class={[
158
158
+
'absolute inset-0 flex items-center justify-center rounded-xl bg-black/50 transition-opacity duration-200',
159
159
+
isHoveringFavicon ? 'opacity-100' : 'opacity-0'
160
160
+
]}
161
161
+
>
162
162
+
<svg
163
163
+
xmlns="http://www.w3.org/2000/svg"
164
164
+
fill="none"
165
165
+
viewBox="0 0 24 24"
166
166
+
stroke-width="2"
167
167
+
stroke="currentColor"
168
168
+
class="size-4 text-white"
169
169
+
>
170
170
+
<path
171
171
+
stroke-linecap="round"
172
172
+
stroke-linejoin="round"
173
173
+
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Z"
174
174
+
/>
175
175
+
</svg>
176
176
+
</div>
177
177
+
</button>
94
178
95
179
<div
96
180
class={[
···
121
205
</div>
122
206
</div>
123
207
124
124
-
{#if hasFetched && browser && ((isMobile() && item.mobileH >= 8) || (!isMobile() && item.h >= 4)) && item.cardData.image}
125
125
-
<img
126
126
-
class="mb-2 aspect-2/1 w-full rounded-xl object-cover opacity-100 transition-opacity duration-100 starting:opacity-0"
127
127
-
src={getImage(item.cardData, did)}
128
128
-
alt=""
129
129
-
/>
208
208
+
{#if hasFetched && browser && ((isMobile() && item.mobileH >= 8) || (!isMobile() && item.h >= 4))}
209
209
+
<button
210
210
+
type="button"
211
211
+
class="hover:ring-accent-500 relative mb-2 aspect-2/1 w-full cursor-pointer overflow-hidden rounded-xl transition-all duration-200 hover:ring-2"
212
212
+
onclick={() => imageInputRef?.click()}
213
213
+
onmouseenter={() => (isHoveringImage = true)}
214
214
+
onmouseleave={() => (isHoveringImage = false)}
215
215
+
>
216
216
+
{#if item.cardData.image}
217
217
+
<img
218
218
+
class="h-full w-full object-cover opacity-100 transition-opacity duration-100 starting:opacity-0"
219
219
+
src={getImage(item.cardData, did)}
220
220
+
alt=""
221
221
+
/>
222
222
+
{:else}
223
223
+
<div class="bg-base-200 dark:bg-base-800 flex h-full w-full items-center justify-center">
224
224
+
<svg
225
225
+
xmlns="http://www.w3.org/2000/svg"
226
226
+
fill="none"
227
227
+
viewBox="0 0 24 24"
228
228
+
stroke-width="1.5"
229
229
+
stroke="currentColor"
230
230
+
class="text-base-400 dark:text-base-600 size-8"
231
231
+
>
232
232
+
<path
233
233
+
stroke-linecap="round"
234
234
+
stroke-linejoin="round"
235
235
+
d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
236
236
+
/>
237
237
+
</svg>
238
238
+
</div>
239
239
+
{/if}
240
240
+
<!-- Hover overlay -->
241
241
+
<div
242
242
+
class={[
243
243
+
'absolute inset-0 flex items-center justify-center rounded-xl bg-black/50 transition-opacity duration-200',
244
244
+
isHoveringImage ? 'opacity-100' : 'opacity-0'
245
245
+
]}
246
246
+
>
247
247
+
<div class="text-center text-sm text-white">
248
248
+
<svg
249
249
+
xmlns="http://www.w3.org/2000/svg"
250
250
+
fill="none"
251
251
+
viewBox="0 0 24 24"
252
252
+
stroke-width="1.5"
253
253
+
stroke="currentColor"
254
254
+
class="mx-auto mb-1 size-6"
255
255
+
>
256
256
+
<path
257
257
+
stroke-linecap="round"
258
258
+
stroke-linejoin="round"
259
259
+
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"
260
260
+
/>
261
261
+
<path
262
262
+
stroke-linecap="round"
263
263
+
stroke-linejoin="round"
264
264
+
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"
265
265
+
/>
266
266
+
</svg>
267
267
+
<span class="font-medium">{item.cardData.image ? 'Change' : 'Add image'}</span>
268
268
+
</div>
269
269
+
</div>
270
270
+
</button>
130
271
{/if}
131
272
</div>
+1
-1
src/lib/cards/LinkCard/LinkCard.svelte
···
23
23
<img
24
24
class="size-6 rounded-lg object-cover"
25
25
onerror={() => (faviconHasError = true)}
26
26
-
src={item.cardData.favicon}
26
26
+
src={getImage(item.cardData, did, 'favicon')}
27
27
alt=""
28
28
/>
29
29
{:else}
+2
-1
src/lib/website/EditableProfile.svelte
···
8
8
import type { Editor } from '@tiptap/core';
9
9
import MadeWithBlento from './MadeWithBlento.svelte';
10
10
11
11
-
let { data = $bindable() }: { data: WebsiteData } = $props();
11
11
+
let { data = $bindable(), hideBlento = false }: { data: WebsiteData; hideBlento?: boolean } =
12
12
+
$props();
12
13
13
14
let profilePosition = $derived(getProfilePosition(data));
14
15