your personal website on atproto - mirror blento.app

Merge pull request #120 from unbedenklich/AllCardsWebsite

copy pages and add allcards to page in dev mode

authored by Florian and committed by GitHub dcfa583a fe185592

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