tangled
alpha
login
or
join now
flo-bit.dev
/
blento
your personal website on atproto - mirror
blento.app
19
fork
atom
overview
issues
pulls
pipelines
add kickstarter card
Florian
5 days ago
059d65a4
14bb44d6
+179
-5
6 changed files
expand all
collapse all
unified
split
src
lib
cards
BigSocialCard
index.ts
KickstarterCard
CreateKickstarterCardModal.svelte
KickstarterCard.svelte
index.ts
index.ts
website
EditableWebsite.svelte
+7
-1
src/lib/cards/BigSocialCard/index.ts
···
61
61
'github',
62
62
'discord',
63
63
'linkedin',
64
64
-
'mastodon'
64
64
+
'mastodon',
65
65
+
'kickstarter'
65
66
],
66
67
groups: ['Social'],
67
68
icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M7.217 10.907a2.25 2.25 0 1 0 0 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186 9.566-5.314m-9.566 7.5 9.566 5.314m0 0a2.25 2.25 0 1 0 3.935 2.186 2.25 2.25 0 0 0-3.935-2.186Zm0-12.814a2.25 2.25 0 1 0 3.933-2.185 2.25 2.25 0 0 0-3.933 2.185Z" /></svg>`
···
103
104
siWechat,
104
105
siLine,
105
106
siArchiveofourown,
107
107
+
siKickstarter,
106
108
type SimpleIcon
107
109
} from 'simple-icons';
108
110
···
159
161
160
162
ao3: /(?:archiveofourown\.org)/i,
161
163
164
164
+
kickstarter: /(?:kickstarter\.com)/i,
165
165
+
162
166
germ: /(?:ger\.mx)/i,
163
167
164
168
tangled: /(?:tangled\.org)/i,
···
258
262
line: siLine,
259
263
260
264
ao3: siArchiveofourown,
265
265
+
266
266
+
kickstarter: siKickstarter,
261
267
262
268
tangled: {
263
269
slug: 'tangled',
+89
src/lib/cards/KickstarterCard/CreateKickstarterCardModal.svelte
···
1
1
+
<script lang="ts">
2
2
+
import { Alert, Button, 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 embedCode = $state('');
8
8
+
let errorMessage = $state('');
9
9
+
10
10
+
function parseInput(code: string): {
11
11
+
src: string | null;
12
12
+
widgetType: 'card' | 'video';
13
13
+
} {
14
14
+
const normalized = code.trim().replaceAll('&', '&');
15
15
+
16
16
+
// Try iframe embed code first
17
17
+
const srcMatch = normalized.match(/src="(https:\/\/www\.kickstarter\.com\/[^"]+)"/);
18
18
+
if (srcMatch) {
19
19
+
const src = srcMatch[1];
20
20
+
const widgetType = src.includes('/widget/video') ? 'video' : 'card';
21
21
+
return { src, widgetType };
22
22
+
}
23
23
+
24
24
+
// Try plain project URL
25
25
+
const urlMatch = normalized.match(/kickstarter\.com\/projects\/([^/]+\/[^/?#\s]+)/i);
26
26
+
if (urlMatch) {
27
27
+
return {
28
28
+
src: `https://www.kickstarter.com/projects/${urlMatch[1]}/widget/card.html?v=2`,
29
29
+
widgetType: 'card'
30
30
+
};
31
31
+
}
32
32
+
33
33
+
return { src: null, widgetType: 'card' };
34
34
+
}
35
35
+
36
36
+
function validate(): boolean {
37
37
+
errorMessage = '';
38
38
+
39
39
+
const { src, widgetType } = parseInput(embedCode);
40
40
+
41
41
+
if (!src) {
42
42
+
errorMessage = 'Could not find a Kickstarter URL in the input';
43
43
+
return false;
44
44
+
}
45
45
+
46
46
+
item.cardData.src = src;
47
47
+
item.cardData.widgetType = widgetType;
48
48
+
49
49
+
if (widgetType === 'video') {
50
50
+
item.w = 4;
51
51
+
item.h = 2;
52
52
+
item.mobileW = 8;
53
53
+
item.mobileH = 4;
54
54
+
} else {
55
55
+
item.w = 4;
56
56
+
item.h = 4;
57
57
+
item.mobileW = 8;
58
58
+
item.mobileH = 8;
59
59
+
}
60
60
+
61
61
+
return true;
62
62
+
}
63
63
+
</script>
64
64
+
65
65
+
<Modal open={true} closeButton={false}>
66
66
+
<Subheading>Paste Kickstarter URL or Embed Code</Subheading>
67
67
+
68
68
+
<textarea
69
69
+
bind:value={embedCode}
70
70
+
placeholder="https://www.kickstarter.com/projects/..."
71
71
+
rows={5}
72
72
+
class="bg-base-100 dark:bg-base-800 border-base-300 dark:border-base-700 text-base-900 dark:text-base-100 w-full rounded-xl border px-3 py-2 font-mono text-sm"
73
73
+
></textarea>
74
74
+
75
75
+
{#if errorMessage}
76
76
+
<Alert type="error" title="Invalid embed code"><span>{errorMessage}</span></Alert>
77
77
+
{/if}
78
78
+
79
79
+
<div class="mt-4 flex justify-end gap-2">
80
80
+
<Button onclick={oncancel} variant="ghost">Cancel</Button>
81
81
+
<Button
82
82
+
onclick={() => {
83
83
+
if (validate()) oncreate();
84
84
+
}}
85
85
+
>
86
86
+
Create
87
87
+
</Button>
88
88
+
</div>
89
89
+
</Modal>
+25
src/lib/cards/KickstarterCard/KickstarterCard.svelte
···
1
1
+
<script lang="ts">
2
2
+
import type { ContentComponentProps } from '../types';
3
3
+
4
4
+
let { item, isEditing }: ContentComponentProps = $props();
5
5
+
6
6
+
let isVideo = $derived(item.cardData.widgetType === 'video');
7
7
+
let projectUrl = $derived(
8
8
+
(item.cardData.src || '').replace(/\/widget\/(card|video)\.html.*$/, '')
9
9
+
);
10
10
+
</script>
11
11
+
12
12
+
<iframe
13
13
+
src={item.cardData.src}
14
14
+
title="Kickstarter widget"
15
15
+
frameborder="0"
16
16
+
scrolling="no"
17
17
+
class={['absolute inset-0 h-full w-full', (!isVideo || isEditing) && 'pointer-events-none']}
18
18
+
></iframe>
19
19
+
20
20
+
{#if !isVideo && !isEditing}
21
21
+
<a href={projectUrl} target="_blank" rel="noopener noreferrer">
22
22
+
<div class="absolute inset-0 z-50"></div>
23
23
+
<span class="sr-only">Open Kickstarter project</span>
24
24
+
</a>
25
25
+
{/if}
+46
src/lib/cards/KickstarterCard/index.ts
···
1
1
+
import type { CardDefinition } from '../types';
2
2
+
import CreateKickstarterCardModal from './CreateKickstarterCardModal.svelte';
3
3
+
import KickstarterCard from './KickstarterCard.svelte';
4
4
+
5
5
+
const cardType = 'kickstarter';
6
6
+
7
7
+
export const KickstarterCardDefinition = {
8
8
+
type: cardType,
9
9
+
contentComponent: KickstarterCard,
10
10
+
creationModalComponent: CreateKickstarterCardModal,
11
11
+
createNew: (item) => {
12
12
+
item.cardType = cardType;
13
13
+
item.cardData = { widgetType: 'card' };
14
14
+
item.w = 4;
15
15
+
item.h = 4;
16
16
+
item.mobileW = 8;
17
17
+
item.mobileH = 8;
18
18
+
},
19
19
+
20
20
+
onUrlHandler: (url, item) => {
21
21
+
const match = url.match(/kickstarter\.com\/projects\/([^/]+\/[^/?#]+)/i);
22
22
+
if (!match) return null;
23
23
+
24
24
+
item.cardData.src = `https://www.kickstarter.com/projects/${match[1]}/widget/card.html?v=2`;
25
25
+
item.cardData.widgetType = 'card';
26
26
+
item.w = 4;
27
27
+
item.h = 4;
28
28
+
item.mobileW = 8;
29
29
+
item.mobileH = 8;
30
30
+
31
31
+
return item;
32
32
+
},
33
33
+
34
34
+
defaultColor: 'transparent',
35
35
+
allowSetColor: false,
36
36
+
37
37
+
urlHandlerPriority: 10,
38
38
+
39
39
+
name: 'Kickstarter',
40
40
+
keywords: ['kickstarter', 'crowdfunding', 'campaign', 'funding'],
41
41
+
groups: ['Social'],
42
42
+
icon: `<svg class="size-4" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
43
43
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.9257 17.2442C20.9257 16.3321 20.6731 15.4527 20.1362 14.6709L18.1153 11.7719L20.1362 8.87291C20.6731 8.12373 20.9257 7.21169 20.9257 6.29964C20.9257 3.88924 18.9994 2.03257 16.7258 2.03257C15.3996 2.03257 14.0733 2.71661 13.2523 3.88924L12.2418 5.32245C11.8629 3.40064 10.2524 2 8.19984 2C5.83151 2 4 3.95438 4 6.36479V17.2768C4 19.6872 5.86309 21.6416 8.19984 21.6416C10.2208 21.6416 11.7997 20.3386 12.2102 18.4494L13.0944 19.7523C13.9154 20.9901 15.2733 21.6416 16.5995 21.6416C18.9994 21.6741 20.9257 19.6546 20.9257 17.2442Z" stroke="currentColor" stroke-width="2"/>
44
44
+
</svg>
45
45
+
`
46
46
+
} as CardDefinition & { type: typeof cardType };
+3
-1
src/lib/cards/index.ts
···
39
39
import { FriendsCardDefinition } from './FriendsCard';
40
40
import { GitHubContributorsCardDefinition } from './GitHubContributorsCard';
41
41
import { ProductHuntCardDefinition } from './ProductHuntCard';
42
42
+
import { KickstarterCardDefinition } from './KickstarterCard';
42
43
// import { Model3DCardDefinition } from './Model3DCard';
43
44
44
45
export const AllCardDefinitions = [
···
82
83
// Model3DCardDefinition
83
84
FriendsCardDefinition,
84
85
GitHubContributorsCardDefinition,
85
85
-
ProductHuntCardDefinition
86
86
+
ProductHuntCardDefinition,
87
87
+
KickstarterCardDefinition
86
88
] as const;
87
89
88
90
export const CardDefinitionsByType = AllCardDefinitions.reduce(
+9
-3
src/lib/website/EditableWebsite.svelte
···
74
74
// svelte-ignore state_referenced_locally
75
75
let savedPublication = $state(JSON.stringify(data.publication));
76
76
77
77
-
let hasUnsavedChanges = $derived(
78
78
-
JSON.stringify(items) !== savedItems || JSON.stringify(data.publication) !== savedPublication
79
79
-
);
77
77
+
let hasUnsavedChanges = $state(false);
78
78
+
79
79
+
$effect(() => {
80
80
+
if (!hasUnsavedChanges) {
81
81
+
hasUnsavedChanges =
82
82
+
JSON.stringify(items) !== savedItems ||
83
83
+
JSON.stringify(data.publication) !== savedPublication;
84
84
+
}
85
85
+
});
80
86
81
87
// Warn user before closing tab if there are unsaved changes
82
88
$effect(() => {