your personal website on atproto - mirror blento.app

add kickstarter card

+179 -5
+7 -1
src/lib/cards/BigSocialCard/index.ts
··· 61 61 'github', 62 62 'discord', 63 63 'linkedin', 64 - 'mastodon' 64 + 'mastodon', 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 + siKickstarter, 106 108 type SimpleIcon 107 109 } from 'simple-icons'; 108 110 ··· 159 161 160 162 ao3: /(?:archiveofourown\.org)/i, 161 163 164 + kickstarter: /(?:kickstarter\.com)/i, 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 + 266 + kickstarter: siKickstarter, 261 267 262 268 tangled: { 263 269 slug: 'tangled',
+89
src/lib/cards/KickstarterCard/CreateKickstarterCardModal.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 parseInput(code: string): { 11 + src: string | null; 12 + widgetType: 'card' | 'video'; 13 + } { 14 + const normalized = code.trim().replaceAll('&amp;', '&'); 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
··· 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
··· 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 39 import { FriendsCardDefinition } from './FriendsCard'; 40 40 import { GitHubContributorsCardDefinition } from './GitHubContributorsCard'; 41 41 import { ProductHuntCardDefinition } from './ProductHuntCard'; 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 - ProductHuntCardDefinition 86 + ProductHuntCardDefinition, 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 - let hasUnsavedChanges = $derived( 78 - JSON.stringify(items) !== savedItems || JSON.stringify(data.publication) !== savedPublication 79 - ); 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 + }); 80 86 81 87 // Warn user before closing tab if there are unsaved changes 82 88 $effect(() => {