tangled
alpha
login
or
join now
flo-bit.dev
/
blento
21
fork
atom
your personal website on atproto - mirror
blento.app
21
fork
atom
overview
issues
pulls
pipelines
copy pages and add allcards to page in dev mode
unbedenklich
1 month ago
37aab1b8
6e82c6b7
+291
-4
2 changed files
expand all
collapse all
unified
split
src
lib
cards
index.ts
website
EditableWebsite.svelte
+1
-1
src/lib/cards/index.ts
···
49
49
LatestBlueskyPostCardDefinition,
50
50
LivestreamCardDefitition,
51
51
LivestreamEmbedCardDefitition,
52
52
-
EmbedCardDefinition,
52
52
+
// EmbedCardDefinition,
53
53
MapCardDefinition,
54
54
ATProtoCollectionsCardDefinition,
55
55
SectionCardDefinition,
+290
-3
src/lib/website/EditableWebsite.svelte
···
7
7
compactItems,
8
8
createEmptyCard,
9
9
findValidPosition,
10
10
+
fixAllCollisions,
10
11
fixCollisions,
11
12
getHideProfileSection,
12
13
getProfilePosition,
···
35
36
import EditBar from './EditBar.svelte';
36
37
import SaveModal from './SaveModal.svelte';
37
38
import FloatingEditButton from './FloatingEditButton.svelte';
38
38
-
import { user } from '$lib/atproto';
39
39
+
import { user, resolveHandle, listRecords, getCDNImageBlobUrl } from '$lib/atproto';
40
40
+
import * as TID from '@atcute/tid';
39
41
import { launchConfetti } from '@foxui/visual';
40
42
import Controls from './Controls.svelte';
41
43
import CardCommand from '$lib/components/card-command/CardCommand.svelte';
···
257
259
}
258
260
259
261
const sidebarItems = AllCardDefinitions.filter((cardDef) => cardDef.name);
262
262
+
263
263
+
function addAllCardTypes() {
264
264
+
const groupOrder = ['Core', 'Social', 'Media', 'Content', 'Visual', 'Utilities', 'Games'];
265
265
+
const grouped = new Map<string, CardDefinition[]>();
266
266
+
267
267
+
for (const def of AllCardDefinitions) {
268
268
+
if (!def.name) continue;
269
269
+
const group = def.groups?.[0] ?? 'Other';
270
270
+
if (!grouped.has(group)) grouped.set(group, []);
271
271
+
grouped.get(group)!.push(def);
272
272
+
}
273
273
+
274
274
+
// Sort groups by predefined order, unknowns at end
275
275
+
const sortedGroups = [...grouped.keys()].sort((a, b) => {
276
276
+
const ai = groupOrder.indexOf(a);
277
277
+
const bi = groupOrder.indexOf(b);
278
278
+
return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
279
279
+
});
280
280
+
281
281
+
// Sample data for cards that would otherwise render empty
282
282
+
const sampleData: Record<string, Record<string, unknown>> = {
283
283
+
text: { text: 'The quick brown fox jumps over the lazy dog. This is a sample text card.' },
284
284
+
link: {
285
285
+
href: 'https://bsky.app',
286
286
+
title: 'Bluesky',
287
287
+
domain: 'bsky.app',
288
288
+
description: 'Social networking that gives you choice',
289
289
+
hasFetched: true
290
290
+
},
291
291
+
image: {
292
292
+
image: 'https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=600',
293
293
+
alt: 'Mountain landscape'
294
294
+
},
295
295
+
button: { text: 'Visit Bluesky', href: 'https://bsky.app' },
296
296
+
bigsocial: { platform: 'bluesky', href: 'https://bsky.app', color: '0085ff' },
297
297
+
blueskyPost: {
298
298
+
uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jt64kgkbbs2y',
299
299
+
href: 'https://bsky.app/profile/bsky.app/post/3jt64kgkbbs2y'
300
300
+
},
301
301
+
blueskyProfile: {
302
302
+
handle: 'bsky.app',
303
303
+
displayName: 'Bluesky',
304
304
+
avatar:
305
305
+
'https://cdn.bsky.app/img/avatar/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkreihagr2cmvl2jt4mgx3sppwe2it3fwolkrbtjrhcnwjk4pcnbaq53m@jpeg'
306
306
+
},
307
307
+
blueskyMedia: {},
308
308
+
latestPost: {},
309
309
+
youtubeVideo: {
310
310
+
youtubeId: 'dQw4w9WgXcQ',
311
311
+
poster: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg',
312
312
+
href: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
313
313
+
showInline: true
314
314
+
},
315
315
+
'spotify-list-embed': {
316
316
+
spotifyType: 'album',
317
317
+
spotifyId: '4aawyAB9vmqN3uQ7FjRGTy',
318
318
+
href: 'https://open.spotify.com/album/4aawyAB9vmqN3uQ7FjRGTy'
319
319
+
},
320
320
+
latestLivestream: {},
321
321
+
livestreamEmbed: {
322
322
+
href: 'https://stream.place/',
323
323
+
embed: 'https://stream.place/embed/'
324
324
+
},
325
325
+
mapLocation: { lat: 48.8584, lon: 2.2945, zoom: 13, name: 'Eiffel Tower, Paris' },
326
326
+
gif: { url: 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.mp4', alt: 'Cat typing' },
327
327
+
event: {
328
328
+
uri: 'at://did:plc:257wekqxg4hyapkq6k47igmp/community.lexicon.calendar.event/3mcsoqzy7gm2q'
329
329
+
},
330
330
+
guestbook: { label: 'Guestbook' },
331
331
+
githubProfile: { user: 'sveltejs', href: 'https://github.com/sveltejs' },
332
332
+
photoGallery: {
333
333
+
galleryUri: 'at://did:plc:tas6hj2xjrqben5653v5kohk/social.grain.gallery/3mclhsljs6h2w'
334
334
+
},
335
335
+
atprotocollections: {},
336
336
+
publicationList: {},
337
337
+
recentPopfeedReviews: {},
338
338
+
recentTealFMPlays: {},
339
339
+
statusphere: { emoji: '✨' },
340
340
+
vcard: {},
341
341
+
'fluid-text': { text: 'Hello World' },
342
342
+
draw: { strokesJson: '[]', viewBox: '', strokeWidth: 1, locked: true },
343
343
+
clock: {},
344
344
+
countdown: { targetDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() },
345
345
+
timer: {},
346
346
+
'dino-game': {},
347
347
+
tetris: {},
348
348
+
updatedBlentos: {}
349
349
+
};
350
350
+
351
351
+
// Labels for cards that support canHaveLabel
352
352
+
const sampleLabels: Record<string, string> = {
353
353
+
image: 'Mountain Landscape',
354
354
+
mapLocation: 'Eiffel Tower',
355
355
+
gif: 'Cat Typing',
356
356
+
bigsocial: 'Bluesky',
357
357
+
guestbook: 'Guestbook',
358
358
+
statusphere: 'My Status',
359
359
+
recentPopfeedReviews: 'My Reviews',
360
360
+
recentTealFMPlays: 'Recently Played',
361
361
+
clock: 'Local Time',
362
362
+
countdown: 'Launch Day',
363
363
+
timer: 'Timer',
364
364
+
'dino-game': 'Dino Game',
365
365
+
tetris: 'Tetris',
366
366
+
blueskyMedia: 'Bluesky Media'
367
367
+
};
368
368
+
369
369
+
const newItems: Item[] = [];
370
370
+
let cursorY = 0;
371
371
+
let mobileCursorY = 0;
372
372
+
373
373
+
for (const group of sortedGroups) {
374
374
+
const defs = grouped.get(group)!;
375
375
+
376
376
+
// Add a section heading for the group
377
377
+
const heading = createEmptyCard(data.page);
378
378
+
heading.cardType = 'section';
379
379
+
heading.cardData = { text: group, verticalAlign: 'bottom', textSize: 1 };
380
380
+
heading.w = COLUMNS;
381
381
+
heading.h = 1;
382
382
+
heading.x = 0;
383
383
+
heading.y = cursorY;
384
384
+
heading.mobileW = COLUMNS;
385
385
+
heading.mobileH = 2;
386
386
+
heading.mobileX = 0;
387
387
+
heading.mobileY = mobileCursorY;
388
388
+
newItems.push(heading);
389
389
+
cursorY += 1;
390
390
+
mobileCursorY += 2;
391
391
+
392
392
+
// Place cards in rows
393
393
+
let rowX = 0;
394
394
+
let rowMaxH = 0;
395
395
+
let mobileRowX = 0;
396
396
+
let mobileRowMaxH = 0;
397
397
+
398
398
+
for (const def of defs) {
399
399
+
if (def.type === 'section' || def.type === 'embed') continue;
400
400
+
401
401
+
const item = createEmptyCard(data.page);
402
402
+
item.cardType = def.type;
403
403
+
item.cardData = {};
404
404
+
def.createNew?.(item);
405
405
+
406
406
+
// Merge in sample data (without overwriting createNew defaults)
407
407
+
const extra = sampleData[def.type];
408
408
+
if (extra) {
409
409
+
item.cardData = { ...item.cardData, ...extra };
410
410
+
}
411
411
+
412
412
+
// Set item-level color for cards that need it
413
413
+
if (def.type === 'button') {
414
414
+
item.color = 'transparent';
415
415
+
}
416
416
+
417
417
+
// Add label if card supports it
418
418
+
const label = sampleLabels[def.type];
419
419
+
if (label && def.canHaveLabel) {
420
420
+
item.cardData.label = label;
421
421
+
}
422
422
+
423
423
+
// Desktop layout
424
424
+
if (rowX + item.w > COLUMNS) {
425
425
+
cursorY += rowMaxH;
426
426
+
rowX = 0;
427
427
+
rowMaxH = 0;
428
428
+
}
429
429
+
item.x = rowX;
430
430
+
item.y = cursorY;
431
431
+
rowX += item.w;
432
432
+
rowMaxH = Math.max(rowMaxH, item.h);
433
433
+
434
434
+
// Mobile layout
435
435
+
if (mobileRowX + item.mobileW > COLUMNS) {
436
436
+
mobileCursorY += mobileRowMaxH;
437
437
+
mobileRowX = 0;
438
438
+
mobileRowMaxH = 0;
439
439
+
}
440
440
+
item.mobileX = mobileRowX;
441
441
+
item.mobileY = mobileCursorY;
442
442
+
mobileRowX += item.mobileW;
443
443
+
mobileRowMaxH = Math.max(mobileRowMaxH, item.mobileH);
444
444
+
445
445
+
newItems.push(item);
446
446
+
}
447
447
+
448
448
+
// Move cursor past last row
449
449
+
cursorY += rowMaxH;
450
450
+
mobileCursorY += mobileRowMaxH;
451
451
+
}
452
452
+
453
453
+
items = newItems;
454
454
+
onLayoutChanged();
455
455
+
}
456
456
+
457
457
+
let copyInput = $state('');
458
458
+
let isCopying = $state(false);
459
459
+
460
460
+
async function copyPageFrom() {
461
461
+
const input = copyInput.trim();
462
462
+
if (!input) return;
463
463
+
464
464
+
isCopying = true;
465
465
+
try {
466
466
+
// Parse "handle" or "handle/page"
467
467
+
const parts = input.split('/');
468
468
+
const handle = parts[0];
469
469
+
const pageName = parts[1] || 'self';
470
470
+
471
471
+
const did = await resolveHandle({ handle: handle as `${string}.${string}` });
472
472
+
if (!did) throw new Error('Could not resolve handle');
473
473
+
474
474
+
const records = await listRecords({ did, collection: 'app.blento.card' });
475
475
+
const targetPage = 'blento.' + pageName;
476
476
+
477
477
+
const copiedCards: Item[] = records
478
478
+
.map((r) => ({ ...r.value }) as Item)
479
479
+
.filter((card) => {
480
480
+
// v0/v1 cards without page field belong to blento.self
481
481
+
if (!card.page) return targetPage === 'blento.self';
482
482
+
return card.page === targetPage;
483
483
+
})
484
484
+
.map((card) => {
485
485
+
// Apply v0→v1 migration (coords were halved in old format)
486
486
+
if (!card.version) {
487
487
+
card.x *= 2;
488
488
+
card.y *= 2;
489
489
+
card.h *= 2;
490
490
+
card.w *= 2;
491
491
+
card.mobileX *= 2;
492
492
+
card.mobileY *= 2;
493
493
+
card.mobileH *= 2;
494
494
+
card.mobileW *= 2;
495
495
+
card.version = 1;
496
496
+
}
497
497
+
498
498
+
// Convert blob refs to CDN URLs using source DID
499
499
+
if (card.cardData) {
500
500
+
for (const key of Object.keys(card.cardData)) {
501
501
+
const val = card.cardData[key];
502
502
+
if (val && typeof val === 'object' && val.$type === 'blob') {
503
503
+
const url = getCDNImageBlobUrl({ did, blob: val });
504
504
+
if (url) card.cardData[key] = url;
505
505
+
}
506
506
+
}
507
507
+
}
508
508
+
509
509
+
// Regenerate ID and assign to current page
510
510
+
card.id = TID.now();
511
511
+
card.page = data.page;
512
512
+
return card;
513
513
+
});
514
514
+
515
515
+
if (copiedCards.length === 0) {
516
516
+
toast.error('No cards found on that page');
517
517
+
return;
518
518
+
}
519
519
+
520
520
+
fixAllCollisions(copiedCards);
521
521
+
fixAllCollisions(copiedCards, true);
522
522
+
compactItems(copiedCards);
523
523
+
compactItems(copiedCards, true);
524
524
+
525
525
+
items = copiedCards;
526
526
+
onLayoutChanged();
527
527
+
toast.success(`Copied ${copiedCards.length} cards from ${handle}`);
528
528
+
} catch (e) {
529
529
+
console.error('Failed to copy page:', e);
530
530
+
toast.error('Failed to copy page');
531
531
+
} finally {
532
532
+
isCopying = false;
533
533
+
}
534
534
+
}
260
535
261
536
let debugPoint = $state({ x: 0, y: 0 });
262
537
···
1152
1427
1153
1428
{#if dev}
1154
1429
<div
1155
1155
-
class="bg-base-900/70 text-base-100 fixed top-2 right-2 z-50 rounded px-2 py-1 font-mono text-xs"
1430
1430
+
class="bg-base-900/70 text-base-100 fixed top-2 right-2 z-50 flex items-center gap-2 rounded px-2 py-1 font-mono text-xs"
1156
1431
>
1157
1157
-
editedOn: {editedOn}
1432
1432
+
<span>editedOn: {editedOn}</span>
1433
1433
+
<button class="underline" onclick={addAllCardTypes}>+ all cards</button>
1434
1434
+
<input
1435
1435
+
bind:value={copyInput}
1436
1436
+
placeholder="handle/page"
1437
1437
+
class="bg-base-800 text-base-100 w-32 rounded px-1 py-0.5"
1438
1438
+
onkeydown={(e) => {
1439
1439
+
if (e.key === 'Enter') copyPageFrom();
1440
1440
+
}}
1441
1441
+
/>
1442
1442
+
<button class="underline" onclick={copyPageFrom} disabled={isCopying}>
1443
1443
+
{isCopying ? 'copying...' : 'copy'}
1444
1444
+
</button>
1158
1445
</div>
1159
1446
{/if}
1160
1447
</Context>