tangled
alpha
login
or
join now
flo-bit.dev
/
blento
21
fork
atom
your personal website on atproto - mirror
blento.app
21
fork
atom
overview
issues
pulls
pipelines
add product hunt card
Florian
1 month ago
50d2c49d
3edf5dd2
+143
-3
6 changed files
expand all
collapse all
unified
split
src
lib
cards
LatestBlueskyPostCard
LatestBlueskyPostCard.svelte
index.ts
ProductHuntCard
CreateProductHuntCardModal.svelte
ProductHuntCard.svelte
index.ts
index.ts
+1
-1
src/lib/cards/LatestBlueskyPostCard/LatestBlueskyPostCard.svelte
···
30
30
31
31
<div class="flex h-full flex-col justify-center-safe overflow-y-scroll p-4">
32
32
{#if feed?.[0]?.post}
33
33
-
<div class={[item.cardData.label ? "pt-8" : '']}>
33
33
+
<div class={[item.cardData.label ? 'pt-8' : '']}>
34
34
<BlueskyPost showLogo feedViewPost={feed?.[0].post}></BlueskyPost>
35
35
</div>
36
36
<div class="h-4 w-full"></div>
+1
-1
src/lib/cards/LatestBlueskyPostCard/index.ts
···
12
12
card.h = 4;
13
13
card.mobileH = 8;
14
14
15
15
-
card.cardData.label = "";
15
15
+
card.cardData.label = '';
16
16
},
17
17
loadData: async (items, { did }) => {
18
18
const authorFeed = await getAuthorFeed({ did, filter: 'posts_no_replies', limit: 2 });
+71
src/lib/cards/ProductHuntCard/CreateProductHuntCardModal.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 parseEmbedCode(code: string): {
11
11
+
imageSrc: string | null;
12
12
+
linkHref: string | null;
13
13
+
} {
14
14
+
const normalized = code.replaceAll('&', '&');
15
15
+
16
16
+
const srcMatch = normalized.match(/src="(https:\/\/api\.producthunt\.com\/[^"]+)"/);
17
17
+
const imageSrc = srcMatch ? srcMatch[1] : null;
18
18
+
19
19
+
const hrefMatch = normalized.match(/href="(https:\/\/www\.producthunt\.com\/[^"]+)"/);
20
20
+
const linkHref = hrefMatch ? hrefMatch[1] : null;
21
21
+
22
22
+
return { imageSrc, linkHref };
23
23
+
}
24
24
+
25
25
+
function validate(): boolean {
26
26
+
errorMessage = '';
27
27
+
28
28
+
const { imageSrc, linkHref } = parseEmbedCode(embedCode);
29
29
+
30
30
+
if (!linkHref) {
31
31
+
errorMessage = 'Could not find a Product Hunt link in the embed code';
32
32
+
return false;
33
33
+
}
34
34
+
35
35
+
if (!imageSrc) {
36
36
+
errorMessage = 'Could not find a Product Hunt badge image in the embed code';
37
37
+
return false;
38
38
+
}
39
39
+
40
40
+
item.cardData.imageSrc = imageSrc;
41
41
+
item.cardData.linkHref = linkHref;
42
42
+
43
43
+
return true;
44
44
+
}
45
45
+
</script>
46
46
+
47
47
+
<Modal open={true} closeButton={false}>
48
48
+
<Subheading>Paste Product Hunt Embed Code</Subheading>
49
49
+
50
50
+
<textarea
51
51
+
bind:value={embedCode}
52
52
+
placeholder="<a href="https://www.producthunt.com/posts/your-product?..."
53
53
+
rows={5}
54
54
+
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"
55
55
+
></textarea>
56
56
+
57
57
+
{#if errorMessage}
58
58
+
<Alert type="error" title="Invalid embed code"><span>{errorMessage}</span></Alert>
59
59
+
{/if}
60
60
+
61
61
+
<div class="mt-4 flex justify-end gap-2">
62
62
+
<Button onclick={oncancel} variant="ghost">Cancel</Button>
63
63
+
<Button
64
64
+
onclick={() => {
65
65
+
if (validate()) oncreate();
66
66
+
}}
67
67
+
>
68
68
+
Create
69
69
+
</Button>
70
70
+
</div>
71
71
+
</Modal>
+37
src/lib/cards/ProductHuntCard/ProductHuntCard.svelte
···
1
1
+
<script lang="ts">
2
2
+
import { getCanEdit } from '$lib/website/context';
3
3
+
import type { ContentComponentProps } from '../types';
4
4
+
5
5
+
let { item }: ContentComponentProps = $props();
6
6
+
7
7
+
let isEditing = getCanEdit();
8
8
+
9
9
+
let linkHref = $derived(item.cardData.linkHref || '');
10
10
+
let lightImageSrc = $derived(
11
11
+
(item.cardData.imageSrc || '').replace(/theme=(light|dark|neutral)/, 'theme=light')
12
12
+
);
13
13
+
let darkImageSrc = $derived(
14
14
+
(item.cardData.imageSrc || '').replace(/theme=(light|dark|neutral)/, 'theme=dark')
15
15
+
);
16
16
+
</script>
17
17
+
18
18
+
<a
19
19
+
href={linkHref}
20
20
+
target="_blank"
21
21
+
rel="noopener noreferrer"
22
22
+
class={[
23
23
+
'flex h-full w-full items-center justify-center p-4',
24
24
+
isEditing() && 'pointer-events-none'
25
25
+
]}
26
26
+
>
27
27
+
<img
28
28
+
src={lightImageSrc}
29
29
+
alt="Product Hunt badge"
30
30
+
class="max-h-full max-w-full object-contain dark:hidden"
31
31
+
/>
32
32
+
<img
33
33
+
src={darkImageSrc}
34
34
+
alt="Product Hunt badge"
35
35
+
class="hidden max-h-full max-w-full object-contain dark:block"
36
36
+
/>
37
37
+
</a>
+30
src/lib/cards/ProductHuntCard/index.ts
···
1
1
+
import type { CardDefinition } from '../types';
2
2
+
import CreateProductHuntCardModal from './CreateProductHuntCardModal.svelte';
3
3
+
import ProductHuntCard from './ProductHuntCard.svelte';
4
4
+
5
5
+
const cardType = 'producthunt';
6
6
+
7
7
+
export const ProductHuntCardDefinition = {
8
8
+
type: cardType,
9
9
+
contentComponent: ProductHuntCard,
10
10
+
creationModalComponent: CreateProductHuntCardModal,
11
11
+
createNew: (item) => {
12
12
+
item.cardType = cardType;
13
13
+
item.cardData = {};
14
14
+
item.w = 4;
15
15
+
item.h = 2;
16
16
+
item.mobileW = 8;
17
17
+
item.mobileH = 2;
18
18
+
},
19
19
+
20
20
+
defaultColor: 'transparent',
21
21
+
22
22
+
allowSetColor: false,
23
23
+
24
24
+
minH: 1,
25
25
+
26
26
+
name: 'Product Hunt',
27
27
+
keywords: ['producthunt', 'product', 'launch', 'badge'],
28
28
+
groups: ['Social'],
29
29
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4"><path d="M13.6 12h-3.2V8h3.2a2 2 0 1 1 0 4ZM12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.6 0 12 0Zm1.6 14.4h-3.2V18H8V6h5.6a4.4 4.4 0 0 1 0 8.8h0v-.4Z"/></svg>`
30
30
+
} as CardDefinition & { type: typeof cardType };
+3
-1
src/lib/cards/index.ts
···
38
38
import { GuestbookCardDefinition } from './GuestbookCard';
39
39
import { FriendsCardDefinition } from './FriendsCard';
40
40
import { GitHubContributorsCardDefinition } from './GitHubContributorsCard';
41
41
+
import { ProductHuntCardDefinition } from './ProductHuntCard';
41
42
// import { Model3DCardDefinition } from './Model3DCard';
42
43
43
44
export const AllCardDefinitions = [
···
80
81
AppleMusicCardDefinition,
81
82
// Model3DCardDefinition
82
83
FriendsCardDefinition,
83
83
-
GitHubContributorsCardDefinition
84
84
+
GitHubContributorsCardDefinition,
85
85
+
ProductHuntCardDefinition
84
86
] as const;
85
87
86
88
export const CardDefinitionsByType = AllCardDefinitions.reduce(