your personal website on atproto - mirror
blento.app
1import type { CardDefinition } from '../../types';
2import { listRecords, getRecord, resolveHandle } from '$lib/atproto';
3import SembleCollectionCard from './SembleCollectionCard.svelte';
4import CreateSembleCollectionCardModal from './CreateSembleCollectionCardModal.svelte';
5
6export type SembleCard = {
7 uri: string;
8 type: 'URL' | 'NOTE';
9 url?: string;
10 title?: string;
11 description?: string;
12 imageUrl?: string;
13 siteName?: string;
14 text?: string;
15 createdAt?: string;
16};
17
18export type SembleCollectionData = {
19 name: string;
20 description?: string;
21 cards: SembleCard[];
22};
23
24function parseSembleUrl(url: string) {
25 const match = url.match(/^https?:\/\/semble\.so\/profile\/([^/]+)\/collections\/([a-z0-9]+)$/);
26 if (!match) return null;
27 return { handle: match[1], rkey: match[2] };
28}
29
30async function loadCollectionData(
31 handle: string,
32 collectionRkey: string
33): Promise<SembleCollectionData | undefined> {
34 const did = await resolveHandle({ handle: handle as `${string}.${string}` });
35 if (!did) return undefined;
36
37 const collectionUri = `at://${did}/network.cosmik.collection/${collectionRkey}`;
38
39 const [collection, allLinks, allCards] = await Promise.all([
40 getRecord({
41 did,
42 collection: 'network.cosmik.collection',
43 rkey: collectionRkey
44 }).catch(() => undefined),
45 listRecords({ did, collection: 'network.cosmik.collectionLink', limit: 0 }).catch(() => []),
46 listRecords({ did, collection: 'network.cosmik.card', limit: 0 }).catch(() => [])
47 ]);
48
49 if (!collection) return undefined;
50
51 const linkedCardUris = new Set(
52 allLinks
53 .filter((link: any) => link.value.collection?.uri === collectionUri)
54 .map((link: any) => link.value.card?.uri)
55 );
56
57 const cards: SembleCard[] = allCards
58 .filter((card: any) => linkedCardUris.has(card.uri))
59 .map((card: any) => {
60 const v = card.value;
61 const content = v.content;
62 if (v.type === 'URL') {
63 return {
64 uri: card.uri,
65 type: 'URL' as const,
66 url: content?.url,
67 title: content?.metadata?.title,
68 description: content?.metadata?.description,
69 imageUrl: content?.metadata?.imageUrl,
70 siteName: content?.metadata?.siteName,
71 createdAt: v.createdAt
72 };
73 }
74 return {
75 uri: card.uri,
76 type: 'NOTE' as const,
77 text: content?.text,
78 createdAt: v.createdAt
79 };
80 });
81
82 return {
83 name: collection.value.name as string,
84 description: collection.value.description as string | undefined,
85 cards
86 };
87}
88
89export const SembleCollectionCardDefinition = {
90 type: 'sembleCollection',
91 contentComponent: SembleCollectionCard,
92 creationModalComponent: CreateSembleCollectionCardModal,
93 createNew: (card) => {
94 card.w = 4;
95 card.mobileW = 8;
96 card.h = 4;
97 card.mobileH = 6;
98 },
99 loadData: async (items) => {
100 const results: Record<string, SembleCollectionData> = {};
101 for (const item of items) {
102 const handle = item.cardData.handle;
103 const rkey = item.cardData.collectionRkey;
104 if (!handle || !rkey) continue;
105 try {
106 const data = await loadCollectionData(handle, rkey);
107 if (data) results[`${handle}/${rkey}`] = data;
108 } catch {
109 // skip failed fetches
110 }
111 }
112 return results;
113 },
114 onUrlHandler: (url, item) => {
115 const parsed = parseSembleUrl(url);
116 if (!parsed) return null;
117 item.cardData.handle = parsed.handle;
118 item.cardData.collectionRkey = parsed.rkey;
119 item.cardData.href = url;
120 item.w = 4;
121 item.mobileW = 8;
122 item.h = 4;
123 item.mobileH = 6;
124 return item;
125 },
126 urlHandlerPriority: 5,
127 minH: 2,
128
129 keywords: ['semble', 'collection', 'bookmarks', 'links', 'cards', 'cosmik'],
130 groups: ['Social'],
131 name: 'Semble Collection',
132 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="M2.25 12.75V12A2.25 2.25 0 0 1 4.5 9.75h15A2.25 2.25 0 0 1 21.75 12v.75m-8.69-6.44-2.12-2.12a1.5 1.5 0 0 0-1.061-.44H4.5A2.25 2.25 0 0 0 2.25 6v12a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9a2.25 2.25 0 0 0-2.25-2.25h-5.379a1.5 1.5 0 0 1-1.06-.44Z" /></svg>`
133} as CardDefinition & { type: 'sembleCollection' };