Tools for the Atmosphere tools.slices.network
quickslice atproto html

docs: add tangled repo search implementation plan

+981
+981
docs/plans/2025-12-18-tangled-repo-search.md
··· 1 + # Tangled Repo Search Implementation Plan 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Build a single-page HTML tool to browse and search Tangled repositories with Catppuccin theming. 6 + 7 + **Architecture:** Single HTML file with embedded CSS and JS. Uses quickslice-client SDK to query GraphQL endpoint. Server-side search with OR/contains across name, description, actorHandle. Catppuccin Latte (light) / Mocha (dark) based on system preference. 8 + 9 + **Tech Stack:** HTML, CSS (variables + prefers-color-scheme), vanilla JS, quickslice GraphQL API 10 + 11 + --- 12 + 13 + ### Task 1: Create HTML Structure 14 + 15 + **Files:** 16 + - Create: `tangled-repos.html` 17 + 18 + **Step 1: Create the base HTML file with structure** 19 + 20 + Create `tangled-repos.html` with the basic HTML structure: 21 + 22 + ```html 23 + <!doctype html> 24 + <html lang="en"> 25 + <head> 26 + <meta charset="UTF-8" /> 27 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 28 + <meta 29 + http-equiv="Content-Security-Policy" 30 + content="default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; connect-src 'self' https://quickslice-production-ddc3.up.railway.app; img-src 'self' https: data:;" 31 + /> 32 + <title>Tangled Repos</title> 33 + <style> 34 + /* Styles will be added in Task 2 */ 35 + </style> 36 + </head> 37 + <body> 38 + <div id="app"> 39 + <header> 40 + <h1>Tangled Repos</h1> 41 + <p class="tagline">Browse repositories from the Atmosphere</p> 42 + </header> 43 + <div class="search-container"> 44 + <input type="text" id="search-input" placeholder="Search repos..." /> 45 + <button id="clear-search" class="hidden" title="Clear search">&times;</button> 46 + </div> 47 + <div id="result-count"></div> 48 + <main> 49 + <div id="repo-feed"></div> 50 + <div id="load-more"></div> 51 + </main> 52 + <div id="error-banner" class="hidden"></div> 53 + </div> 54 + 55 + <!-- Quickslice Client SDK --> 56 + <script src="https://cdn.jsdelivr.net/gh/bigmoves/quickslice@main/quickslice-client-js/dist/quickslice-client.min.js"></script> 57 + 58 + <script> 59 + // JavaScript will be added in Task 3+ 60 + </script> 61 + </body> 62 + </html> 63 + ``` 64 + 65 + **Step 2: Verify file opens in browser** 66 + 67 + Open in browser and verify basic structure renders. 68 + 69 + **Step 3: Commit** 70 + 71 + ```bash 72 + git add tangled-repos.html 73 + git commit -m "feat(tangled): add base HTML structure" 74 + ``` 75 + 76 + --- 77 + 78 + ### Task 2: Add Catppuccin CSS Theming 79 + 80 + **Files:** 81 + - Modify: `tangled-repos.html` (style section) 82 + 83 + **Step 1: Add CSS reset and Catppuccin variables** 84 + 85 + Replace the `<style>` section with: 86 + 87 + ```css 88 + /* CSS Reset */ 89 + *, 90 + *::before, 91 + *::after { 92 + box-sizing: border-box; 93 + } 94 + * { 95 + margin: 0; 96 + } 97 + body { 98 + line-height: 1.5; 99 + -webkit-font-smoothing: antialiased; 100 + } 101 + input, 102 + button { 103 + font: inherit; 104 + } 105 + 106 + /* Catppuccin Latte (Light) */ 107 + :root { 108 + --bg-base: #eff1f5; 109 + --bg-mantle: #e6e9ef; 110 + --bg-surface0: #ccd0da; 111 + --bg-surface1: #bcc0cc; 112 + --text-primary: #4c4f69; 113 + --text-secondary: #6c6f85; 114 + --text-subtext: #7c7f93; 115 + --accent: #1e66f5; 116 + --accent-hover: #2a6ff7; 117 + --border: #ccd0da; 118 + --error-bg: #fce4e6; 119 + --error-border: #e64553; 120 + --error-text: #d20f39; 121 + --star-color: #df8e1d; 122 + --topic-bg: #dce0e8; 123 + --topic-text: #5c5f77; 124 + } 125 + 126 + /* Catppuccin Mocha (Dark) */ 127 + @media (prefers-color-scheme: dark) { 128 + :root { 129 + --bg-base: #1e1e2e; 130 + --bg-mantle: #181825; 131 + --bg-surface0: #313244; 132 + --bg-surface1: #45475a; 133 + --text-primary: #cdd6f4; 134 + --text-secondary: #a6adc8; 135 + --text-subtext: #bac2de; 136 + --accent: #89b4fa; 137 + --accent-hover: #9cc4fc; 138 + --border: #313244; 139 + --error-bg: #45293b; 140 + --error-border: #f38ba8; 141 + --error-text: #f38ba8; 142 + --star-color: #f9e2af; 143 + --topic-bg: #313244; 144 + --topic-text: #bac2de; 145 + } 146 + } 147 + 148 + body { 149 + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 150 + background: var(--bg-base); 151 + color: var(--text-primary); 152 + min-height: 100vh; 153 + padding: 2rem 1rem; 154 + } 155 + 156 + #app { 157 + max-width: 700px; 158 + margin: 0 auto; 159 + } 160 + 161 + /* Header */ 162 + header { 163 + text-align: center; 164 + margin-bottom: 1.5rem; 165 + } 166 + 167 + header h1 { 168 + font-size: 2rem; 169 + color: var(--accent); 170 + margin-bottom: 0.25rem; 171 + } 172 + 173 + .tagline { 174 + color: var(--text-secondary); 175 + font-size: 0.875rem; 176 + } 177 + 178 + /* Search */ 179 + .search-container { 180 + position: relative; 181 + margin-bottom: 1rem; 182 + } 183 + 184 + #search-input { 185 + width: 100%; 186 + padding: 0.75rem 2.5rem 0.75rem 1rem; 187 + border: 1px solid var(--border); 188 + border-radius: 0.5rem; 189 + background: var(--bg-mantle); 190 + color: var(--text-primary); 191 + font-size: 1rem; 192 + } 193 + 194 + #search-input::placeholder { 195 + color: var(--text-secondary); 196 + } 197 + 198 + #search-input:focus { 199 + outline: none; 200 + border-color: var(--accent); 201 + } 202 + 203 + #clear-search { 204 + position: absolute; 205 + right: 0.5rem; 206 + top: 50%; 207 + transform: translateY(-50%); 208 + background: none; 209 + border: none; 210 + color: var(--text-secondary); 211 + font-size: 1.25rem; 212 + cursor: pointer; 213 + padding: 0.25rem 0.5rem; 214 + line-height: 1; 215 + } 216 + 217 + #clear-search:hover { 218 + color: var(--text-primary); 219 + } 220 + 221 + #result-count { 222 + color: var(--text-secondary); 223 + font-size: 0.875rem; 224 + margin-bottom: 1rem; 225 + min-height: 1.25rem; 226 + } 227 + 228 + /* Cards */ 229 + .card { 230 + background: var(--bg-mantle); 231 + border-radius: 0.5rem; 232 + padding: 1rem; 233 + margin-bottom: 0.75rem; 234 + border: 1px solid var(--border); 235 + } 236 + 237 + .card:hover { 238 + border-color: var(--bg-surface1); 239 + } 240 + 241 + /* Repo Card Header */ 242 + .repo-header { 243 + display: flex; 244 + align-items: center; 245 + gap: 0.75rem; 246 + margin-bottom: 0.75rem; 247 + } 248 + 249 + .repo-avatar { 250 + width: 36px; 251 + height: 36px; 252 + border-radius: 50%; 253 + background: var(--bg-surface0); 254 + overflow: hidden; 255 + flex-shrink: 0; 256 + } 257 + 258 + .repo-avatar img { 259 + width: 100%; 260 + height: 100%; 261 + object-fit: cover; 262 + } 263 + 264 + .repo-meta { 265 + flex: 1; 266 + min-width: 0; 267 + } 268 + 269 + .repo-owner { 270 + color: var(--accent); 271 + text-decoration: none; 272 + font-weight: 500; 273 + font-size: 0.875rem; 274 + } 275 + 276 + .repo-owner:hover { 277 + text-decoration: underline; 278 + } 279 + 280 + .repo-time { 281 + color: var(--text-secondary); 282 + font-size: 0.75rem; 283 + } 284 + 285 + /* Repo Name & Stars */ 286 + .repo-title-row { 287 + display: flex; 288 + justify-content: space-between; 289 + align-items: baseline; 290 + gap: 0.5rem; 291 + margin-bottom: 0.5rem; 292 + } 293 + 294 + .repo-name { 295 + font-size: 1.125rem; 296 + font-weight: 600; 297 + color: var(--text-primary); 298 + text-decoration: none; 299 + min-width: 0; 300 + overflow: hidden; 301 + text-overflow: ellipsis; 302 + white-space: nowrap; 303 + } 304 + 305 + .repo-name:hover { 306 + color: var(--accent); 307 + } 308 + 309 + .repo-stars { 310 + color: var(--star-color); 311 + font-size: 0.875rem; 312 + flex-shrink: 0; 313 + display: flex; 314 + align-items: center; 315 + gap: 0.25rem; 316 + } 317 + 318 + /* Description */ 319 + .repo-description { 320 + color: var(--text-secondary); 321 + font-size: 0.875rem; 322 + margin-bottom: 0.5rem; 323 + line-height: 1.4; 324 + } 325 + 326 + /* Topics */ 327 + .repo-topics { 328 + display: flex; 329 + flex-wrap: wrap; 330 + gap: 0.375rem; 331 + margin-bottom: 0.5rem; 332 + } 333 + 334 + .topic-tag { 335 + background: var(--topic-bg); 336 + color: var(--topic-text); 337 + font-size: 0.75rem; 338 + padding: 0.125rem 0.5rem; 339 + border-radius: 1rem; 340 + } 341 + 342 + /* Footer Links */ 343 + .repo-footer { 344 + display: flex; 345 + gap: 1rem; 346 + padding-top: 0.5rem; 347 + border-top: 1px solid var(--border); 348 + margin-top: 0.5rem; 349 + } 350 + 351 + .repo-link { 352 + color: var(--text-secondary); 353 + text-decoration: none; 354 + font-size: 0.75rem; 355 + } 356 + 357 + .repo-link:hover { 358 + color: var(--accent); 359 + } 360 + 361 + /* Status Messages */ 362 + .status-msg { 363 + text-align: center; 364 + color: var(--text-secondary); 365 + padding: 2rem; 366 + } 367 + 368 + .load-more { 369 + text-align: center; 370 + padding: 1rem; 371 + } 372 + 373 + /* Buttons */ 374 + .btn { 375 + padding: 0.75rem 1.5rem; 376 + border: none; 377 + border-radius: 0.5rem; 378 + font-size: 0.875rem; 379 + font-weight: 500; 380 + cursor: pointer; 381 + transition: background-color 0.15s, opacity 0.15s; 382 + } 383 + 384 + .btn-primary { 385 + background: var(--accent); 386 + color: var(--bg-base); 387 + } 388 + 389 + .btn-primary:hover { 390 + background: var(--accent-hover); 391 + } 392 + 393 + .btn-primary:disabled { 394 + opacity: 0.5; 395 + cursor: not-allowed; 396 + } 397 + 398 + /* Error Banner */ 399 + #error-banner { 400 + position: fixed; 401 + top: 1rem; 402 + left: 50%; 403 + transform: translateX(-50%); 404 + background: var(--error-bg); 405 + border: 1px solid var(--error-border); 406 + color: var(--error-text); 407 + padding: 0.75rem 1rem; 408 + border-radius: 0.5rem; 409 + display: flex; 410 + align-items: center; 411 + gap: 0.75rem; 412 + max-width: 90%; 413 + z-index: 100; 414 + } 415 + 416 + #error-banner.hidden { 417 + display: none; 418 + } 419 + 420 + #error-banner button { 421 + background: none; 422 + border: none; 423 + color: var(--error-text); 424 + cursor: pointer; 425 + font-size: 1.25rem; 426 + line-height: 1; 427 + } 428 + 429 + .hidden { 430 + display: none !important; 431 + } 432 + 433 + /* Spinner */ 434 + .spinner { 435 + width: 32px; 436 + height: 32px; 437 + border: 3px solid var(--border); 438 + border-top-color: var(--accent); 439 + border-radius: 50%; 440 + animation: spin 0.8s linear infinite; 441 + margin: 0 auto; 442 + } 443 + 444 + @keyframes spin { 445 + to { 446 + transform: rotate(360deg); 447 + } 448 + } 449 + 450 + .loading-container { 451 + display: flex; 452 + flex-direction: column; 453 + align-items: center; 454 + gap: 0.75rem; 455 + padding: 2rem; 456 + color: var(--text-secondary); 457 + } 458 + ``` 459 + 460 + **Step 2: Test light and dark modes** 461 + 462 + Open in browser, verify Catppuccin Latte colors. Change system to dark mode, verify Mocha colors. 463 + 464 + **Step 3: Commit** 465 + 466 + ```bash 467 + git add tangled-repos.html 468 + git commit -m "feat(tangled): add Catppuccin theming with light/dark support" 469 + ``` 470 + 471 + --- 472 + 473 + ### Task 3: Add JavaScript Configuration and State 474 + 475 + **Files:** 476 + - Modify: `tangled-repos.html` (script section) 477 + 478 + **Step 1: Add configuration and state** 479 + 480 + Add inside the `<script>` tag: 481 + 482 + ```javascript 483 + // ============================================================================= 484 + // CONFIGURATION 485 + // ============================================================================= 486 + 487 + const SERVER_URL = "https://quickslice-production-ddc3.up.railway.app"; 488 + const PAGE_SIZE = 20; 489 + const DEBOUNCE_MS = 300; 490 + 491 + // ============================================================================= 492 + // STATE 493 + // ============================================================================= 494 + 495 + const state = { 496 + repos: [], 497 + cursor: null, 498 + hasMore: true, 499 + isLoading: false, 500 + searchQuery: "", 501 + totalCount: 0, 502 + }; 503 + ``` 504 + 505 + **Step 2: Commit** 506 + 507 + ```bash 508 + git add tangled-repos.html 509 + git commit -m "feat(tangled): add configuration and state management" 510 + ``` 511 + 512 + --- 513 + 514 + ### Task 4: Add GraphQL Query and Data Fetching 515 + 516 + **Files:** 517 + - Modify: `tangled-repos.html` (script section) 518 + 519 + **Step 1: Add GraphQL query and fetch function** 520 + 521 + Add after the state section: 522 + 523 + ```javascript 524 + // ============================================================================= 525 + // GRAPHQL 526 + // ============================================================================= 527 + 528 + const REPOS_QUERY = ` 529 + query GetRepos($first: Int!, $after: String, $where: ShTangledRepoWhereInput) { 530 + shTangledRepo( 531 + first: $first 532 + after: $after 533 + sortBy: [{ field: createdAt, direction: DESC }] 534 + where: $where 535 + ) { 536 + totalCount 537 + edges { 538 + node { 539 + uri 540 + name 541 + description 542 + knot 543 + topics 544 + website 545 + actorHandle 546 + createdAt 547 + appBskyActorProfileByDid { 548 + displayName 549 + avatar { url(preset: "avatar") } 550 + } 551 + shTangledFeedStarViaSubject { 552 + totalCount 553 + } 554 + } 555 + } 556 + pageInfo { 557 + hasNextPage 558 + endCursor 559 + } 560 + } 561 + } 562 + `; 563 + 564 + // ============================================================================= 565 + // DATA FETCHING 566 + // ============================================================================= 567 + 568 + function buildWhereClause(query) { 569 + if (!query || !query.trim()) return null; 570 + const q = query.trim(); 571 + return { 572 + or: [ 573 + { name: { contains: q } }, 574 + { description: { contains: q } }, 575 + { actorHandle: { contains: q } }, 576 + ], 577 + }; 578 + } 579 + 580 + async function fetchRepos(cursor = null, searchQuery = "") { 581 + const variables = { 582 + first: PAGE_SIZE, 583 + after: cursor, 584 + where: buildWhereClause(searchQuery), 585 + }; 586 + 587 + const res = await fetch(`${SERVER_URL}/graphql`, { 588 + method: "POST", 589 + headers: { "Content-Type": "application/json" }, 590 + body: JSON.stringify({ query: REPOS_QUERY, variables }), 591 + }); 592 + 593 + if (!res.ok) throw new Error(`HTTP ${res.status}`); 594 + 595 + const json = await res.json(); 596 + if (json.errors) throw new Error(json.errors[0].message); 597 + 598 + return json.data.shTangledRepo; 599 + } 600 + ``` 601 + 602 + **Step 2: Commit** 603 + 604 + ```bash 605 + git add tangled-repos.html 606 + git commit -m "feat(tangled): add GraphQL query and fetch function" 607 + ``` 608 + 609 + --- 610 + 611 + ### Task 5: Add Helper Functions 612 + 613 + **Files:** 614 + - Modify: `tangled-repos.html` (script section) 615 + 616 + **Step 1: Add helper functions** 617 + 618 + Add after the data fetching section: 619 + 620 + ```javascript 621 + // ============================================================================= 622 + // HELPERS 623 + // ============================================================================= 624 + 625 + function showError(msg) { 626 + const el = document.getElementById("error-banner"); 627 + el.innerHTML = `<span>${esc(msg)}</span><button onclick="hideError()">×</button>`; 628 + el.classList.remove("hidden"); 629 + } 630 + 631 + function hideError() { 632 + document.getElementById("error-banner").classList.add("hidden"); 633 + } 634 + 635 + function esc(str) { 636 + if (!str) return ""; 637 + const d = document.createElement("div"); 638 + d.textContent = str; 639 + return d.innerHTML; 640 + } 641 + 642 + function formatTime(iso) { 643 + const d = new Date(iso); 644 + const now = new Date(); 645 + const diff = Math.floor((now - d) / 1000); 646 + 647 + if (diff < 60) return "just now"; 648 + if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; 649 + if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; 650 + if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`; 651 + 652 + return d.toLocaleDateString("en-US", { month: "short", day: "numeric" }); 653 + } 654 + 655 + function debounce(fn, ms) { 656 + let timeout; 657 + return (...args) => { 658 + clearTimeout(timeout); 659 + timeout = setTimeout(() => fn(...args), ms); 660 + }; 661 + } 662 + ``` 663 + 664 + **Step 2: Commit** 665 + 666 + ```bash 667 + git add tangled-repos.html 668 + git commit -m "feat(tangled): add helper functions" 669 + ``` 670 + 671 + --- 672 + 673 + ### Task 6: Add Repo Card Rendering 674 + 675 + **Files:** 676 + - Modify: `tangled-repos.html` (script section) 677 + 678 + **Step 1: Add render function for repo cards** 679 + 680 + Add after helpers: 681 + 682 + ```javascript 683 + // ============================================================================= 684 + // RENDERING 685 + // ============================================================================= 686 + 687 + function renderRepoCard(repo) { 688 + const profile = repo.appBskyActorProfileByDid; 689 + const handle = repo.actorHandle || "unknown"; 690 + const avatar = profile?.avatar?.url || ""; 691 + const displayName = profile?.displayName || handle; 692 + const stars = repo.shTangledFeedStarViaSubject?.totalCount || 0; 693 + const topics = repo.topics || []; 694 + const tangledUrl = `https://tangled.org/${handle}/${repo.name}`; 695 + 696 + let topicsHtml = ""; 697 + if (topics.length > 0) { 698 + topicsHtml = ` 699 + <div class="repo-topics"> 700 + ${topics.map((t) => `<span class="topic-tag">${esc(t)}</span>`).join("")} 701 + </div> 702 + `; 703 + } 704 + 705 + let footerLinks = `<a href="${esc(tangledUrl)}" target="_blank" class="repo-link">View on Tangled →</a>`; 706 + if (repo.website) { 707 + const websiteDisplay = repo.website.replace(/^https?:\/\//, "").replace(/\/$/, ""); 708 + footerLinks = `<a href="${esc(repo.website)}" target="_blank" class="repo-link">${esc(websiteDisplay)}</a>` + footerLinks; 709 + } 710 + 711 + return ` 712 + <div class="card" data-uri="${esc(repo.uri)}"> 713 + <div class="repo-header"> 714 + <div class="repo-avatar"> 715 + ${avatar ? `<img src="${esc(avatar)}" alt="">` : ""} 716 + </div> 717 + <div class="repo-meta"> 718 + <a href="https://bsky.app/profile/${esc(handle)}" target="_blank" class="repo-owner">@${esc(handle)}</a> 719 + <div class="repo-time">${formatTime(repo.createdAt)}</div> 720 + </div> 721 + </div> 722 + <div class="repo-title-row"> 723 + <a href="${esc(tangledUrl)}" target="_blank" class="repo-name">${esc(repo.name)}</a> 724 + ${stars > 0 ? `<span class="repo-stars">★ ${stars}</span>` : ""} 725 + </div> 726 + ${repo.description ? `<div class="repo-description">${esc(repo.description)}</div>` : ""} 727 + ${topicsHtml} 728 + <div class="repo-footer"> 729 + ${footerLinks} 730 + </div> 731 + </div> 732 + `; 733 + } 734 + ``` 735 + 736 + **Step 2: Commit** 737 + 738 + ```bash 739 + git add tangled-repos.html 740 + git commit -m "feat(tangled): add repo card rendering" 741 + ``` 742 + 743 + --- 744 + 745 + ### Task 7: Add Feed and Load More Rendering 746 + 747 + **Files:** 748 + - Modify: `tangled-repos.html` (script section) 749 + 750 + **Step 1: Add feed rendering functions** 751 + 752 + Add after renderRepoCard: 753 + 754 + ```javascript 755 + function renderFeed() { 756 + const el = document.getElementById("repo-feed"); 757 + 758 + if (state.isLoading && state.repos.length === 0) { 759 + el.innerHTML = `<div class="loading-container"><div class="spinner"></div><span>Loading repos...</span></div>`; 760 + return; 761 + } 762 + 763 + if (state.repos.length === 0) { 764 + const msg = state.searchQuery 765 + ? `No repos found for "${esc(state.searchQuery)}"` 766 + : "No repos yet."; 767 + el.innerHTML = `<div class="status-msg">${msg}</div>`; 768 + return; 769 + } 770 + 771 + el.innerHTML = state.repos.map((r) => renderRepoCard(r)).join(""); 772 + } 773 + 774 + function renderResultCount() { 775 + const el = document.getElementById("result-count"); 776 + if (state.searchQuery && state.repos.length > 0) { 777 + el.textContent = `${state.totalCount} results for "${state.searchQuery}"`; 778 + } else if (!state.searchQuery && state.totalCount > 0) { 779 + el.textContent = `${state.totalCount} repos`; 780 + } else { 781 + el.textContent = ""; 782 + } 783 + } 784 + 785 + function renderLoadMore() { 786 + const el = document.getElementById("load-more"); 787 + 788 + if (state.repos.length === 0) { 789 + el.innerHTML = ""; 790 + return; 791 + } 792 + 793 + if (!state.hasMore) { 794 + el.innerHTML = `<div class="status-msg">No more repos</div>`; 795 + return; 796 + } 797 + 798 + el.innerHTML = ` 799 + <div class="load-more"> 800 + <button class="btn btn-primary" onclick="handleLoadMore()" ${state.isLoading ? "disabled" : ""}> 801 + ${state.isLoading ? "Loading..." : "Load More"} 802 + </button> 803 + </div> 804 + `; 805 + } 806 + ``` 807 + 808 + **Step 2: Commit** 809 + 810 + ```bash 811 + git add tangled-repos.html 812 + git commit -m "feat(tangled): add feed and load more rendering" 813 + ``` 814 + 815 + --- 816 + 817 + ### Task 8: Add Load and Search Logic 818 + 819 + **Files:** 820 + - Modify: `tangled-repos.html` (script section) 821 + 822 + **Step 1: Add load and search functions** 823 + 824 + Add after rendering functions: 825 + 826 + ```javascript 827 + // ============================================================================= 828 + // ACTIONS 829 + // ============================================================================= 830 + 831 + async function loadRepos(append = false) { 832 + if (state.isLoading) return; 833 + state.isLoading = true; 834 + renderFeed(); 835 + renderLoadMore(); 836 + 837 + try { 838 + const data = await fetchRepos( 839 + append ? state.cursor : null, 840 + state.searchQuery 841 + ); 842 + const newRepos = data.edges.map((e) => e.node); 843 + 844 + state.repos = append ? [...state.repos, ...newRepos] : newRepos; 845 + state.cursor = data.pageInfo.endCursor; 846 + state.hasMore = data.pageInfo.hasNextPage; 847 + state.totalCount = data.totalCount; 848 + 849 + renderFeed(); 850 + renderResultCount(); 851 + } catch (err) { 852 + console.error("Load failed:", err); 853 + showError(`Failed to load: ${err.message}`); 854 + } finally { 855 + state.isLoading = false; 856 + renderLoadMore(); 857 + } 858 + } 859 + 860 + function handleLoadMore() { 861 + loadRepos(true); 862 + } 863 + 864 + function handleSearch(query) { 865 + state.searchQuery = query; 866 + state.cursor = null; 867 + state.repos = []; 868 + state.hasMore = true; 869 + loadRepos(); 870 + } 871 + 872 + const debouncedSearch = debounce(handleSearch, DEBOUNCE_MS); 873 + 874 + function clearSearch() { 875 + const input = document.getElementById("search-input"); 876 + input.value = ""; 877 + document.getElementById("clear-search").classList.add("hidden"); 878 + handleSearch(""); 879 + } 880 + ``` 881 + 882 + **Step 2: Commit** 883 + 884 + ```bash 885 + git add tangled-repos.html 886 + git commit -m "feat(tangled): add load and search logic" 887 + ``` 888 + 889 + --- 890 + 891 + ### Task 9: Add Main Function and Event Listeners 892 + 893 + **Files:** 894 + - Modify: `tangled-repos.html` (script section) 895 + 896 + **Step 1: Add main function and event setup** 897 + 898 + Add at the end of the script: 899 + 900 + ```javascript 901 + // ============================================================================= 902 + // MAIN 903 + // ============================================================================= 904 + 905 + function main() { 906 + // Set up search input 907 + const searchInput = document.getElementById("search-input"); 908 + const clearBtn = document.getElementById("clear-search"); 909 + 910 + searchInput.addEventListener("input", (e) => { 911 + const value = e.target.value; 912 + clearBtn.classList.toggle("hidden", !value); 913 + debouncedSearch(value); 914 + }); 915 + 916 + clearBtn.addEventListener("click", clearSearch); 917 + 918 + // Initial load 919 + loadRepos(); 920 + } 921 + 922 + main(); 923 + ``` 924 + 925 + **Step 2: Test the complete application** 926 + 927 + 1. Open `tangled-repos.html` in browser 928 + 2. Verify repos load on page load 929 + 3. Test search functionality (try "bsky", "tool", etc.) 930 + 4. Test clear search button 931 + 5. Test Load More button 932 + 6. Test light/dark mode switching 933 + 7. Verify links work (Bsky profile, Tangled repo, website) 934 + 935 + **Step 3: Commit** 936 + 937 + ```bash 938 + git add tangled-repos.html 939 + git commit -m "feat(tangled): add main function and event listeners" 940 + ``` 941 + 942 + --- 943 + 944 + ### Task 10: Add to Index Page 945 + 946 + **Files:** 947 + - Modify: `index.html` 948 + 949 + **Step 1: Check current index.html structure** 950 + 951 + Read `index.html` to understand the current link structure. 952 + 953 + **Step 2: Add link to Tangled Repos tool** 954 + 955 + Add a link to `tangled-repos.html` following the existing pattern. 956 + 957 + **Step 3: Commit** 958 + 959 + ```bash 960 + git add index.html 961 + git commit -m "feat(tangled): add link to index page" 962 + ``` 963 + 964 + --- 965 + 966 + ## Final Verification 967 + 968 + After all tasks complete: 969 + 970 + 1. Run through full user flow: 971 + - Page loads with recent repos 972 + - Search filters results 973 + - Clear search returns to default view 974 + - Load More works 975 + - All links open correctly 976 + - Light/dark themes work 977 + 978 + 2. Test edge cases: 979 + - Empty search results 980 + - Network error handling 981 + - Repos with no description/topics/website