extremely claude-assisted go game based on atproto! working on cleaning up and giving a more unique design, still has a bit of a slop vibe to it.

got reactions working a bit better

+81 -29
+5 -5
TODOS.md
··· 1 1 Features not yet implemented 2 - - [x] new reaction lexicon that contains an optional emoji field, a required text field, an an optional stars field, and a mandatory "Move ID" field, referencing a move in a game that can be commented on by any logged in user who is spectating. These could be shown on the right side of the board, and moves can show a preview of the reaction emojis as badges 3 - - [x] sharing a game using bsky post intents : The web URL endpoint is https://bsky.app/intent/compose, with the HTTP query parameter text. Remember to use URL-escaping on the query parameter value, and that the post length limit on Bluesky is 300 characters (more precisely, 300 Unicode Grapheme Clusters). 4 - - [x] tag the players involved in the game and use a hashtag composed of the game ID 5 - - [x] Opengraph image preview of the game 6 - - [x] reorganize the completed game section into a grid to save space, and add pagination 2 + - [ ] currently reactions are being saved to PDS using the move number rather than the rKey, this causes a mismatch and prevents them from loading properly in the app since they dont match with the constellation RKEY for moves. (might be fixed now) 3 + - [ ] allow viewing all reactions at once rather than just for a given move 4 + - [ ] add url parameters to navigate to a specific move URI in a game rather than the current move. 5 + - [ ] swap board view from to more scalable and robust tenuki library https://github.com/aprescott/tenuki?tab=readme-ov-file, which also includes a scoring engine 6 + - [ ] fix the scoring logic upon completion of a game to use tenuki's scoring engine and pre-populate scores based on these, still allow changes if the owner disagrees but suggesting a score makes it much easier than counting manually.
+23 -12
src/lib/components/Board.svelte
··· 128 128 // largeWalnut board configuration, scaled responsively 129 129 const boardOptions = { 130 130 padding: { normal: gridSize * 0.8, clipped: gridSize * 0.4 }, 131 - margin: { normal: gridSize * 0.8, clipped: gridSize * 0.2, color: '#e2baa0' }, 131 + margin: { normal: gridSize * 0.8, clipped: gridSize * 0.2, color: '#f8dcc5' }, 132 132 grid: { 133 133 x: gridSize, 134 134 y: gridSize, ··· 147 147 blur: gridSize * 0.06 148 148 }, 149 149 boardShadow: { 150 - color: '#e2baa0', 150 + color: '#f2c5a0', 151 151 blur: gridSize * 0.6, 152 152 offX: Math.max(3, gridSize * 0.1), 153 153 offY: Math.max(3, gridSize * 0.1) ··· 440 440 441 441 <div class="board-container"> 442 442 {#if !isReady} 443 - <div class="loading">Loading board...</div> 443 + <div class="loading">...</div> 444 444 {/if} 445 445 446 446 <div class="board-wrapper" class:hidden={!isReady}> ··· 479 479 } 480 480 481 481 .jgoboard { 482 - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 482 + box-shadow: 0 8px 24px rgba(90, 122, 144, 0.15); 483 483 display: block; 484 + border-radius: 4px; 484 485 } 485 486 486 487 @media (max-width: 768px) { ··· 493 494 display: flex; 494 495 gap: 2rem; 495 496 align-items: center; 497 + padding: 0.75rem 1.5rem; 498 + background: var(--sky-white, #f5f8fa); 499 + border-radius: 0.75rem; 500 + border: 1px solid var(--sky-blue-pale, #d4e5ef); 501 + box-shadow: 0 2px 8px rgba(90, 122, 144, 0.08); 496 502 } 497 503 498 504 .pass-button { 499 - padding: 0.5rem 2rem; 505 + padding: 0.625rem 2rem; 500 506 font-size: 1rem; 501 - background: #4a5568; 507 + background: var(--sky-slate, #5a7a90); 502 508 color: white; 503 509 border: none; 504 - border-radius: 0.375rem; 510 + border-radius: 0.5rem; 505 511 cursor: pointer; 506 - transition: background 0.2s; 512 + transition: all 0.2s; 513 + font-weight: 600; 507 514 } 508 515 509 516 .pass-button:hover { 510 - background: #2d3748; 517 + background: var(--sky-slate-dark, #455d6e); 518 + transform: translateY(-1px); 511 519 } 512 520 513 521 .turn-indicator { 514 522 font-size: 1.125rem; 515 523 font-weight: 600; 524 + color: var(--sky-slate, #5a7a90); 516 525 } 517 526 518 527 .turn-black { 519 - color: #1a202c; 528 + color: #1a1a1a; 529 + font-weight: 700; 520 530 } 521 531 522 532 .turn-white { 523 - color: #718096; 533 + color: var(--sky-gray, #94a8b8); 534 + font-weight: 700; 524 535 } 525 536 526 537 .loading { 527 538 padding: 2rem; 528 539 text-align: center; 529 - color: #718096; 540 + color: var(--sky-gray, #94a8b8); 530 541 } 531 542 532 543 .hidden {
+11 -1
src/routes/+layout.svelte
··· 4 4 let { children } = $props(); 5 5 </script> 6 6 7 - {@render children()} 7 + <div class="clouds-container"> 8 + <div class="cloud cloud-1"></div> 9 + <div class="cloud cloud-2"></div> 10 + <div class="cloud cloud-3"></div> 11 + <div class="cloud cloud-4"></div> 12 + <div class="cloud cloud-5"></div> 13 + </div> 14 + 15 + <div class="page-content"> 16 + {@render children()} 17 + </div>
+42 -11
src/routes/game/[id]/+page.svelte
··· 105 105 function getMoveUri(move: MoveRecord): string { 106 106 if (move.uri) { 107 107 return move.uri; 108 + } else { 109 + console.error(`Missing URI for move ${move.id}`); 110 + return; 108 111 } 109 112 // Fallback: construct synthetic URI (shouldn't be needed normally) 110 - return `at://${move.player}/boo.sky.go.move/${move.moveNumber}`; 113 + // return `at://${move.player}/boo.sky.go.move/${move.moveNumber}`; 111 114 } 112 115 113 116 // Get reactions for the selected move 114 117 const selectedMoveReactions = $derived(() => { 115 118 if (!selectedMove) return []; 116 119 const moveUri = getMoveUri(selectedMove); 120 + console.log(`Selected move ${moveUri}`); 117 121 return reactions.get(moveUri) || []; 118 122 }); 119 123 ··· 175 179 fetchGameReactions(data.gameAtUri).then((reactionsMap) => { 176 180 reactions = reactionsMap; 177 181 loadingReactions = false; 182 + console.log('Reactions fetched:', reactions); 178 183 179 184 // Resolve handles for reaction authors 180 185 const authorDids = new Set<string>(); ··· 657 662 </div> 658 663 {/if} 659 664 660 - <Board 661 - bind:this={boardRef} 662 - boardSize={gameBoardSize} 663 - gameState={{ moves: boardMoves }} 664 - onMove={handleMove} 665 - onPass={handlePass} 666 - interactive={gameStatus === 'active' && isMyTurn() && reviewMoveIndex === null} 667 - currentTurn={currentTurn()} 668 - /> 665 + {#if loadingMoves} 666 + <div class="board-loading-placeholder" style="--board-size: {gameBoardSize}"> 667 + <div class="board-loading-text">...</div> 668 + </div> 669 + {:else} 670 + <Board 671 + bind:this={boardRef} 672 + boardSize={gameBoardSize} 673 + gameState={{ moves: boardMoves }} 674 + onMove={handleMove} 675 + onPass={handlePass} 676 + interactive={gameStatus === 'active' && isMyTurn() && reviewMoveIndex === null} 677 + currentTurn={currentTurn()} 678 + /> 679 + {/if} 669 680 670 - {#if reviewMoveIndex === null && !isMyTurn() && gameStatus === 'active'} 681 + {#if !loadingMoves && reviewMoveIndex === null && !isMyTurn() && gameStatus === 'active'} 671 682 <p class="turn-message">Waiting for opponent's move...</p> 672 683 {/if} 673 684 ··· 934 945 @keyframes shimmer { 935 946 0% { background-position: 200% 0; } 936 947 100% { background-position: -200% 0; } 948 + } 949 + 950 + .board-loading-placeholder { 951 + aspect-ratio: 1; 952 + width: 100%; 953 + max-width: min(calc(var(--board-size, 19) * 30px + 60px), 90vw); 954 + background: linear-gradient(135deg, #DEB887 0%, #C4A067 100%); 955 + border-radius: 0.5rem; 956 + display: flex; 957 + align-items: center; 958 + justify-content: center; 959 + margin: 0 auto; 960 + box-shadow: 0 4px 16px rgba(90, 122, 144, 0.15); 961 + } 962 + 963 + .board-loading-text { 964 + color: rgba(92, 72, 48, 0.7); 965 + font-size: 1.25rem; 966 + font-weight: 600; 967 + animation: pulse 1.5s ease-in-out infinite; 937 968 } 938 969 939 970 .loading-state {