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
jycouet
2 weeks ago
42e3b83a
890d1bf9
+369
-27
15 changed files
expand all
collapse all
unified
split
package.json
pnpm-lock.yaml
src
lib
cards
BaseCard
BaseEditingCard.svelte
BigSocialCard
BigSocialCard.svelte
BlueskyProfileCard
BlueskyProfileCard.svelte
GitHubProfileCard
GitHubProfileCard.svelte
ImageCard
ImageCard.svelte
LinkCard
LinkCard.svelte
MapCard
MapCard.svelte
components
qr
QRCodeDisplay.svelte
QRCodeModal.svelte
QRModalProvider.svelte
qrOverlay.svelte.ts
website
Profile.svelte
Website.svelte
+1
package.json
···
73
"mapbox-gl": "^3.18.1",
74
"marked": "^17.0.1",
75
"plyr": "^3.8.4",
0
76
"simple-icons": "^16.6.0",
77
"svelte-sonner": "^1.0.7",
78
"tailwind-merge": "^3.4.0",
···
73
"mapbox-gl": "^3.18.1",
74
"marked": "^17.0.1",
75
"plyr": "^3.8.4",
76
+
"qr-code-styling": "^1.8.6",
77
"simple-icons": "^16.6.0",
78
"svelte-sonner": "^1.0.7",
79
"tailwind-merge": "^3.4.0",
+16
pnpm-lock.yaml
···
110
plyr:
111
specifier: ^3.8.4
112
version: 3.8.4
0
0
0
113
simple-icons:
114
specifier: ^16.6.0
115
version: 16.6.0
···
2484
punycode@2.3.1:
2485
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
2486
engines: {node: '>=6'}
0
0
0
0
0
0
0
2487
2488
quickselect@3.0.0:
2489
resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==}
···
5047
punycode.js@2.3.1: {}
5048
5049
punycode@2.3.1: {}
0
0
0
0
0
0
5050
5051
quickselect@3.0.0: {}
5052
···
110
plyr:
111
specifier: ^3.8.4
112
version: 3.8.4
113
+
qr-code-styling:
114
+
specifier: ^1.8.6
115
+
version: 1.9.2
116
simple-icons:
117
specifier: ^16.6.0
118
version: 16.6.0
···
2487
punycode@2.3.1:
2488
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
2489
engines: {node: '>=6'}
2490
+
2491
+
qr-code-styling@1.9.2:
2492
+
resolution: {integrity: sha512-RgJaZJ1/RrXJ6N0j7a+pdw3zMBmzZU4VN2dtAZf8ZggCfRB5stEQ3IoDNGaNhYY3nnZKYlYSLl5YkfWN5dPutg==}
2493
+
engines: {node: '>=18.18.0'}
2494
+
2495
+
qrcode-generator@1.5.2:
2496
+
resolution: {integrity: sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw==}
2497
2498
quickselect@3.0.0:
2499
resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==}
···
5057
punycode.js@2.3.1: {}
5058
5059
punycode@2.3.1: {}
5060
+
5061
+
qr-code-styling@1.9.2:
5062
+
dependencies:
5063
+
qrcode-generator: 1.5.2
5064
+
5065
+
qrcode-generator@1.5.2: {}
5066
5067
quickselect@3.0.0: {}
5068
+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);
+16
-3
src/lib/cards/BigSocialCard/BigSocialCard.svelte
···
1
<script lang="ts">
2
import { platformsData } from '.';
3
import type { ContentComponentProps } from '../types';
0
4
5
let { item, isEditing }: ContentComponentProps = $props();
6
7
const platform = $derived(item.cardData.platform as string);
0
8
</script>
9
10
<div
···
14
<div
15
class="flex aspect-square max-h-full max-w-full items-center justify-center [&_svg]:size-full [&_svg]:max-w-60 [&_svg]:fill-white"
16
>
17
-
{@html platformsData[platform].svg}
18
</div>
19
</div>
20
21
{#if !isEditing}
22
-
<a href={item.cardData.href} target="_blank" rel="noopener noreferrer">
0
0
0
0
0
0
0
0
0
0
0
23
<div class="absolute inset-0 z-50"></div>
24
-
<span class="sr-only">open {platformsData[platform].title}</span>
25
</a>
26
{/if}
···
1
<script lang="ts">
2
import { platformsData } from '.';
3
import type { ContentComponentProps } from '../types';
4
+
import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte';
5
6
let { item, isEditing }: ContentComponentProps = $props();
7
8
const platform = $derived(item.cardData.platform as string);
9
+
const platformData = $derived(platformsData[platform]);
10
</script>
11
12
<div
···
16
<div
17
class="flex aspect-square max-h-full max-w-full items-center justify-center [&_svg]:size-full [&_svg]:max-w-60 [&_svg]:fill-white"
18
>
19
+
{@html platformData?.svg}
20
</div>
21
</div>
22
23
{#if !isEditing}
24
+
<a
25
+
href={item.cardData.href}
26
+
target="_blank"
27
+
rel="noopener noreferrer"
28
+
use:qrOverlay={{
29
+
context: {
30
+
title: platformData?.title,
31
+
icon: platformData?.svg,
32
+
iconColor: platformData?.hex
33
+
}
34
+
}}
35
+
>
36
<div class="absolute inset-0 z-50"></div>
37
+
<span class="sr-only">open {platformData?.title}</span>
38
</a>
39
{/if}
+13
-3
src/lib/cards/BlueskyProfileCard/BlueskyProfileCard.svelte
···
1
<script lang="ts">
2
-
import type { Item } from '$lib/types';
0
3
4
-
let { item }: { item: Item } = $props();
0
0
5
</script>
6
7
<a
8
target="_blank"
9
-
href="/{item.cardData.handle}"
10
class="flex h-full w-full flex-col items-center justify-center gap-2 rounded-xl p-2 transition-colors duration-150"
0
0
0
0
0
0
0
11
>
12
<img
13
src={item.cardData.avatar}
···
1
<script lang="ts">
2
+
import type { ContentComponentProps } from '../types';
3
+
import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte';
4
5
+
let { item, isEditing }: ContentComponentProps = $props();
6
+
7
+
const profileUrl = $derived(`https://bsky.app/profile/${item.cardData.handle}`);
8
</script>
9
10
<a
11
target="_blank"
12
+
href={profileUrl}
13
class="flex h-full w-full flex-col items-center justify-center gap-2 rounded-xl p-2 transition-colors duration-150"
14
+
use:qrOverlay={{
15
+
disabled: isEditing,
16
+
context: {
17
+
title: item.cardData.displayName || item.cardData.handle,
18
+
avatar: item.cardData.avatar
19
+
}
20
+
}}
21
>
22
<img
23
src={item.cardData.avatar}
+14
-4
src/lib/cards/GitHubProfileCard/GitHubProfileCard.svelte
···
7
import GithubContributionsGraph from './GithubContributionsGraph.svelte';
8
import { Button } from '@foxui/core';
9
import { browser } from '$app/environment';
0
0
0
10
11
-
let { item }: ContentComponentProps = $props();
12
13
const data = getAdditionalUserData();
14
···
75
</div>
76
</div>
77
78
-
{#if item.cardData.href}
79
<a
80
-
href={item.cardData.href}
81
class="absolute inset-0 h-full w-full"
82
target="_blank"
83
rel="noopener noreferrer"
0
0
0
0
0
0
0
84
>
85
-
<span class="sr-only"> Show on github </span>
86
</a>
87
{/if}
···
7
import GithubContributionsGraph from './GithubContributionsGraph.svelte';
8
import { Button } from '@foxui/core';
9
import { browser } from '$app/environment';
10
+
import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte';
11
+
12
+
let { item, isEditing }: ContentComponentProps = $props();
13
14
+
const githubUrl = $derived(`https://github.com/${item.cardData.user}`);
15
16
const data = getAdditionalUserData();
17
···
78
</div>
79
</div>
80
81
+
{#if (item.cardData.href || item.cardData.user) && !isEditing}
82
<a
83
+
href={item.cardData.href || githubUrl}
84
class="absolute inset-0 h-full w-full"
85
target="_blank"
86
rel="noopener noreferrer"
87
+
use:qrOverlay={{
88
+
context: {
89
+
title: item.cardData.user,
90
+
icon: siGithub.svg,
91
+
iconColor: siGithub.hex
92
+
}
93
+
}}
94
>
95
+
<span class="sr-only">Show on github</span>
96
</a>
97
{/if}
+2
src/lib/cards/ImageCard/ImageCard.svelte
···
2
import { getDidContext } from '$lib/website/context';
3
import { getImageBlobUrl } from '$lib/atproto';
4
import type { ContentComponentProps } from '../types';
0
5
6
let { item = $bindable(), isEditing }: ContentComponentProps = $props();
7
···
33
class="absolute inset-0 z-50 h-full w-full"
34
target="_blank"
35
rel="noopener noreferrer"
0
36
>
37
<span class="sr-only">
38
{item.cardData.hrefText ?? 'Learn more'}
···
2
import { getDidContext } from '$lib/website/context';
3
import { getImageBlobUrl } from '$lib/atproto';
4
import type { ContentComponentProps } from '../types';
5
+
import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte';
6
7
let { item = $bindable(), isEditing }: ContentComponentProps = $props();
8
···
34
class="absolute inset-0 z-50 h-full w-full"
35
target="_blank"
36
rel="noopener noreferrer"
37
+
use:qrOverlay={{ context: { title: item.cardData.hrefText ?? 'Learn more' } }}
38
>
39
<span class="sr-only">
40
{item.cardData.hrefText ?? 'Learn more'}
+9
-2
src/lib/cards/LinkCard/LinkCard.svelte
···
2
import { browser } from '$app/environment';
3
import { getIsMobile } from '$lib/website/context';
4
import type { ContentComponentProps } from '../types';
0
5
6
-
let { item }: ContentComponentProps = $props();
7
8
let isMobile = getIsMobile();
9
···
62
alt=""
63
/>
64
{/if}
65
-
{#if item.cardData.href}
66
<a
67
href={item.cardData.href}
68
class="absolute inset-0 h-full w-full"
69
target="_blank"
70
rel="noopener noreferrer"
0
0
0
0
0
0
71
>
72
<span class="sr-only">
73
{item.cardData.hrefText ?? 'Learn more'}
···
2
import { browser } from '$app/environment';
3
import { getIsMobile } from '$lib/website/context';
4
import type { ContentComponentProps } from '../types';
5
+
import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte';
6
7
+
let { item, isEditing }: ContentComponentProps = $props();
8
9
let isMobile = getIsMobile();
10
···
63
alt=""
64
/>
65
{/if}
66
+
{#if item.cardData.href && !isEditing}
67
<a
68
href={item.cardData.href}
69
class="absolute inset-0 h-full w-full"
70
target="_blank"
71
rel="noopener noreferrer"
72
+
use:qrOverlay={{
73
+
context: {
74
+
title: item.cardData.title,
75
+
favicon: item.cardData.favicon
76
+
}
77
+
}}
78
>
79
<span class="sr-only">
80
{item.cardData.hrefText ?? 'Learn more'}
+8
-2
src/lib/cards/MapCard/MapCard.svelte
···
1
<script lang="ts">
2
import type { ContentComponentProps } from '../types';
3
import Map from './Map.svelte';
0
4
5
let { item = $bindable(), isEditing }: ContentComponentProps = $props();
0
0
0
0
0
6
</script>
7
8
<Map bind:item />
···
11
<a
12
target="_blank"
13
rel="noopener noreferrer"
14
-
href={'http://maps.google.com/maps?q=' +
15
-
encodeURIComponent(item.cardData.lat + ',' + item.cardData.lon)}
16
>
17
<div class="absolute inset-0 z-100"></div>
18
<span class="sr-only">open map</span>
···
1
<script lang="ts">
2
import type { ContentComponentProps } from '../types';
3
import Map from './Map.svelte';
4
+
import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte';
5
6
let { item = $bindable(), isEditing }: ContentComponentProps = $props();
7
+
8
+
const mapsUrl = $derived(
9
+
'https://maps.google.com/maps?q=' +
10
+
encodeURIComponent(item.cardData.lat + ',' + item.cardData.lon)
11
+
);
12
</script>
13
14
<Map bind:item />
···
17
<a
18
target="_blank"
19
rel="noopener noreferrer"
20
+
href={mapsUrl}
21
+
use:qrOverlay={{ context: { title: 'Google Maps' } }}
22
>
23
<div class="absolute inset-0 z-100"></div>
24
<span class="sr-only">open map</span>
+52
src/lib/components/qr/QRCodeDisplay.svelte
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
+
};
42
+
}
43
+
44
+
const qrCode = new QRCodeStyling(options);
45
+
qrCode.append(container!);
46
+
};
47
+
48
+
render();
49
+
});
50
+
</script>
51
+
52
+
<div bind:this={container} class="flex items-center justify-center"></div>
+84
src/lib/components/qr/QRCodeModal.svelte
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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 {
14
+
open = $bindable(false),
15
+
href,
16
+
context = {}
17
+
}: {
18
+
open: boolean;
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
+
32
+
const logoUrl = $derived(context.avatar || context.favicon);
33
+
</script>
34
+
35
+
<Modal bind:open closeButton={true} class="max-w-sm">
36
+
<div class="flex flex-col items-center gap-4 p-2">
37
+
{#if context.icon}
38
+
<div
39
+
class="flex size-14 items-center justify-center rounded-2xl [&_svg]:size-8 [&_svg]:fill-white"
40
+
style:background-color={context.iconColor ? `#${context.iconColor}` : '#000'}
41
+
>
42
+
{@html context.icon}
43
+
</div>
44
+
{:else if context.avatar}
45
+
<img src={context.avatar} alt="" class="size-14 rounded-full object-cover" />
46
+
{:else if context.favicon}
47
+
<img src={context.favicon} alt="" class="size-10 rounded-lg object-cover" />
48
+
{/if}
49
+
50
+
{#if context.title}
51
+
<div class="text-base-900 dark:text-base-100 text-lg font-semibold">
52
+
{context.title}
53
+
</div>
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">
61
+
<div
62
+
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"
63
+
>
64
+
{href}
65
+
</div>
66
+
<Button onclick={copyUrl} variant="ghost" size="sm">
67
+
<svg
68
+
xmlns="http://www.w3.org/2000/svg"
69
+
fill="none"
70
+
viewBox="0 0 24 24"
71
+
stroke-width="1.5"
72
+
stroke="currentColor"
73
+
class="size-4"
74
+
>
75
+
<path
76
+
stroke-linecap="round"
77
+
stroke-linejoin="round"
78
+
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"
79
+
/>
80
+
</svg>
81
+
</Button>
82
+
</div>
83
+
</div>
84
+
</Modal>
+25
src/lib/components/qr/QRModalProvider.svelte
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
<script lang="ts">
2
+
import { onMount, onDestroy } from 'svelte';
3
+
import QRCodeModal, { type QRContext } from './QRCodeModal.svelte';
4
+
import { registerQRModal, unregisterQRModal } from './qrOverlay.svelte';
5
+
6
+
let open = $state(false);
7
+
let href = $state('');
8
+
let context = $state<QRContext>({});
9
+
10
+
function showModal(newHref: string, newContext: QRContext) {
11
+
href = newHref;
12
+
context = newContext;
13
+
open = true;
14
+
}
15
+
16
+
onMount(() => {
17
+
registerQRModal(showModal);
18
+
});
19
+
20
+
onDestroy(() => {
21
+
unregisterQRModal();
22
+
});
23
+
</script>
24
+
25
+
<QRCodeModal bind:open {href} {context} />
+72
src/lib/components/qr/qrOverlay.svelte.ts
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import type { QRContext } from './QRCodeModal.svelte';
2
+
3
+
// Global state for QR modal
4
+
let openModal: ((href: string, context: QRContext) => void) | null = null;
5
+
6
+
export function registerQRModal(fn: (href: string, context: QRContext) => void) {
7
+
openModal = fn;
8
+
}
9
+
10
+
export function unregisterQRModal() {
11
+
openModal = null;
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
+
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
+
31
+
function cancelLongPress() {
32
+
if (longPressTimer) {
33
+
clearTimeout(longPressTimer);
34
+
longPressTimer = null;
35
+
}
36
+
}
37
+
38
+
function handleClick(e: MouseEvent) {
39
+
if (isLongPress) {
40
+
e.preventDefault();
41
+
isLongPress = false;
42
+
}
43
+
}
44
+
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);
52
+
node.addEventListener('pointerup', cancelLongPress);
53
+
node.addEventListener('pointercancel', cancelLongPress);
54
+
node.addEventListener('pointerleave', cancelLongPress);
55
+
node.addEventListener('click', handleClick);
56
+
node.addEventListener('contextmenu', handleContextMenu);
57
+
58
+
return {
59
+
update(newParams: { context?: QRContext; disabled?: boolean }) {
60
+
params = newParams;
61
+
},
62
+
destroy() {
63
+
node.removeEventListener('pointerdown', startLongPress);
64
+
node.removeEventListener('pointerup', cancelLongPress);
65
+
node.removeEventListener('pointercancel', cancelLongPress);
66
+
node.removeEventListener('pointerleave', cancelLongPress);
67
+
node.removeEventListener('click', handleClick);
68
+
node.removeEventListener('contextmenu', handleContextMenu);
69
+
cancelLongPress();
70
+
}
71
+
};
72
+
}
+51
-9
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';
0
11
12
let {
13
data,
···
20
const renderer = new marked.Renderer();
21
renderer.link = ({ href, title, text }) =>
22
`<a target="_blank" href="${href}" title="${title}">${text}</a>`;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
23
</script>
24
25
<!-- lg:fixed lg:h-screen lg:w-1/4 lg:max-w-none lg:px-12 lg:pt-24 xl:w-1/3 -->
···
27
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"
28
>
29
<div class="flex flex-col gap-4 pt-16 pb-8 @5xl/wrapper:h-screen @5xl/wrapper:pt-24">
30
-
{#if data.profile.avatar}
31
-
<img
32
-
class="border-base-400 dark:border-base-800 size-32 rounded-full border @5xl/wrapper:size-44"
33
-
src={data.profile.avatar}
34
-
alt=""
35
-
/>
36
-
{:else}
37
-
<div class="bg-base-300 dark:bg-base-700 size-32 rounded-full @5xl/wrapper:size-44"></div>
38
-
{/if}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
39
40
<div class="text-4xl font-bold wrap-anywhere">
41
{getName(data)}
···
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,
···
21
const renderer = new marked.Renderer();
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}
62
+
>
63
+
{#if data.profile.avatar}
64
+
<img
65
+
class="border-base-400 dark:border-base-800 size-32 rounded-full border @5xl/wrapper:size-44"
66
+
src={data.profile.avatar}
67
+
alt=""
68
+
/>
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)}
+2
src/lib/website/Website.svelte
···
9
import Context from './Context.svelte';
10
import Head from './Head.svelte';
11
import type { Did, Handle } from '@atcute/lexicons';
0
12
13
let { data }: { data: WebsiteData } = $props();
14
···
38
/>
39
40
<Context {data}>
0
41
<div class="@container/wrapper relative w-full">
42
{#if !getHideProfileSection(data)}
43
<Profile {data} showEditButton={true} />
···
9
import Context from './Context.svelte';
10
import Head from './Head.svelte';
11
import type { Did, Handle } from '@atcute/lexicons';
12
+
import QRModalProvider from '$lib/components/qr/QRModalProvider.svelte';
13
14
let { data }: { data: WebsiteData } = $props();
15
···
39
/>
40
41
<Context {data}>
42
+
<QRModalProvider />
43
<div class="@container/wrapper relative w-full">
44
{#if !getHideProfileSection(data)}
45
<Profile {data} showEditButton={true} />