tangled
alpha
login
or
join now
flo-bit.dev
/
blento
your personal website on atproto - mirror
blento.app
16
fork
atom
overview
issues
pulls
pipelines
v0.1
jycouet
1 week ago
bc24bc8f
ce18dffd
+50
-90
5 changed files
expand all
collapse all
unified
split
src
lib
cards
BaseCard
BaseEditingCard.svelte
components
qr
QRCodeDisplay.svelte
QRCodeModal.svelte
qrOverlay.svelte.ts
website
Profile.svelte
+4
-4
src/lib/cards/BaseCard/BaseEditingCard.svelte
···
57
58
const cardDef = $derived(CardDefinitionsByType[item.cardType]);
59
60
-
const minW = $derived(cardDef?.minW ?? (isMobile() ? 2 : 2));
61
-
const minH = $derived(cardDef?.minH ?? (isMobile() ? 2 : 2));
62
63
-
const maxW = $derived(cardDef?.maxW ?? COLUMNS);
64
-
const maxH = $derived(cardDef?.maxH ?? (isMobile() ? 12 : 6));
65
66
// Resize handle state
67
let isResizing = $state(false);
···
57
58
const cardDef = $derived(CardDefinitionsByType[item.cardType]);
59
60
+
const minW = $derived(cardDef.minW ?? (isMobile() ? 2 : 2));
61
+
const minH = $derived(cardDef.minH ?? (isMobile() ? 2 : 2));
62
63
+
const maxW = $derived(cardDef.maxW ?? COLUMNS);
64
+
const maxH = $derived(cardDef.maxH ?? (isMobile() ? 12 : 6));
65
66
// Resize handle state
67
let isResizing = $state(false);
+24
-38
src/lib/components/qr/QRCodeDisplay.svelte
···
1
<script lang="ts">
2
-
import { browser } from '$app/environment';
3
4
-
let { url, size = 280, logo }: { url: string; size?: number; logo?: string } = $props();
5
6
let container: HTMLDivElement | undefined = $state();
7
8
-
$effect(() => {
9
-
if (!browser || !container) return;
10
-
11
-
const render = async () => {
12
-
const QRCodeStylingModule = await import('qr-code-styling');
13
-
const QRCodeStyling = QRCodeStylingModule.default;
14
-
15
-
container!.innerHTML = '';
16
17
-
const options: ConstructorParameters<typeof QRCodeStyling>[0] = {
18
-
width: size,
19
-
height: size,
20
-
data: url,
21
-
dotsOptions: {
22
-
color: '#000',
23
-
type: 'rounded'
24
-
},
25
-
backgroundOptions: {
26
-
color: '#fff'
27
-
},
28
-
cornersSquareOptions: {
29
-
type: 'extra-rounded'
30
-
},
31
-
cornersDotOptions: {
32
-
type: 'dot'
33
-
}
34
-
};
35
36
-
if (logo) {
37
-
options.image = logo;
38
-
options.imageOptions = {
39
-
crossOrigin: 'anonymous',
40
-
margin: 4
41
-
};
0
0
0
0
0
0
0
0
0
0
42
}
43
-
44
-
const qrCode = new QRCodeStyling(options);
45
-
qrCode.append(container!);
46
};
47
48
-
render();
0
49
});
50
</script>
51
···
1
<script lang="ts">
2
+
import { onMount } from 'svelte';
3
4
+
let { url, size = 280 }: { url: string; size?: number } = $props();
5
6
let container: HTMLDivElement | undefined = $state();
7
8
+
onMount(async () => {
9
+
if (!container) return;
0
0
0
0
0
0
10
11
+
const module = await import('qr-code-styling');
12
+
const QRCodeStyling = module.default;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
13
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'
27
+
},
28
+
cornersDotOptions: {
29
+
type: 'dot'
30
}
0
0
0
31
};
32
33
+
const qrCode = new QRCodeStyling(options);
34
+
qrCode.append(container);
35
});
36
</script>
37
+1
-3
src/lib/components/qr/QRCodeModal.svelte
···
28
toast.error('Failed to copy');
29
}
30
}
31
-
32
-
const logoUrl = $derived(context.avatar || context.favicon);
33
</script>
34
35
<Modal bind:open closeButton={true} class="max-w-sm">
···
54
{/if}
55
56
<div class="overflow-hidden rounded-2xl">
57
-
<QRCodeDisplay url={href} size={280} logo={logoUrl} />
58
</div>
59
60
<div class="flex w-full items-center gap-2">
···
28
toast.error('Failed to copy');
29
}
30
}
0
0
31
</script>
32
33
<Modal bind:open closeButton={true} class="max-w-sm">
···
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">
+9
-5
src/lib/components/qr/qrOverlay.svelte.ts
···
12
}
13
14
export function qrOverlay(
15
-
node: HTMLAnchorElement,
16
-
params: { context?: QRContext; disabled?: boolean } = {}
17
) {
18
const LONG_PRESS_DURATION = 500;
19
let longPressTimer: ReturnType<typeof setTimeout> | null = null;
20
let isLongPress = false;
21
0
0
0
0
22
function startLongPress() {
23
if (params.disabled) return;
24
isLongPress = false;
25
longPressTimer = setTimeout(() => {
26
isLongPress = true;
27
-
openModal?.(node.href, params.context ?? {});
28
}, LONG_PRESS_DURATION);
29
}
30
···
45
function handleContextMenu(e: MouseEvent) {
46
if (params.disabled) return;
47
e.preventDefault();
48
-
openModal?.(node.href, params.context ?? {});
49
}
50
51
node.addEventListener('pointerdown', startLongPress);
···
56
node.addEventListener('contextmenu', handleContextMenu);
57
58
return {
59
-
update(newParams: { context?: QRContext; disabled?: boolean }) {
60
params = newParams;
61
},
62
destroy() {
···
12
}
13
14
export function qrOverlay(
15
+
node: HTMLElement,
16
+
params: { href?: string; context?: QRContext; disabled?: boolean } = {}
17
) {
18
const LONG_PRESS_DURATION = 500;
19
let longPressTimer: ReturnType<typeof setTimeout> | null = null;
20
let isLongPress = false;
21
22
+
function getHref() {
23
+
return params.href || (node as HTMLAnchorElement).href || '';
24
+
}
25
+
26
function startLongPress() {
27
if (params.disabled) return;
28
isLongPress = false;
29
longPressTimer = setTimeout(() => {
30
isLongPress = true;
31
+
openModal?.(getHref(), params.context ?? {});
32
}, LONG_PRESS_DURATION);
33
}
34
···
49
function handleContextMenu(e: MouseEvent) {
50
if (params.disabled) return;
51
e.preventDefault();
52
+
openModal?.(getHref(), params.context ?? {});
53
}
54
55
node.addEventListener('pointerdown', startLongPress);
···
60
node.addEventListener('contextmenu', handleContextMenu);
61
62
return {
63
+
update(newParams: { href?: string; context?: QRContext; disabled?: boolean }) {
64
params = newParams;
65
},
66
destroy() {
+12
-40
src/lib/website/Profile.svelte
···
8
import { getDescription, getName } from '$lib/helper';
9
import { page } from '$app/state';
10
import type { ActorIdentifier } from '@atcute/lexicons';
11
-
import QRCodeModal from '$lib/components/qr/QRCodeModal.svelte';
12
13
let {
14
data,
···
22
renderer.link = ({ href, title, text }) =>
23
`<a target="_blank" href="${href}" title="${title}">${text}</a>`;
24
25
-
let qrOpen = $state(false);
26
-
let longPressTimer: ReturnType<typeof setTimeout> | null = null;
27
-
28
-
const profileUrl = $derived(`${page.url}/${data.handle}`);
29
-
30
-
function startLongPress() {
31
-
longPressTimer = setTimeout(() => {
32
-
qrOpen = true;
33
-
}, 500);
34
-
}
35
-
36
-
function cancelLongPress() {
37
-
if (longPressTimer) {
38
-
clearTimeout(longPressTimer);
39
-
longPressTimer = null;
40
-
}
41
-
}
42
-
43
-
function handleContextMenu(e: MouseEvent) {
44
-
e.preventDefault();
45
-
qrOpen = true;
46
-
}
47
</script>
48
49
<!-- lg:fixed lg:h-screen lg:w-1/4 lg:max-w-none lg:px-12 lg:pt-24 xl:w-1/3 -->
···
51
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"
52
>
53
<div class="flex flex-col gap-4 pt-16 pb-8 @5xl/wrapper:h-screen @5xl/wrapper:pt-24">
54
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
55
-
<div
56
-
class="w-fit cursor-pointer"
57
-
onpointerdown={startLongPress}
58
-
onpointerup={cancelLongPress}
59
-
onpointercancel={cancelLongPress}
60
-
onpointerleave={cancelLongPress}
61
-
oncontextmenu={handleContextMenu}
0
62
>
63
{#if data.profile.avatar}
64
<img
···
69
{:else}
70
<div class="bg-base-300 dark:bg-base-700 size-32 rounded-full @5xl/wrapper:size-44"></div>
71
{/if}
72
-
</div>
73
-
<QRCodeModal
74
-
bind:open={qrOpen}
75
-
href={profileUrl}
76
-
context={{
77
-
title: getName(data),
78
-
avatar: data.profile.avatar
79
-
}}
80
-
/>
81
82
<div class="text-4xl font-bold wrap-anywhere">
83
{getName(data)}
···
8
import { getDescription, getName } from '$lib/helper';
9
import { page } from '$app/state';
10
import type { ActorIdentifier } from '@atcute/lexicons';
11
+
import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte';
12
13
let {
14
data,
···
22
renderer.link = ({ href, title, text }) =>
23
`<a target="_blank" href="${href}" title="${title}">${text}</a>`;
24
25
+
const profileUrl = $derived(`${page.url.origin}/${data.handle}`);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
26
</script>
27
28
<!-- lg:fixed lg:h-screen lg:w-1/4 lg:max-w-none lg:px-12 lg:pt-24 xl:w-1/3 -->
···
30
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"
31
>
32
<div class="flex flex-col gap-4 pt-16 pb-8 @5xl/wrapper:h-screen @5xl/wrapper:pt-24">
33
+
<a
34
+
href={profileUrl}
35
+
class="w-fit"
36
+
use:qrOverlay={{
37
+
context: {
38
+
title: getName(data),
39
+
avatar: data.profile.avatar
40
+
}
41
+
}}
42
>
43
{#if data.profile.avatar}
44
<img
···
49
{:else}
50
<div class="bg-base-300 dark:bg-base-700 size-32 rounded-full @5xl/wrapper:size-44"></div>
51
{/if}
52
+
</a>
0
0
0
0
0
0
0
0
53
54
<div class="text-4xl font-bold wrap-anywhere">
55
{getName(data)}