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
add kickstarter card
Florian
6 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
'github',
62
'discord',
63
'linkedin',
64
-
'mastodon'
0
65
],
66
groups: ['Social'],
67
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
siWechat,
104
siLine,
105
siArchiveofourown,
0
106
type SimpleIcon
107
} from 'simple-icons';
108
···
159
160
ao3: /(?:archiveofourown\.org)/i,
161
0
0
162
germ: /(?:ger\.mx)/i,
163
164
tangled: /(?:tangled\.org)/i,
···
258
line: siLine,
259
260
ao3: siArchiveofourown,
0
0
261
262
tangled: {
263
slug: 'tangled',
···
61
'github',
62
'discord',
63
'linkedin',
64
+
'mastodon',
65
+
'kickstarter'
66
],
67
groups: ['Social'],
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>`
···
104
siWechat,
105
siLine,
106
siArchiveofourown,
107
+
siKickstarter,
108
type SimpleIcon
109
} from 'simple-icons';
110
···
161
162
ao3: /(?:archiveofourown\.org)/i,
163
164
+
kickstarter: /(?:kickstarter\.com)/i,
165
+
166
germ: /(?:ger\.mx)/i,
167
168
tangled: /(?:tangled\.org)/i,
···
262
line: siLine,
263
264
ao3: siArchiveofourown,
265
+
266
+
kickstarter: siKickstarter,
267
268
tangled: {
269
slug: 'tangled',
+89
src/lib/cards/KickstarterCard/CreateKickstarterCardModal.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
0
0
0
0
0
···
1
+
<script lang="ts">
2
+
import { Alert, Button, Modal, Subheading } from '@foxui/core';
3
+
import type { CreationModalComponentProps } from '../types';
4
+
5
+
let { item = $bindable(), oncreate, oncancel }: CreationModalComponentProps = $props();
6
+
7
+
let embedCode = $state('');
8
+
let errorMessage = $state('');
9
+
10
+
function parseInput(code: string): {
11
+
src: string | null;
12
+
widgetType: 'card' | 'video';
13
+
} {
14
+
const normalized = code.trim().replaceAll('&', '&');
15
+
16
+
// Try iframe embed code first
17
+
const srcMatch = normalized.match(/src="(https:\/\/www\.kickstarter\.com\/[^"]+)"/);
18
+
if (srcMatch) {
19
+
const src = srcMatch[1];
20
+
const widgetType = src.includes('/widget/video') ? 'video' : 'card';
21
+
return { src, widgetType };
22
+
}
23
+
24
+
// Try plain project URL
25
+
const urlMatch = normalized.match(/kickstarter\.com\/projects\/([^/]+\/[^/?#\s]+)/i);
26
+
if (urlMatch) {
27
+
return {
28
+
src: `https://www.kickstarter.com/projects/${urlMatch[1]}/widget/card.html?v=2`,
29
+
widgetType: 'card'
30
+
};
31
+
}
32
+
33
+
return { src: null, widgetType: 'card' };
34
+
}
35
+
36
+
function validate(): boolean {
37
+
errorMessage = '';
38
+
39
+
const { src, widgetType } = parseInput(embedCode);
40
+
41
+
if (!src) {
42
+
errorMessage = 'Could not find a Kickstarter URL in the input';
43
+
return false;
44
+
}
45
+
46
+
item.cardData.src = src;
47
+
item.cardData.widgetType = widgetType;
48
+
49
+
if (widgetType === 'video') {
50
+
item.w = 4;
51
+
item.h = 2;
52
+
item.mobileW = 8;
53
+
item.mobileH = 4;
54
+
} else {
55
+
item.w = 4;
56
+
item.h = 4;
57
+
item.mobileW = 8;
58
+
item.mobileH = 8;
59
+
}
60
+
61
+
return true;
62
+
}
63
+
</script>
64
+
65
+
<Modal open={true} closeButton={false}>
66
+
<Subheading>Paste Kickstarter URL or Embed Code</Subheading>
67
+
68
+
<textarea
69
+
bind:value={embedCode}
70
+
placeholder="https://www.kickstarter.com/projects/..."
71
+
rows={5}
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
+
></textarea>
74
+
75
+
{#if errorMessage}
76
+
<Alert type="error" title="Invalid embed code"><span>{errorMessage}</span></Alert>
77
+
{/if}
78
+
79
+
<div class="mt-4 flex justify-end gap-2">
80
+
<Button onclick={oncancel} variant="ghost">Cancel</Button>
81
+
<Button
82
+
onclick={() => {
83
+
if (validate()) oncreate();
84
+
}}
85
+
>
86
+
Create
87
+
</Button>
88
+
</div>
89
+
</Modal>
+25
src/lib/cards/KickstarterCard/KickstarterCard.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 type { ContentComponentProps } from '../types';
3
+
4
+
let { item, isEditing }: ContentComponentProps = $props();
5
+
6
+
let isVideo = $derived(item.cardData.widgetType === 'video');
7
+
let projectUrl = $derived(
8
+
(item.cardData.src || '').replace(/\/widget\/(card|video)\.html.*$/, '')
9
+
);
10
+
</script>
11
+
12
+
<iframe
13
+
src={item.cardData.src}
14
+
title="Kickstarter widget"
15
+
frameborder="0"
16
+
scrolling="no"
17
+
class={['absolute inset-0 h-full w-full', (!isVideo || isEditing) && 'pointer-events-none']}
18
+
></iframe>
19
+
20
+
{#if !isVideo && !isEditing}
21
+
<a href={projectUrl} target="_blank" rel="noopener noreferrer">
22
+
<div class="absolute inset-0 z-50"></div>
23
+
<span class="sr-only">Open Kickstarter project</span>
24
+
</a>
25
+
{/if}
+46
src/lib/cards/KickstarterCard/index.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
···
1
+
import type { CardDefinition } from '../types';
2
+
import CreateKickstarterCardModal from './CreateKickstarterCardModal.svelte';
3
+
import KickstarterCard from './KickstarterCard.svelte';
4
+
5
+
const cardType = 'kickstarter';
6
+
7
+
export const KickstarterCardDefinition = {
8
+
type: cardType,
9
+
contentComponent: KickstarterCard,
10
+
creationModalComponent: CreateKickstarterCardModal,
11
+
createNew: (item) => {
12
+
item.cardType = cardType;
13
+
item.cardData = { widgetType: 'card' };
14
+
item.w = 4;
15
+
item.h = 4;
16
+
item.mobileW = 8;
17
+
item.mobileH = 8;
18
+
},
19
+
20
+
onUrlHandler: (url, item) => {
21
+
const match = url.match(/kickstarter\.com\/projects\/([^/]+\/[^/?#]+)/i);
22
+
if (!match) return null;
23
+
24
+
item.cardData.src = `https://www.kickstarter.com/projects/${match[1]}/widget/card.html?v=2`;
25
+
item.cardData.widgetType = 'card';
26
+
item.w = 4;
27
+
item.h = 4;
28
+
item.mobileW = 8;
29
+
item.mobileH = 8;
30
+
31
+
return item;
32
+
},
33
+
34
+
defaultColor: 'transparent',
35
+
allowSetColor: false,
36
+
37
+
urlHandlerPriority: 10,
38
+
39
+
name: 'Kickstarter',
40
+
keywords: ['kickstarter', 'crowdfunding', 'campaign', 'funding'],
41
+
groups: ['Social'],
42
+
icon: `<svg class="size-4" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
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
+
</svg>
45
+
`
46
+
} as CardDefinition & { type: typeof cardType };
+3
-1
src/lib/cards/index.ts
···
39
import { FriendsCardDefinition } from './FriendsCard';
40
import { GitHubContributorsCardDefinition } from './GitHubContributorsCard';
41
import { ProductHuntCardDefinition } from './ProductHuntCard';
0
42
// import { Model3DCardDefinition } from './Model3DCard';
43
44
export const AllCardDefinitions = [
···
82
// Model3DCardDefinition
83
FriendsCardDefinition,
84
GitHubContributorsCardDefinition,
85
-
ProductHuntCardDefinition
0
86
] as const;
87
88
export const CardDefinitionsByType = AllCardDefinitions.reduce(
···
39
import { FriendsCardDefinition } from './FriendsCard';
40
import { GitHubContributorsCardDefinition } from './GitHubContributorsCard';
41
import { ProductHuntCardDefinition } from './ProductHuntCard';
42
+
import { KickstarterCardDefinition } from './KickstarterCard';
43
// import { Model3DCardDefinition } from './Model3DCard';
44
45
export const AllCardDefinitions = [
···
83
// Model3DCardDefinition
84
FriendsCardDefinition,
85
GitHubContributorsCardDefinition,
86
+
ProductHuntCardDefinition,
87
+
KickstarterCardDefinition
88
] as const;
89
90
export const CardDefinitionsByType = AllCardDefinitions.reduce(
+9
-3
src/lib/website/EditableWebsite.svelte
···
74
// svelte-ignore state_referenced_locally
75
let savedPublication = $state(JSON.stringify(data.publication));
76
77
-
let hasUnsavedChanges = $derived(
78
-
JSON.stringify(items) !== savedItems || JSON.stringify(data.publication) !== savedPublication
79
-
);
0
0
0
0
0
0
80
81
// Warn user before closing tab if there are unsaved changes
82
$effect(() => {
···
74
// svelte-ignore state_referenced_locally
75
let savedPublication = $state(JSON.stringify(data.publication));
76
77
+
let hasUnsavedChanges = $state(false);
78
+
79
+
$effect(() => {
80
+
if (!hasUnsavedChanges) {
81
+
hasUnsavedChanges =
82
+
JSON.stringify(items) !== savedItems ||
83
+
JSON.stringify(data.publication) !== savedPublication;
84
+
}
85
+
});
86
87
// Warn user before closing tab if there are unsaved changes
88
$effect(() => {