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