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
v0.2
jycouet
2 weeks ago
bbcdbb12
bc24bc8f
+71
-65
5 changed files
expand all
collapse all
unified
split
src
lib
cards
BlueskyProfileCard
BlueskyProfileCard.svelte
EventCard
EventCard.svelte
components
qr
QRCodeDisplay.svelte
QRCodeModal.svelte
website
Profile.svelte
+1
-2
src/lib/cards/BlueskyProfileCard/BlueskyProfileCard.svelte
···
14
14
use:qrOverlay={{
15
15
disabled: isEditing,
16
16
context: {
17
17
-
title: item.cardData.displayName || item.cardData.handle,
18
18
-
avatar: item.cardData.avatar
17
17
+
title: item.cardData.displayName || item.cardData.handle
19
18
}
20
19
}}
21
20
>
+6
src/lib/cards/EventCard/EventCard.svelte
···
7
7
import type { EventData } from '.';
8
8
import { parseUri } from '$lib/atproto';
9
9
import { browser } from '$app/environment';
10
10
+
import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte';
10
11
11
12
let { item }: ContentComponentProps = $props();
12
13
···
268
269
class="absolute inset-0 h-full w-full"
269
270
target="_blank"
270
271
rel="noopener noreferrer"
272
272
+
use:qrOverlay={{
273
273
+
context: {
274
274
+
title: eventData?.name ?? ''
275
275
+
}
276
276
+
}}
271
277
>
272
278
<span class="sr-only">View event on smokesignal.events</span>
273
279
</a>
+52
-7
src/lib/components/qr/QRCodeDisplay.svelte
···
1
1
<script lang="ts">
2
2
import { onMount } from 'svelte';
3
3
4
4
-
let { url, size = 280 }: { url: string; size?: number } = $props();
4
4
+
let {
5
5
+
url,
6
6
+
icon,
7
7
+
iconColor,
8
8
+
class: className = ''
9
9
+
}: {
10
10
+
url: string;
11
11
+
icon?: string;
12
12
+
iconColor?: string;
13
13
+
class?: string;
14
14
+
} = $props();
5
15
6
16
let container: HTMLDivElement | undefined = $state();
7
17
18
18
+
// Convert SVG string to data URI for use as QR center image
19
19
+
function svgToDataUri(svg: string, color: string): string {
20
20
+
// Add fill color to SVG - insert fill attribute on the svg tag
21
21
+
let coloredSvg = svg;
22
22
+
if (!svg.includes('fill=')) {
23
23
+
// No fill attribute, add it to the svg tag
24
24
+
coloredSvg = svg.replace('<svg', `<svg fill="${color}"`);
25
25
+
} else {
26
26
+
// Replace existing fill attributes
27
27
+
coloredSvg = svg.replace(/fill="[^"]*"/g, `fill="${color}"`);
28
28
+
}
29
29
+
const encoded = encodeURIComponent(coloredSvg);
30
30
+
return `data:image/svg+xml,${encoded}`;
31
31
+
}
32
32
+
8
33
onMount(async () => {
9
34
if (!container) return;
10
35
36
36
+
// Use iconColor or default accent, ensure # prefix
37
37
+
const rawColor = iconColor || 'f6339a';
38
38
+
const dotColor = rawColor.startsWith('#') ? rawColor : `#${rawColor}`;
39
39
+
11
40
const module = await import('qr-code-styling');
12
41
const QRCodeStyling = module.default;
13
42
43
43
+
// Get container size for responsive QR
44
44
+
const rect = container.getBoundingClientRect();
45
45
+
const size = Math.min(rect.width, rect.height) || 280;
46
46
+
14
47
const options: ConstructorParameters<typeof QRCodeStyling>[0] = {
15
48
width: size,
16
49
height: size,
17
50
data: url,
18
51
dotsOptions: {
19
19
-
color: '#000',
52
52
+
color: dotColor,
20
53
type: 'rounded'
21
54
},
22
55
backgroundOptions: {
23
23
-
color: '#fff'
56
56
+
color: '#FFF'
24
57
},
25
58
cornersSquareOptions: {
26
26
-
type: 'extra-rounded'
59
59
+
type: 'extra-rounded',
60
60
+
color: dotColor
27
61
},
28
62
cornersDotOptions: {
29
29
-
type: 'dot'
30
30
-
}
63
63
+
type: 'dot',
64
64
+
color: dotColor
65
65
+
},
66
66
+
margin: 10
31
67
};
32
68
69
69
+
// Add icon as center image if provided (as SVG string)
70
70
+
if (icon) {
71
71
+
options.image = svgToDataUri(icon, dotColor);
72
72
+
options.imageOptions = {
73
73
+
margin: 10,
74
74
+
imageSize: 0.5
75
75
+
};
76
76
+
}
77
77
+
33
78
const qrCode = new QRCodeStyling(options);
34
79
qrCode.append(container);
35
80
});
36
81
</script>
37
82
38
38
-
<div bind:this={container} class="flex items-center justify-center"></div>
83
83
+
<div bind:this={container} class="flex items-center justify-center {className}"></div>
+11
-54
src/lib/components/qr/QRCodeModal.svelte
···
1
1
<script lang="ts">
2
2
-
import { Modal, Button, toast } from '@foxui/core';
2
2
+
import { Modal } from '@foxui/core';
3
3
import QRCodeDisplay from './QRCodeDisplay.svelte';
4
4
5
5
export type QRContext = {
6
6
title?: string;
7
7
icon?: string;
8
8
iconColor?: string;
9
9
-
favicon?: string;
10
10
-
avatar?: string;
11
9
};
12
10
13
11
let {
···
19
17
href: string;
20
18
context?: QRContext;
21
19
} = $props();
22
22
-
23
23
-
async function copyUrl() {
24
24
-
try {
25
25
-
await navigator.clipboard.writeText(href);
26
26
-
toast.success('URL copied!');
27
27
-
} catch {
28
28
-
toast.error('Failed to copy');
29
29
-
}
30
30
-
}
31
20
</script>
32
21
33
33
-
<Modal bind:open closeButton={true} class="max-w-sm">
34
34
-
<div class="flex flex-col items-center gap-4 p-2">
35
35
-
{#if context.icon}
36
36
-
<div
37
37
-
class="flex size-14 items-center justify-center rounded-2xl [&_svg]:size-8 [&_svg]:fill-white"
38
38
-
style:background-color={context.iconColor ? `#${context.iconColor}` : '#000'}
39
39
-
>
40
40
-
{@html context.icon}
41
41
-
</div>
42
42
-
{:else if context.avatar}
43
43
-
<img src={context.avatar} alt="" class="size-14 rounded-full object-cover" />
44
44
-
{:else if context.favicon}
45
45
-
<img src={context.favicon} alt="" class="size-10 rounded-lg object-cover" />
46
46
-
{/if}
47
47
-
22
22
+
<Modal bind:open closeButton={true} class="max-w-[90vw]! sm:max-w-sm! md:max-w-md!">
23
23
+
<div class="flex flex-col items-center justify-center gap-4 p-4">
48
24
{#if context.title}
49
49
-
<div class="text-base-900 dark:text-base-100 text-lg font-semibold">
25
25
+
<div class="text-base-900 dark:text-base-100 text-center text-xl font-semibold">
50
26
{context.title}
51
27
</div>
52
28
{/if}
53
29
54
54
-
<div class="overflow-hidden rounded-2xl">
55
55
-
<QRCodeDisplay url={href} size={280} />
56
56
-
</div>
57
57
-
58
58
-
<div class="flex w-full items-center gap-2">
59
59
-
<div
60
60
-
class="bg-base-100 dark:bg-base-800 text-base-600 dark:text-base-400 flex-1 truncate rounded-lg px-3 py-2 text-sm"
61
61
-
>
62
62
-
{href}
63
63
-
</div>
64
64
-
<Button onclick={copyUrl} variant="ghost" size="sm">
65
65
-
<svg
66
66
-
xmlns="http://www.w3.org/2000/svg"
67
67
-
fill="none"
68
68
-
viewBox="0 0 24 24"
69
69
-
stroke-width="1.5"
70
70
-
stroke="currentColor"
71
71
-
class="size-4"
72
72
-
>
73
73
-
<path
74
74
-
stroke-linecap="round"
75
75
-
stroke-linejoin="round"
76
76
-
d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9.75a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184"
77
77
-
/>
78
78
-
</svg>
79
79
-
</Button>
30
30
+
<div class="flex items-center justify-center overflow-hidden rounded-2xl">
31
31
+
<QRCodeDisplay
32
32
+
url={href}
33
33
+
icon={context.icon}
34
34
+
iconColor={context.iconColor}
35
35
+
class="size-[min(70vw,320px)] sm:size-72 md:size-80"
36
36
+
/>
80
37
</div>
81
38
</div>
82
39
</Modal>
+1
-2
src/lib/website/Profile.svelte
···
35
35
class="w-fit"
36
36
use:qrOverlay={{
37
37
context: {
38
38
-
title: getName(data),
39
39
-
avatar: data.profile.avatar
38
38
+
title: getName(data) + "'s blento"
40
39
}
41
40
}}
42
41
>