diff --git a/src/lib/cards/BaseCard/BaseEditingCard.svelte b/src/lib/cards/BaseCard/BaseEditingCard.svelte
index 852289c..93d80de 100644
--- a/src/lib/cards/BaseCard/BaseEditingCard.svelte
+++ b/src/lib/cards/BaseCard/BaseEditingCard.svelte
@@ -9,6 +9,7 @@
import { COLUMNS } from '$lib';
import { getCanEdit, getIsMobile } from '$lib/website/context';
import PlainTextEditor from '$lib/components/PlainTextEditor.svelte';
+ import { fixAllCollisions, fixCollisions } from '$lib/helper';
let colorsChoices = [
{ class: 'text-base-500', label: 'base' },
@@ -55,7 +56,7 @@
let colorPopoverOpen = $state(false);
- const cardDef = $derived(CardDefinitionsByType[item.cardType]);
+ const cardDef = $derived(CardDefinitionsByType[item.cardType] ?? {});
const minW = $derived(cardDef.minW ?? (isMobile() ? 2 : 2));
const minH = $derived(cardDef.minH ?? (isMobile() ? 2 : 2));
diff --git a/src/lib/cards/SpotifyCard/CreateSpotifyCardModal.svelte b/src/lib/cards/SpotifyCard/CreateSpotifyCardModal.svelte
new file mode 100644
index 0000000..5fe88c2
--- /dev/null
+++ b/src/lib/cards/SpotifyCard/CreateSpotifyCardModal.svelte
@@ -0,0 +1,51 @@
+
+
+
+ Enter a Spotify album or playlist URL
+ {
+ if (e.key === 'Enter' && checkUrl()) oncreate();
+ }}
+ />
+
+ {#if errorMessage}
+ {errorMessage}
+ {/if}
+
+
+
+
+
+
diff --git a/src/lib/cards/SpotifyCard/SpotifyCard.svelte b/src/lib/cards/SpotifyCard/SpotifyCard.svelte
new file mode 100644
index 0000000..47580ca
--- /dev/null
+++ b/src/lib/cards/SpotifyCard/SpotifyCard.svelte
@@ -0,0 +1,23 @@
+
+
+{#if item.cardData?.spotifyType && item.cardData?.spotifyId}
+
+
+
+{:else}
+
+ Missing Spotify data
+
+{/if}
diff --git a/src/lib/cards/SpotifyCard/index.ts b/src/lib/cards/SpotifyCard/index.ts
new file mode 100644
index 0000000..9db2828
--- /dev/null
+++ b/src/lib/cards/SpotifyCard/index.ts
@@ -0,0 +1,66 @@
+import type { CardDefinition } from '../types';
+import CreateSpotifyCardModal from './CreateSpotifyCardModal.svelte';
+import SpotifyCard from './SpotifyCard.svelte';
+
+const cardType = 'spotify-list-embed';
+
+export const SpotifyCardDefinition = {
+ type: cardType,
+ contentComponent: SpotifyCard,
+ creationModalComponent: CreateSpotifyCardModal,
+ sidebarButtonText: 'Spotify Embed',
+
+ createNew: (item) => {
+ item.cardType = cardType;
+ item.cardData = {};
+ item.w = 4;
+ item.mobileW = 8;
+ item.h = 5;
+ item.mobileH = 10;
+ },
+
+ onUrlHandler: (url, item) => {
+ const match = matchSpotifyUrl(url);
+ if (!match) return null;
+
+ item.cardData.spotifyType = match.type;
+ item.cardData.spotifyId = match.id;
+ item.cardData.href = url;
+
+ item.w = 4;
+ item.mobileW = 8;
+ item.h = 5;
+ item.mobileH = 10;
+
+ return item;
+ },
+
+ urlHandlerPriority: 2,
+
+ name: 'Spotify Embed',
+ canResize: true,
+ minW: 4,
+ minH: 5
+} as CardDefinition & { type: typeof cardType };
+
+// Match Spotify album and playlist URLs
+// Examples:
+// https://open.spotify.com/album/1DFixLWuPkv3KT3TnV35m3
+// https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M
+function matchSpotifyUrl(
+ url: string | undefined
+): { type: 'album' | 'playlist'; id: string } | null {
+ if (!url) return null;
+
+ const pattern = /open\.spotify\.com\/(album|playlist)\/([a-zA-Z0-9]+)/;
+ const match = url.match(pattern);
+
+ if (match) {
+ return {
+ type: match[1] as 'album' | 'playlist',
+ id: match[2]
+ };
+ }
+
+ return null;
+}
diff --git a/src/lib/cards/index.ts b/src/lib/cards/index.ts
index b86875f..9302990 100644
--- a/src/lib/cards/index.ts
+++ b/src/lib/cards/index.ts
@@ -30,6 +30,7 @@ import { EventCardDefinition } from './EventCard';
import { VCardCardDefinition } from './VCardCard';
import { DrawCardDefinition } from './DrawCard';
import { TimerCardDefinition } from './TimerCard';
+import { SpotifyCardDefinition } from './SpotifyCard';
export const AllCardDefinitions = [
ImageCardDefinition,
@@ -62,7 +63,8 @@ export const AllCardDefinitions = [
EventCardDefinition,
VCardCardDefinition,
DrawCardDefinition,
- TimerCardDefinition
+ TimerCardDefinition,
+ SpotifyCardDefinition
] as const;
export const CardDefinitionsByType = AllCardDefinitions.reduce(