your personal website on atproto - mirror blento.app

Merge pull request #134 from flo-bit/product-hunt

add product hunt card

authored by Florian and committed by GitHub 14bb44d6 3edf5dd2

+143 -3
+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 - <div class={[item.cardData.label ? "pt-8" : '']}> 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 - card.cardData.label = ""; 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 + <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 parseEmbedCode(code: string): { 11 + imageSrc: string | null; 12 + linkHref: string | null; 13 + } { 14 + const normalized = code.replaceAll('&amp;', '&'); 15 + 16 + const srcMatch = normalized.match(/src="(https:\/\/api\.producthunt\.com\/[^"]+)"/); 17 + const imageSrc = srcMatch ? srcMatch[1] : null; 18 + 19 + const hrefMatch = normalized.match(/href="(https:\/\/www\.producthunt\.com\/[^"]+)"/); 20 + const linkHref = hrefMatch ? hrefMatch[1] : null; 21 + 22 + return { imageSrc, linkHref }; 23 + } 24 + 25 + function validate(): boolean { 26 + errorMessage = ''; 27 + 28 + const { imageSrc, linkHref } = parseEmbedCode(embedCode); 29 + 30 + if (!linkHref) { 31 + errorMessage = 'Could not find a Product Hunt link in the embed code'; 32 + return false; 33 + } 34 + 35 + if (!imageSrc) { 36 + errorMessage = 'Could not find a Product Hunt badge image in the embed code'; 37 + return false; 38 + } 39 + 40 + item.cardData.imageSrc = imageSrc; 41 + item.cardData.linkHref = linkHref; 42 + 43 + return true; 44 + } 45 + </script> 46 + 47 + <Modal open={true} closeButton={false}> 48 + <Subheading>Paste Product Hunt Embed Code</Subheading> 49 + 50 + <textarea 51 + bind:value={embedCode} 52 + placeholder="<a href=&quot;https://www.producthunt.com/posts/your-product?..." 53 + rows={5} 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 + ></textarea> 56 + 57 + {#if errorMessage} 58 + <Alert type="error" title="Invalid embed code"><span>{errorMessage}</span></Alert> 59 + {/if} 60 + 61 + <div class="mt-4 flex justify-end gap-2"> 62 + <Button onclick={oncancel} variant="ghost">Cancel</Button> 63 + <Button 64 + onclick={() => { 65 + if (validate()) oncreate(); 66 + }} 67 + > 68 + Create 69 + </Button> 70 + </div> 71 + </Modal>
+37
src/lib/cards/ProductHuntCard/ProductHuntCard.svelte
··· 1 + <script lang="ts"> 2 + import { getCanEdit } from '$lib/website/context'; 3 + import type { ContentComponentProps } from '../types'; 4 + 5 + let { item }: ContentComponentProps = $props(); 6 + 7 + let isEditing = getCanEdit(); 8 + 9 + let linkHref = $derived(item.cardData.linkHref || ''); 10 + let lightImageSrc = $derived( 11 + (item.cardData.imageSrc || '').replace(/theme=(light|dark|neutral)/, 'theme=light') 12 + ); 13 + let darkImageSrc = $derived( 14 + (item.cardData.imageSrc || '').replace(/theme=(light|dark|neutral)/, 'theme=dark') 15 + ); 16 + </script> 17 + 18 + <a 19 + href={linkHref} 20 + target="_blank" 21 + rel="noopener noreferrer" 22 + class={[ 23 + 'flex h-full w-full items-center justify-center p-4', 24 + isEditing() && 'pointer-events-none' 25 + ]} 26 + > 27 + <img 28 + src={lightImageSrc} 29 + alt="Product Hunt badge" 30 + class="max-h-full max-w-full object-contain dark:hidden" 31 + /> 32 + <img 33 + src={darkImageSrc} 34 + alt="Product Hunt badge" 35 + class="hidden max-h-full max-w-full object-contain dark:block" 36 + /> 37 + </a>
+30
src/lib/cards/ProductHuntCard/index.ts
··· 1 + import type { CardDefinition } from '../types'; 2 + import CreateProductHuntCardModal from './CreateProductHuntCardModal.svelte'; 3 + import ProductHuntCard from './ProductHuntCard.svelte'; 4 + 5 + const cardType = 'producthunt'; 6 + 7 + export const ProductHuntCardDefinition = { 8 + type: cardType, 9 + contentComponent: ProductHuntCard, 10 + creationModalComponent: CreateProductHuntCardModal, 11 + createNew: (item) => { 12 + item.cardType = cardType; 13 + item.cardData = {}; 14 + item.w = 4; 15 + item.h = 2; 16 + item.mobileW = 8; 17 + item.mobileH = 2; 18 + }, 19 + 20 + defaultColor: 'transparent', 21 + 22 + allowSetColor: false, 23 + 24 + minH: 1, 25 + 26 + name: 'Product Hunt', 27 + keywords: ['producthunt', 'product', 'launch', 'badge'], 28 + groups: ['Social'], 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 + } 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 + 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 - GitHubContributorsCardDefinition 84 + GitHubContributorsCardDefinition, 85 + ProductHuntCardDefinition 84 86 ] as const; 85 87 86 88 export const CardDefinitionsByType = AllCardDefinitions.reduce(