your personal website on atproto - mirror blento.app

Merge pull request #64 from unbedenklich/music-streaming-card

spotify card

authored by Florian and committed by GitHub 5b5239f2 e21d9dfc

+144 -1
+2 -1
src/lib/cards/BaseCard/BaseEditingCard.svelte
··· 9 9 import { COLUMNS } from '$lib'; 10 10 import { getCanEdit, getIsMobile } from '$lib/website/context'; 11 11 import PlainTextEditor from '$lib/components/PlainTextEditor.svelte'; 12 + import { fixAllCollisions, fixCollisions } from '$lib/helper'; 12 13 13 14 let colorsChoices = [ 14 15 { class: 'text-base-500', label: 'base' }, ··· 55 56 56 57 let colorPopoverOpen = $state(false); 57 58 58 - const cardDef = $derived(CardDefinitionsByType[item.cardType]); 59 + const cardDef = $derived(CardDefinitionsByType[item.cardType] ?? {}); 59 60 60 61 const minW = $derived(cardDef.minW ?? (isMobile() ? 2 : 2)); 61 62 const minH = $derived(cardDef.minH ?? (isMobile() ? 2 : 2));
+51
src/lib/cards/SpotifyCard/CreateSpotifyCardModal.svelte
··· 1 + <script lang="ts"> 2 + import { Alert, Button, Input, Modal, Subheading } from '@foxui/core'; 3 + import type { CreationModalComponentProps } from '../types'; 4 + 5 + let { item = $bindable(), oncreate, oncancel }: CreationModalComponentProps = $props(); 6 + 7 + let errorMessage = $state(''); 8 + 9 + function checkUrl() { 10 + errorMessage = ''; 11 + 12 + const pattern = /open\.spotify\.com\/(album|playlist)\/([a-zA-Z0-9]+)/; 13 + const match = item.cardData.href?.match(pattern); 14 + 15 + if (!match) { 16 + errorMessage = 'Please enter a valid Spotify album or playlist URL'; 17 + return false; 18 + } 19 + 20 + item.cardData.spotifyType = match[1]; 21 + item.cardData.spotifyId = match[2]; 22 + 23 + return true; 24 + } 25 + </script> 26 + 27 + <Modal open={true} closeButton={false}> 28 + <Subheading>Enter a Spotify album or playlist URL</Subheading> 29 + <Input 30 + bind:value={item.cardData.href} 31 + placeholder="https://open.spotify.com/album/..." 32 + onkeydown={(e) => { 33 + if (e.key === 'Enter' && checkUrl()) oncreate(); 34 + }} 35 + /> 36 + 37 + {#if errorMessage} 38 + <Alert type="error" title="Invalid URL"><span>{errorMessage}</span></Alert> 39 + {/if} 40 + 41 + <div class="mt-4 flex justify-end gap-2"> 42 + <Button onclick={oncancel} variant="ghost">Cancel</Button> 43 + <Button 44 + onclick={() => { 45 + if (checkUrl()) oncreate(); 46 + }} 47 + > 48 + Create 49 + </Button> 50 + </div> 51 + </Modal>
+23
src/lib/cards/SpotifyCard/SpotifyCard.svelte
··· 1 + <script lang="ts"> 2 + import type { ContentComponentProps } from '../types'; 3 + 4 + let { item, isEditing }: ContentComponentProps = $props(); 5 + </script> 6 + 7 + {#if item.cardData?.spotifyType && item.cardData?.spotifyId} 8 + <div class="absolute inset-0 p-2"> 9 + <iframe 10 + class={['h-full w-full rounded-2xl', isEditing && 'pointer-events-none']} 11 + src="https://open.spotify.com/embed/{item.cardData.spotifyType}/{item.cardData 12 + .spotifyId}?utm_source=generator&theme=0" 13 + frameborder="0" 14 + allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" 15 + loading="lazy" 16 + title="Spotify {item.cardData.spotifyType}" 17 + ></iframe> 18 + </div> 19 + {:else} 20 + <div class="flex h-full items-center justify-center p-4 text-center opacity-50"> 21 + Missing Spotify data 22 + </div> 23 + {/if}
+66
src/lib/cards/SpotifyCard/index.ts
··· 1 + import type { CardDefinition } from '../types'; 2 + import CreateSpotifyCardModal from './CreateSpotifyCardModal.svelte'; 3 + import SpotifyCard from './SpotifyCard.svelte'; 4 + 5 + const cardType = 'spotify-list-embed'; 6 + 7 + export const SpotifyCardDefinition = { 8 + type: cardType, 9 + contentComponent: SpotifyCard, 10 + creationModalComponent: CreateSpotifyCardModal, 11 + sidebarButtonText: 'Spotify Embed', 12 + 13 + createNew: (item) => { 14 + item.cardType = cardType; 15 + item.cardData = {}; 16 + item.w = 4; 17 + item.mobileW = 8; 18 + item.h = 5; 19 + item.mobileH = 10; 20 + }, 21 + 22 + onUrlHandler: (url, item) => { 23 + const match = matchSpotifyUrl(url); 24 + if (!match) return null; 25 + 26 + item.cardData.spotifyType = match.type; 27 + item.cardData.spotifyId = match.id; 28 + item.cardData.href = url; 29 + 30 + item.w = 4; 31 + item.mobileW = 8; 32 + item.h = 5; 33 + item.mobileH = 10; 34 + 35 + return item; 36 + }, 37 + 38 + urlHandlerPriority: 2, 39 + 40 + name: 'Spotify Embed', 41 + canResize: true, 42 + minW: 4, 43 + minH: 5 44 + } as CardDefinition & { type: typeof cardType }; 45 + 46 + // Match Spotify album and playlist URLs 47 + // Examples: 48 + // https://open.spotify.com/album/1DFixLWuPkv3KT3TnV35m3 49 + // https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M 50 + function matchSpotifyUrl( 51 + url: string | undefined 52 + ): { type: 'album' | 'playlist'; id: string } | null { 53 + if (!url) return null; 54 + 55 + const pattern = /open\.spotify\.com\/(album|playlist)\/([a-zA-Z0-9]+)/; 56 + const match = url.match(pattern); 57 + 58 + if (match) { 59 + return { 60 + type: match[1] as 'album' | 'playlist', 61 + id: match[2] 62 + }; 63 + } 64 + 65 + return null; 66 + }
+2
src/lib/cards/index.ts
··· 30 30 import { VCardCardDefinition } from './VCardCard'; 31 31 import { DrawCardDefinition } from './DrawCard'; 32 32 import { TimerCardDefinition } from './TimerCard'; 33 + import { SpotifyCardDefinition } from './SpotifyCard'; 33 34 import { Model3DCardDefinition } from './Model3DCard'; 34 35 35 36 export const AllCardDefinitions = [ ··· 64 65 VCardCardDefinition, 65 66 DrawCardDefinition, 66 67 TimerCardDefinition, 68 + SpotifyCardDefinition, 67 69 Model3DCardDefinition 68 70 ] as const; 69 71