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
improve link adding
Florian
3 weeks ago
604ac7af
0faab9c0
+361
-205
12 changed files
expand all
collapse all
unified
split
src
lib
cards
BaseCard
BaseEditingCard.svelte
BigSocialCard
index.ts
LinkCard
CreateLinkCardModal.svelte
EditingLinkCard.svelte
LinkCard.svelte
LinkCardSettings.svelte
index.ts
TextCard
TextCardSettings.svelte
types.ts
utils
PlainTextEditor.svelte
helper.ts
website
EditableWebsite.svelte
+16
-5
src/lib/cards/BaseCard/BaseEditingCard.svelte
···
136
136
if (!cardDef) return false;
137
137
138
138
if (isMobile()) {
139
139
-
140
140
-
return w >= minW && w*2 <= maxW && h >= minH && h*2 <= maxH;
139
139
+
return w >= minW && w * 2 <= maxW && h >= minH && h * 2 <= maxH;
141
140
}
142
141
143
142
return w >= minW && w <= maxW && h >= minH && h <= maxH;
···
154
153
let settingsPopoverOpen = $state(false);
155
154
</script>
156
155
157
157
-
<BaseCard {item} isEditing={true} bind:ref showOutline={isResizing} class="starting:scale-0 scale-100 starting:opacity-0 opacity-100" {...rest} >
156
156
+
<BaseCard
157
157
+
{item}
158
158
+
isEditing={true}
159
159
+
bind:ref
160
160
+
showOutline={isResizing}
161
161
+
class="scale-100 opacity-100 starting:scale-0 starting:opacity-0"
162
162
+
{...rest}
163
163
+
>
158
164
{@render children?.()}
159
165
160
166
{#snippet controls()}
···
283
289
{/if}
284
290
285
291
{#if cardDef.settingsComponent}
286
286
-
<Popover bind:open={settingsPopoverOpen}>
292
292
+
<Popover bind:open={settingsPopoverOpen} class="bg-base-50 dark:bg-base-900">
287
293
{#snippet child({ props })}
288
294
<button {...props} class="hover:bg-accent-500/10 cursor-pointer rounded-xl p-2">
289
295
<svg
···
307
313
</svg>
308
314
</button>
309
315
{/snippet}
310
310
-
<cardDef.settingsComponent bind:item />
316
316
+
<cardDef.settingsComponent
317
317
+
bind:item
318
318
+
onclose={() => {
319
319
+
settingsPopoverOpen = false;
320
320
+
}}
321
321
+
/>
311
322
</Popover>
312
323
{/if}
313
324
</div>
+2
-1
src/lib/cards/BigSocialCard/index.ts
···
32
32
item.cardData.href = url;
33
33
34
34
return item;
35
35
-
}
35
35
+
},
36
36
+
urlHandlerPriority: 1
36
37
} as CardDefinition & { type: 'bigsocial' };
37
38
38
39
import {
-59
src/lib/cards/LinkCard/CreateLinkCardModal.svelte
···
1
1
-
<script lang="ts">
2
2
-
import { Alert, Button, Input, Modal, Subheading } from '@foxui/core';
3
3
-
import type { CreationModalComponentProps } from '../types';
4
4
-
5
5
-
let { item = $bindable(), oncreate, oncancel }: CreationModalComponentProps = $props();
6
6
-
7
7
-
let isFetchingMetadata = $state(false);
8
8
-
9
9
-
let errorMessage = $state('');
10
10
-
11
11
-
async function fetchMetadata() {
12
12
-
errorMessage = '';
13
13
-
try {
14
14
-
item.cardData.domain = new URL(item.cardData.href).hostname;
15
15
-
} catch (error) {
16
16
-
errorMessage = 'Invalid URL!';
17
17
-
return false;
18
18
-
}
19
19
-
isFetchingMetadata = true;
20
20
-
21
21
-
try {
22
22
-
const response = await fetch('/api/links?link=' + encodeURIComponent(item.cardData.href));
23
23
-
if (response.ok) {
24
24
-
const data = await response.json();
25
25
-
item.cardData.description = data.description || '';
26
26
-
item.cardData.title = data.title || '';
27
27
-
item.cardData.image = data.images?.[0] || '';
28
28
-
item.cardData.favicon = data.favicons?.[0] || undefined;
29
29
-
} else {
30
30
-
throw new Error();
31
31
-
}
32
32
-
} catch (error) {
33
33
-
errorMessage = "Couldn't fetch metadata for this link!";
34
34
-
return false;
35
35
-
} finally {
36
36
-
isFetchingMetadata = false;
37
37
-
}
38
38
-
return true;
39
39
-
}
40
40
-
</script>
41
41
-
42
42
-
<Modal open={true} closeButton={false}>
43
43
-
<Subheading>Enter a link</Subheading>
44
44
-
<Input bind:value={item.cardData.href} />
45
45
-
46
46
-
{#if errorMessage}
47
47
-
<Alert type="error" title="Failed to create link card"><span>{errorMessage}</span></Alert>
48
48
-
{/if}
49
49
-
50
50
-
<div class="mt-4 flex justify-end gap-2">
51
51
-
<Button onclick={oncancel} variant="ghost">Cancel</Button>
52
52
-
<Button
53
53
-
disabled={isFetchingMetadata}
54
54
-
onclick={async () => {
55
55
-
if (await fetchMetadata()) oncreate();
56
56
-
}}>{isFetchingMetadata ? 'Creating...' : 'Create'}</Button
57
57
-
>
58
58
-
</div>
59
59
-
</Modal>
+99
-38
src/lib/cards/LinkCard/EditingLinkCard.svelte
···
3
3
import { getIsMobile } from '$lib/website/context';
4
4
import type { ContentComponentProps } from '../types';
5
5
import PlainTextEditor from '../utils/PlainTextEditor.svelte';
6
6
+
import { onMount } from 'svelte';
6
7
7
8
let { item = $bindable() }: ContentComponentProps = $props();
8
9
9
10
let isMobile = getIsMobile();
10
11
11
12
let faviconHasError = $state(false);
13
13
+
let isFetchingMetadata = $state(false);
14
14
+
15
15
+
let hasFetched = $derived(item.cardData.hasFetched !== false);
16
16
+
17
17
+
async function fetchMetadata() {
18
18
+
let domain: string;
19
19
+
try {
20
20
+
domain = new URL(item.cardData.href).hostname;
21
21
+
} catch (error) {
22
22
+
return;
23
23
+
}
24
24
+
item.cardData.domain = domain;
25
25
+
faviconHasError = false;
26
26
+
27
27
+
try {
28
28
+
const response = await fetch('/api/links?link=' + encodeURIComponent(item.cardData.href));
29
29
+
if (!response.ok) {
30
30
+
throw new Error();
31
31
+
}
32
32
+
const data = await response.json();
33
33
+
item.cardData.description = data.description || '';
34
34
+
item.cardData.title = data.title || '';
35
35
+
item.cardData.image = data.images?.[0] || '';
36
36
+
item.cardData.favicon = data.favicons?.[0] || undefined;
37
37
+
} catch (error) {
38
38
+
return;
39
39
+
}
40
40
+
}
41
41
+
42
42
+
$effect(() => {
43
43
+
if (hasFetched !== false || isFetchingMetadata) {
44
44
+
return;
45
45
+
}
46
46
+
47
47
+
isFetchingMetadata = true;
48
48
+
49
49
+
fetchMetadata().then(() => {
50
50
+
item.cardData.hasFetched = true;
51
51
+
isFetchingMetadata = false;
52
52
+
});
53
53
+
});
12
54
</script>
13
55
14
14
-
<div class="flex h-full flex-col justify-between p-4">
15
15
-
<div>
16
16
-
{#if item.cardData.favicon}
17
17
-
<div
18
18
-
class="bg-base-100 border-base-300 dark:border-base-800 dark:bg-base-900 mb-2 inline-flex size-8 items-center justify-center rounded-xl border shadow-sm"
19
19
-
>
20
20
-
{#if !faviconHasError}
21
21
-
<img
22
22
-
class="size-6 rounded-lg object-cover"
23
23
-
onerror={() => (faviconHasError = true)}
24
24
-
src={item.cardData.favicon}
25
25
-
alt=""
56
56
+
<div class="relative flex h-full flex-col justify-between p-4">
57
57
+
<div
58
58
+
class={[
59
59
+
'accent:bg-accent-500/50 absolute inset-0 z-20 bg-white/50 dark:bg-black/50',
60
60
+
!hasFetched ? 'animate-pulse' : 'hidden'
61
61
+
]}
62
62
+
></div>
63
63
+
64
64
+
<div class={isFetchingMetadata ? 'pointer-events-none' : ''}>
65
65
+
<div
66
66
+
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"
67
67
+
>
68
68
+
{#if hasFetched && item.cardData.favicon && !faviconHasError}
69
69
+
<img
70
70
+
class="size-6 rounded-lg object-cover"
71
71
+
onerror={() => (faviconHasError = true)}
72
72
+
src={item.cardData.favicon}
73
73
+
alt=""
74
74
+
/>
75
75
+
{:else}
76
76
+
<svg
77
77
+
xmlns="http://www.w3.org/2000/svg"
78
78
+
fill="none"
79
79
+
viewBox="0 0 24 24"
80
80
+
stroke-width="1.5"
81
81
+
stroke="currentColor"
82
82
+
class="size-4"
83
83
+
>
84
84
+
<path
85
85
+
stroke-linecap="round"
86
86
+
stroke-linejoin="round"
87
87
+
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 1 1.242 7.244"
26
88
/>
27
27
-
{:else}
28
28
-
<svg
29
29
-
xmlns="http://www.w3.org/2000/svg"
30
30
-
fill="none"
31
31
-
viewBox="0 0 24 24"
32
32
-
stroke-width="1.5"
33
33
-
stroke="currentColor"
34
34
-
class="size-4"
35
35
-
>
36
36
-
<path
37
37
-
stroke-linecap="round"
38
38
-
stroke-linejoin="round"
39
39
-
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
40
40
-
/>
41
41
-
</svg>
42
42
-
{/if}
43
43
-
</div>
44
44
-
{/if}
89
89
+
</svg>
90
90
+
{/if}
91
91
+
</div>
45
92
46
93
<div
47
47
-
class="hover:bg-base-200/70 dark:hover:bg-base-800/70 accent:hover:bg-accent-400 -m-1 rounded-md p-1 transition-colors duration-200"
94
94
+
class={[
95
95
+
'-m-1 rounded-md p-1 transition-colors duration-200',
96
96
+
hasFetched
97
97
+
? 'hover:bg-base-200/70 dark:hover:bg-base-800/70 accent:hover:bg-accent-200/30'
98
98
+
: ''
99
99
+
]}
48
100
>
49
49
-
<PlainTextEditor
50
50
-
class="text-base-900 dark:text-base-50 text-lg font-bold"
51
51
-
key="title"
52
52
-
bind:item
53
53
-
/>
101
101
+
{#if hasFetched}
102
102
+
<PlainTextEditor
103
103
+
class="text-base-900 dark:text-base-50 line-clamp-2 text-lg font-bold"
104
104
+
key="title"
105
105
+
bind:item
106
106
+
placeholder="Title here"
107
107
+
/>
108
108
+
{:else}
109
109
+
<span class={'text-base-900 dark:text-base-50 line-clamp-2 text-lg font-bold'}>
110
110
+
Loading data...
111
111
+
</span>
112
112
+
{/if}
54
113
</div>
55
114
<!-- <div class="text-base-800 dark:text-base-100 mt-2 text-xs">{item.cardData.description}</div> -->
56
56
-
<div class="text-accent-600 accent:text-accent-950 font-semibold dark:text-accent-400 mt-2 text-xs">
115
115
+
<div
116
116
+
class="text-accent-600 accent:text-accent-950 dark:text-accent-400 mt-2 text-xs font-semibold"
117
117
+
>
57
118
{item.cardData.domain}
58
119
</div>
59
120
</div>
60
121
61
61
-
{#if browser && ((isMobile() && item.mobileH >= 8) || (!isMobile() && item.h >= 4)) && item.cardData.image}
122
122
+
{#if hasFetched && browser && ((isMobile() && item.mobileH >= 8) || (!isMobile() && item.h >= 4)) && item.cardData.image}
62
123
<img class=" mb-2 max-h-32 w-full rounded-xl object-cover" src={item.cardData.image} alt="" />
63
124
{/if}
64
125
</div>
+31
-29
src/lib/cards/LinkCard/LinkCard.svelte
···
12
12
13
13
<div class="flex h-full flex-col justify-between p-4">
14
14
<div>
15
15
-
{#if item.cardData.favicon}
16
16
-
<div
17
17
-
class="bg-base-100 border-base-300 dark:border-base-800 dark:bg-base-900 mb-2 inline-flex size-8 items-center justify-center rounded-xl border shadow-sm"
18
18
-
>
19
19
-
{#if !faviconHasError}
20
20
-
<img
21
21
-
class="size-6 rounded-lg object-cover"
22
22
-
onerror={() => (faviconHasError = true)}
23
23
-
src={item.cardData.favicon}
24
24
-
alt=""
15
15
+
<div
16
16
+
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"
17
17
+
>
18
18
+
{#if item.cardData.favicon && !faviconHasError}
19
19
+
<img
20
20
+
class="size-6 rounded-lg object-cover"
21
21
+
onerror={() => (faviconHasError = true)}
22
22
+
src={item.cardData.favicon}
23
23
+
alt=""
24
24
+
/>
25
25
+
{:else}
26
26
+
<svg
27
27
+
xmlns="http://www.w3.org/2000/svg"
28
28
+
fill="none"
29
29
+
viewBox="0 0 24 24"
30
30
+
stroke-width="1.5"
31
31
+
stroke="currentColor"
32
32
+
class="size-4"
33
33
+
>
34
34
+
<path
35
35
+
stroke-linecap="round"
36
36
+
stroke-linejoin="round"
37
37
+
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
25
38
/>
26
26
-
{:else}
27
27
-
<svg
28
28
-
xmlns="http://www.w3.org/2000/svg"
29
29
-
fill="none"
30
30
-
viewBox="0 0 24 24"
31
31
-
stroke-width="1.5"
32
32
-
stroke="currentColor"
33
33
-
class="size-4"
34
34
-
>
35
35
-
<path
36
36
-
stroke-linecap="round"
37
37
-
stroke-linejoin="round"
38
38
-
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
39
39
-
/>
40
40
-
</svg>
41
41
-
{/if}
42
42
-
</div>
43
43
-
{/if}
39
39
+
</svg>
40
40
+
{/if}
41
41
+
</div>
44
42
<div
45
43
class={[
46
44
'text-base-900 dark:text-base-50 text-lg font-bold',
···
58
56
</div>
59
57
60
58
{#if browser && ((isMobile() && item.mobileH >= 8) || (!isMobile() && item.h >= 4)) && item.cardData.image}
61
61
-
<img class="mb-2 max-h-32 w-full starting:opacity-0 opacity-100 transition-opacity duration-100 rounded-xl object-cover" src={item.cardData.image} alt="" />
59
59
+
<img
60
60
+
class="mb-2 max-h-32 w-full rounded-xl object-cover opacity-100 transition-opacity duration-100 starting:opacity-0"
61
61
+
src={item.cardData.image}
62
62
+
alt=""
63
63
+
/>
62
64
{/if}
63
65
{#if item.cardData.href}
64
66
<a
+50
src/lib/cards/LinkCard/LinkCardSettings.svelte
···
1
1
+
<script lang="ts">
2
2
+
import { validateLink } from '$lib/helper';
3
3
+
import type { Item } from '$lib/types';
4
4
+
import { Button, Input, toast } from '@foxui/core';
5
5
+
6
6
+
let { item, onclose }: { item: Item; onclose: () => void } = $props();
7
7
+
8
8
+
let linkValue = $derived(item.cardData.href.replace('https://', '').replace('http://', ''));
9
9
+
10
10
+
function updateLink() {
11
11
+
if (!linkValue.trim()) return;
12
12
+
13
13
+
let link = validateLink(linkValue);
14
14
+
if (!link) {
15
15
+
toast.error('Invalid link');
16
16
+
return;
17
17
+
}
18
18
+
19
19
+
item.cardData.href = link;
20
20
+
item.cardData.domain = new URL(link).hostname;
21
21
+
item.cardData.hasFetched = false;
22
22
+
23
23
+
onclose?.();
24
24
+
}
25
25
+
</script>
26
26
+
27
27
+
<Input
28
28
+
spellcheck={false}
29
29
+
type="url"
30
30
+
bind:value={linkValue}
31
31
+
onkeydown={(event) => {
32
32
+
if (event.code === 'Enter') {
33
33
+
updateLink();
34
34
+
event.preventDefault();
35
35
+
}
36
36
+
}}
37
37
+
placeholder="Enter link"
38
38
+
/>
39
39
+
<Button onclick={updateLink} size="icon"
40
40
+
><svg
41
41
+
xmlns="http://www.w3.org/2000/svg"
42
42
+
fill="none"
43
43
+
viewBox="0 0 24 24"
44
44
+
stroke-width="1.5"
45
45
+
stroke="currentColor"
46
46
+
class="size-6"
47
47
+
>
48
48
+
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
49
49
+
</svg>
50
50
+
</Button>
+9
-5
src/lib/cards/LinkCard/index.ts
···
1
1
import type { CardDefinition } from '../types';
2
2
-
import CreateLinkCardModal from './CreateLinkCardModal.svelte';
3
2
import EditingLinkCard from './EditingLinkCard.svelte';
4
3
import LinkCard from './LinkCard.svelte';
4
4
+
import LinkCardSettings from './LinkCardSettings.svelte';
5
5
6
6
export const LinkCardDefinition = {
7
7
type: 'link',
···
9
9
editingContentComponent: EditingLinkCard,
10
10
createNew: (card) => {
11
11
card.cardType = 'link';
12
12
-
card.cardData = {
13
13
-
href: 'https://'
14
14
-
};
15
12
},
16
16
-
creationModalComponent: CreateLinkCardModal
13
13
+
settingsComponent: LinkCardSettings,
14
14
+
onUrlHandler: (url, item) => {
15
15
+
item.cardData.href = url;
16
16
+
item.cardData.domain = new URL(url).hostname;
17
17
+
item.cardData.hasFetched = false;
18
18
+
return item;
19
19
+
},
20
20
+
urlHandlerPriority: 0
17
21
} as CardDefinition & { type: 'link' };
+6
-6
src/lib/cards/TextCard/TextCardSettings.svelte
···
27
27
viewBox="0 0 24 24"
28
28
fill="none"
29
29
stroke="currentColor"
30
30
-
stroke-width="1.5"
30
30
+
stroke-width="2"
31
31
stroke-linecap="round"
32
32
stroke-linejoin="round"><path d="M21 5H3" /><path d="M15 12H3" /><path d="M17 19H3" /></svg
33
33
></ToggleGroupItem
···
38
38
viewBox="0 0 24 24"
39
39
fill="none"
40
40
stroke="currentColor"
41
41
-
stroke-width="1.5"
41
41
+
stroke-width="2"
42
42
stroke-linecap="round"
43
43
stroke-linejoin="round"><path d="M21 5H3" /><path d="M17 12H7" /><path d="M19 19H5" /></svg
44
44
></ToggleGroupItem
···
49
49
viewBox="0 0 24 24"
50
50
fill="none"
51
51
stroke="currentColor"
52
52
-
stroke-width="1.5"
52
52
+
stroke-width="2"
53
53
stroke-linecap="round"
54
54
stroke-linejoin="round"><path d="M21 5H3" /><path d="M21 12H9" /><path d="M21 19H7" /></svg
55
55
></ToggleGroupItem
···
74
74
viewBox="0 0 24 24"
75
75
fill="none"
76
76
stroke="currentColor"
77
77
-
stroke-width="1.5"
77
77
+
stroke-width="2"
78
78
stroke-linecap="round"
79
79
stroke-linejoin="round"
80
80
><rect width="6" height="16" x="4" y="6" rx="2" /><rect
···
92
92
viewBox="0 0 24 24"
93
93
fill="none"
94
94
stroke="currentColor"
95
95
-
stroke-width="1.5"
95
95
+
stroke-width="2"
96
96
stroke-linecap="round"
97
97
stroke-linejoin="round"
98
98
><rect width="10" height="6" x="7" y="9" rx="2" /><path d="M22 20H2" /><path
···
106
106
viewBox="0 0 24 24"
107
107
fill="none"
108
108
stroke="currentColor"
109
109
-
stroke-width="1.5"
109
109
+
stroke-width="2"
110
110
stroke-linecap="round"
111
111
stroke-linejoin="round"
112
112
><rect width="14" height="6" x="5" y="12" rx="2" /><rect
+4
-4
src/lib/cards/types.ts
···
7
7
oncancel: () => void;
8
8
};
9
9
10
10
-
export type SettingsModalComponentProps = {
10
10
+
export type SettingsComponentProps = {
11
11
item: Item;
12
12
-
onsave: (item: Item) => void;
13
13
-
oncancel: () => void;
12
12
+
onclose: () => void;
14
13
};
15
14
16
15
export type SidebarComponentProps = {
···
37
36
sidebarButtonText?: string;
38
37
39
38
// if this component exists, a settings button with a popover will be shown containing this component
40
40
-
settingsComponent?: Component<ContentComponentProps>;
39
39
+
settingsComponent?: Component<SettingsComponentProps>;
41
40
42
41
// optionally load some extra data
43
42
loadData?: (
···
62
61
canResize?: boolean;
63
62
64
63
onUrlHandler?: (url: string, item: Item) => Item | null;
64
64
+
urlHandlerPriority?: number;
65
65
};
+1
src/lib/cards/utils/PlainTextEditor.svelte
···
76
76
:global(.tiptap p.is-editor-empty:first-child::before) {
77
77
color: var(--color-base-800);
78
78
content: attr(data-placeholder);
79
79
+
opacity: 50%;
79
80
float: left;
80
81
height: 0;
81
82
pointer-events: none;
+25
src/lib/helper.ts
···
299
299
300
300
return isEditable;
301
301
}
302
302
+
303
303
+
export function validateLink(
304
304
+
link: string | undefined,
305
305
+
tryAdding: boolean = true
306
306
+
): string | undefined {
307
307
+
if (!link) return;
308
308
+
try {
309
309
+
new URL(link);
310
310
+
311
311
+
return link;
312
312
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
313
313
+
} catch (e) {
314
314
+
if (!tryAdding) return;
315
315
+
316
316
+
try {
317
317
+
link = 'https://' + link;
318
318
+
new URL(link);
319
319
+
320
320
+
return link;
321
321
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
322
322
+
} catch (e) {
323
323
+
return;
324
324
+
}
325
325
+
}
326
326
+
}
+118
-58
src/lib/website/EditableWebsite.svelte
···
1
1
<script lang="ts">
2
2
import { client, login } from '$lib/oauth/auth.svelte.js';
3
3
4
4
-
import { Navbar, Button, toast, Toaster, Toggle, Sidebar } from '@foxui/core';
4
4
+
import {
5
5
+
Navbar,
6
6
+
Button,
7
7
+
toast,
8
8
+
Toaster,
9
9
+
Toggle,
10
10
+
Sidebar,
11
11
+
Popover,
12
12
+
Input,
13
13
+
Label
14
14
+
} from '@foxui/core';
5
15
import { BlueskyLogin } from '@foxui/social';
6
16
7
17
import { COLUMNS, margin, mobileMargin } from '$lib';
···
14
24
getHideProfile,
15
25
getName,
16
26
isTyping,
17
17
-
setPositionOfNewItem
27
27
+
setPositionOfNewItem,
28
28
+
validateLink
18
29
} from '../helper';
19
30
import Profile from './Profile.svelte';
20
31
import type { Item, WebsiteData } from '../types';
···
76
87
77
88
let maxHeight = $derived(items.reduce((max, item) => Math.max(max, getY(item) + getH(item)), 0));
78
89
79
79
-
function newCard(type: string = 'link') {
90
90
+
function newCard(type: string = 'link', cardData?: any) {
80
91
// close sidebar if open
81
92
const popover = document.getElementById('mobile-menu');
82
93
if (popover) {
···
94
105
mobileX: 0,
95
106
mobileY: 0,
96
107
cardType: type,
97
97
-
cardData: {},
108
108
+
cardData: cardData ?? {},
98
109
version: 2,
99
110
page: data.page
100
111
};
···
232
243
233
244
let debugPoint = $state({ x: 0, y: 0 });
234
245
246
246
+
let linkPopoverOpen = $state(false);
247
247
+
235
248
function getDragXY(
236
249
e: DragEvent & {
237
250
currentTarget: EventTarget & HTMLDivElement;
···
264
277
}
265
278
return { x: gridX, y: gridY };
266
279
}
267
267
-
</script>
268
280
269
269
-
<svelte:body
270
270
-
onpaste={(event) => {
271
271
-
if (isTyping()) return;
281
281
+
let linkValue = $state('');
272
282
273
273
-
const text = event.clipboardData?.getData('text/plain');
274
274
-
275
275
-
if (!text) return;
283
283
+
function addLink(url: string) {
284
284
+
let link = validateLink(url);
285
285
+
if (!link) {
286
286
+
toast.error('invalid link');
287
287
+
return;
288
288
+
}
276
289
277
277
-
try {
278
278
-
const url = new URL(text);
290
290
+
let item: Item = {
291
291
+
id: TID.nextStr(),
292
292
+
x: 0,
293
293
+
y: 0,
294
294
+
w: 2,
295
295
+
h: 2,
296
296
+
mobileH: 4,
297
297
+
mobileW: 4,
298
298
+
mobileX: 0,
299
299
+
mobileY: 0,
300
300
+
cardType: '',
301
301
+
cardData: {}
302
302
+
};
279
303
280
280
-
let item: Item = {
281
281
-
id: TID.nextStr(),
282
282
-
x: 0,
283
283
-
y: 0,
284
284
-
w: 2,
285
285
-
h: 2,
286
286
-
mobileH: 4,
287
287
-
mobileW: 4,
288
288
-
mobileX: 0,
289
289
-
mobileY: 0,
290
290
-
cardType: '',
291
291
-
cardData: {}
292
292
-
};
304
304
+
newItem.item = item;
293
305
294
294
-
newItem.item = item;
306
306
+
console.log(AllCardDefinitions.toSorted(
307
307
+
(a, b) => (b.urlHandlerPriority ?? 0) - (a.urlHandlerPriority ?? 0)
308
308
+
));
295
309
296
296
-
for (const cardDef of AllCardDefinitions) {
297
297
-
if (cardDef.onUrlHandler?.(text, item)) {
298
298
-
item.cardType = cardDef.type;
299
299
-
saveNewItem();
300
300
-
return;
301
301
-
}
310
310
+
for (const cardDef of AllCardDefinitions.toSorted(
311
311
+
(a, b) => (b.urlHandlerPriority ?? 0) - (a.urlHandlerPriority ?? 0)
312
312
+
)) {
313
313
+
if (cardDef.onUrlHandler?.(link, item)) {
314
314
+
item.cardType = cardDef.type;
315
315
+
saveNewItem();
316
316
+
break;
302
317
}
318
318
+
}
303
319
304
304
-
newItem = {};
305
305
-
} catch (e) {
306
306
-
return;
320
320
+
newItem = {};
321
321
+
322
322
+
if(linkValue === url) {
323
323
+
linkValue = '';
324
324
+
linkPopoverOpen = false;
307
325
}
326
326
+
}
327
327
+
</script>
328
328
+
329
329
+
<svelte:body
330
330
+
onpaste={(event) => {
331
331
+
if (isTyping()) return;
332
332
+
333
333
+
const text = event.clipboardData?.getData('text/plain');
334
334
+
const link = validateLink(text, false);
335
335
+
if (!link) return;
336
336
+
337
337
+
addLink(link);
308
338
}}
309
339
/>
310
340
···
514
544
/></svg
515
545
>
516
546
</Button>
517
517
-
<Button
518
518
-
size="iconLg"
519
519
-
variant="ghost"
520
520
-
class="backdrop-blur-none"
521
521
-
onclick={() => {
522
522
-
newCard('link');
523
523
-
}}
524
524
-
>
525
525
-
<svg
526
526
-
xmlns="http://www.w3.org/2000/svg"
527
527
-
fill="none"
528
528
-
viewBox="-2 -2 28 28"
529
529
-
stroke-width="1.5"
530
530
-
stroke="currentColor"
531
531
-
>
532
532
-
<path
533
533
-
stroke-linecap="round"
534
534
-
stroke-linejoin="round"
535
535
-
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
536
536
-
/>
537
537
-
</svg>
538
538
-
</Button>
547
547
+
548
548
+
<Popover sideOffset={16} bind:open={linkPopoverOpen} class="bg-base-100 dark:bg-base-900">
549
549
+
{#snippet child({ props })}
550
550
+
<Button
551
551
+
size="iconLg"
552
552
+
variant="ghost"
553
553
+
class="backdrop-blur-none"
554
554
+
onclick={() => {
555
555
+
newCard('link');
556
556
+
}}
557
557
+
{...props}
558
558
+
>
559
559
+
<svg
560
560
+
xmlns="http://www.w3.org/2000/svg"
561
561
+
fill="none"
562
562
+
viewBox="-2 -2 28 28"
563
563
+
stroke-width="1.5"
564
564
+
stroke="currentColor"
565
565
+
>
566
566
+
<path
567
567
+
stroke-linecap="round"
568
568
+
stroke-linejoin="round"
569
569
+
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
570
570
+
/>
571
571
+
</svg>
572
572
+
</Button>
573
573
+
{/snippet}
574
574
+
<Input
575
575
+
spellcheck={false}
576
576
+
type="url"
577
577
+
bind:value={linkValue}
578
578
+
onkeydown={(event) => {
579
579
+
if (event.code === 'Enter') {
580
580
+
addLink(linkValue);
581
581
+
event.preventDefault();
582
582
+
}
583
583
+
}}
584
584
+
placeholder="Enter link"
585
585
+
/>
586
586
+
<Button onclick={() => addLink(linkValue)} size="icon"
587
587
+
><svg
588
588
+
xmlns="http://www.w3.org/2000/svg"
589
589
+
fill="none"
590
590
+
viewBox="0 0 24 24"
591
591
+
stroke-width="1.5"
592
592
+
stroke="currentColor"
593
593
+
class="size-6"
594
594
+
>
595
595
+
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
596
596
+
</svg>
597
597
+
</Button>
598
598
+
</Popover>
539
599
540
600
<Button
541
601
size="iconLg"