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.

Add turn indicators and filtering on homepage

- Show "Your turn" / "Waiting" badges on current games
- Add "Your turn" filter checkbox next to "My games"
- Highlight games where it's your turn with accent border
- Show which player's turn it is (black/white indicator)
- Bold the current player's name in game list
- Include action_count in game data for turn calculation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+118 -12
+1 -1
src/routes/+page.server.ts
··· 10 10 // Fetch all games from index (filtering/sorting done client-side) 11 11 const games = await db 12 12 .selectFrom('games') 13 - .select(['rkey', 'id', 'player_one', 'player_two', 'board_size', 'status', 'created_at', 'updated_at', 'last_action_type']) 13 + .select(['rkey', 'id', 'player_one', 'player_two', 'board_size', 'status', 'created_at', 'updated_at', 'last_action_type', 'action_count']) 14 14 .orderBy('updated_at', 'desc') 15 15 .limit(100) 16 16 .execute();
+117 -11
src/routes/+page.svelte
··· 14 14 let handles = $state<Record<string, string>>({}); 15 15 let sessionHandle = $state<string | null>(null); 16 16 let showMyGamesOnly = $state(false); 17 + let showMyTurnOnly = $state(false); 17 18 let archivePage = $state(1); 18 19 const ARCHIVE_PAGE_SIZE = 6; 19 20 21 + // Helper to determine whose turn it is in a game 22 + function getWhoseTurn(game: typeof data.games[0]): 'black' | 'white' { 23 + const actionCount = game.action_count || 0; 24 + return actionCount % 2 === 0 ? 'black' : 'white'; 25 + } 26 + 27 + // Helper to check if it's the current user's turn 28 + function isMyTurn(game: typeof data.games[0]): boolean { 29 + if (!data.session) return false; 30 + const turn = getWhoseTurn(game); 31 + if (turn === 'black') { 32 + return game.player_one === data.session.did; 33 + } else { 34 + return game.player_two === data.session.did; 35 + } 36 + } 37 + 38 + // Helper to check if user is a player in the game 39 + function isMyGame(game: typeof data.games[0]): boolean { 40 + if (!data.session) return false; 41 + return game.player_one === data.session.did || game.player_two === data.session.did; 42 + } 43 + 20 44 // Split games by status 21 45 const currentGames = $derived( 22 46 (data.games || []) 23 47 .filter((g) => g.status === 'active') 24 - .filter((g) => !showMyGamesOnly || !data.session || g.player_one === data.session.did || g.player_two === data.session.did) 48 + .filter((g) => !showMyGamesOnly || isMyGame(g)) 49 + .filter((g) => !showMyTurnOnly || isMyTurn(g)) 25 50 .sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()) 26 51 ); 27 52 ··· 339 364 <div class="card current-games"> 340 365 <div class="section-header"> 341 366 <h2>Current Games</h2> 342 - <label class="toggle-label"> 343 - <input type="checkbox" bind:checked={showMyGamesOnly} /> 344 - My games only 345 - </label> 367 + <div class="filter-toggles"> 368 + <label class="toggle-label"> 369 + <input type="checkbox" bind:checked={showMyGamesOnly} /> 370 + My games 371 + </label> 372 + <label class="toggle-label"> 373 + <input type="checkbox" bind:checked={showMyTurnOnly} /> 374 + Your turn 375 + </label> 376 + </div> 346 377 </div> 347 378 {#if currentGames.length > 0} 348 379 <div class="games-list"> 349 380 {#each currentGames as game} 350 - <div class="game-item"> 381 + {@const whoseTurn = getWhoseTurn(game)} 382 + {@const myTurn = isMyTurn(game)} 383 + {@const playing = isMyGame(game)} 384 + <div class="game-item" class:my-turn={myTurn}> 351 385 <div class="game-info"> 352 - <div class="game-title">{game.title}</div> 386 + <div class="game-title-row"> 387 + <span class="game-title">{game.title}</span> 388 + {#if playing} 389 + {#if myTurn} 390 + <span class="turn-badge your-turn">Your turn</span> 391 + {:else} 392 + <span class="turn-badge their-turn">Waiting</span> 393 + {/if} 394 + {:else} 395 + <span class="turn-badge spectating">{whoseTurn === 'black' ? '⚫' : '⚪'} to play</span> 396 + {/if} 397 + </div> 353 398 <div> 354 399 <strong>{game.board_size}x{game.board_size}</strong> board 355 400 <span class="move-count">{moveCounts[game.id] != null ? `${moveCounts[game.id]} moves` : '...'}</span> 356 401 </div> 357 402 <div class="game-players"> 358 - Player 1: <a href="https://bsky.app/profile/{game.player_one}" target="_blank" rel="noopener noreferrer" class="player-link">{handles[game.player_one] || game.player_one}</a> 403 + <span class:current-turn={whoseTurn === 'black'}>⚫ <a href="https://bsky.app/profile/{game.player_one}" target="_blank" rel="noopener noreferrer" class="player-link">{handles[game.player_one] || game.player_one}</a></span> 359 404 {#if game.player_two} 360 - <br />Player 2: <a href="https://bsky.app/profile/{game.player_two}" target="_blank" rel="noopener noreferrer" class="player-link">{handles[game.player_two] || game.player_two}</a> 405 + <span class="vs">vs</span> 406 + <span class:current-turn={whoseTurn === 'white'}>⚪ <a href="https://bsky.app/profile/{game.player_two}" target="_blank" rel="noopener noreferrer" class="player-link">{handles[game.player_two] || game.player_two}</a></span> 361 407 {/if} 362 408 </div> 363 409 </div> 364 - {#if game.player_one === data.session.did || game.player_two === data.session.did} 410 + {#if playing} 365 411 <a href="/game/{game.rkey}" class="button button-primary button-sm"> 366 - Play 412 + {myTurn ? 'Play' : 'View'} 367 413 </a> 368 414 {:else} 369 415 <a href="/game/{game.rkey}" class="button button-secondary button-sm"> ··· 723 769 box-shadow: 0 4px 12px rgba(90, 122, 144, 0.1); 724 770 } 725 771 772 + .game-item.my-turn { 773 + border-color: var(--sky-apricot); 774 + background: linear-gradient(135deg, var(--sky-apricot-light) 0%, var(--sky-white) 100%); 775 + } 776 + 726 777 .game-info { 727 778 display: flex; 728 779 flex-direction: column; 729 780 gap: 0.375rem; 781 + } 782 + 783 + .game-title-row { 784 + display: flex; 785 + align-items: center; 786 + gap: 0.5rem; 787 + flex-wrap: wrap; 788 + } 789 + 790 + .turn-badge { 791 + display: inline-block; 792 + padding: 0.2rem 0.5rem; 793 + border-radius: 0.25rem; 794 + font-size: 0.7rem; 795 + font-weight: 600; 796 + text-transform: uppercase; 797 + } 798 + 799 + .turn-badge.your-turn { 800 + background: var(--sky-apricot); 801 + color: white; 802 + } 803 + 804 + .turn-badge.their-turn { 805 + background: var(--sky-gray-light); 806 + color: var(--sky-slate); 807 + } 808 + 809 + .turn-badge.spectating { 810 + background: var(--sky-cloud); 811 + color: var(--sky-gray); 812 + font-size: 0.75rem; 813 + text-transform: none; 814 + } 815 + 816 + .filter-toggles { 817 + display: flex; 818 + gap: 1rem; 819 + } 820 + 821 + .game-players { 822 + display: flex; 823 + flex-wrap: wrap; 824 + align-items: center; 825 + gap: 0.25rem; 826 + } 827 + 828 + .game-players .vs { 829 + color: var(--sky-gray-light); 830 + font-size: 0.8rem; 831 + margin: 0 0.25rem; 832 + } 833 + 834 + .game-players .current-turn { 835 + font-weight: 600; 730 836 } 731 837 732 838 .game-status {