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.

updated rules page, added profile page (broken), fixed profile image to be bigger

+49 -95
+35
src/lib/atproto-client.ts
··· 399 399 400 400 return reactionsByMove; 401 401 } 402 + 403 + export interface UserProfile { 404 + did: string; 405 + handle: string; 406 + displayName?: string; 407 + avatar?: string; 408 + description?: string; 409 + } 410 + 411 + /** 412 + * Fetch user profile including avatar from Bluesky public API 413 + */ 414 + export async function fetchUserProfile(did: string): Promise<UserProfile | null> { 415 + try { 416 + const params = new URLSearchParams({ actor: did }); 417 + const res = await fetch( 418 + `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?${params}`, 419 + { headers: { Accept: 'application/json' } } 420 + ); 421 + 422 + if (!res.ok) return null; 423 + 424 + const data = await res.json(); 425 + return { 426 + did: data.did, 427 + handle: data.handle, 428 + displayName: data.displayName, 429 + avatar: data.avatar, 430 + description: data.description, 431 + }; 432 + } catch (err) { 433 + console.error('Failed to fetch user profile:', err); 434 + return null; 435 + } 436 + }
+4 -1
src/routes/+layout.svelte
··· 2 2 import '../app.css'; 3 3 import TutorialPopup from '$lib/components/TutorialPopup.svelte'; 4 4 import Footer from '$lib/components/Footer.svelte'; 5 + import Header from '$lib/components/Header.svelte'; 6 + import type { LayoutData } from './$types'; 5 7 6 - let { children } = $props(); 8 + let { children, data }: { children: any; data: LayoutData } = $props(); 7 9 8 10 let tutorialPopup: TutorialPopup | null = $state(null); 9 11 ··· 21 23 </div> 22 24 23 25 <div class="page-content"> 26 + <Header session={data.session} /> 24 27 {@render children()} 25 28 </div> 26 29
+10 -92
src/routes/+page.svelte
··· 184 184 </svelte:head> 185 185 186 186 <div class="container"> 187 - <header class="header-card"> 188 - <h1>☁️ Cloud Go ☁️</h1> 189 - <p class="subtitle">An atmospheric go-playing experience</p> 190 - <p class="tagline">All your games are contained safely in your personal repository, yours forever.</p> 191 - </header> 192 - 193 187 {#if !data.session} 194 188 {#if !spectating} 195 189 <!-- Login Form --> ··· 238 232 <span class="move-count">{moveCounts[game.id] != null ? `${moveCounts[game.id]} moves` : '...'}</span> 239 233 </div> 240 234 <div class="game-players"> 241 - 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> 235 + Player 1: <a href="/profile/{game.player_one}" class="player-link">{handles[game.player_one] || game.player_one}</a> 242 236 {#if game.player_two} 243 - <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> 237 + <br />Player 2: <a href="/profile/{game.player_two}" class="player-link">{handles[game.player_two] || game.player_two}</a> 244 238 {/if} 245 239 </div> 246 240 </div> ··· 257 251 258 252 <!-- Waiting Games --> 259 253 <div class="card waiting-games"> 260 - <h2>Waiting for Players</h2> 254 + <div class="section-header"> 255 + <h2>Waiting for Players</h2> 256 + </div> 261 257 {#if waitingGames.length > 0} 262 258 <div class="waiting-games-grid"> 263 259 {#each waitingGames as game} ··· 338 334 {/if} 339 335 {:else} 340 336 <!-- Logged In View --> 341 - <div class="user-section"> 342 - <p>Welcome, <a href="https://bsky.app/profile/{data.session.did}" target="_blank" rel="noopener noreferrer" class="profile-link"><strong>{sessionHandle || data.session.did}</strong></a>!</p> 343 - <button onclick={logout} class="button button-secondary">Logout</button> 344 - </div> 345 - 346 337 <!-- Create Game Section --> 347 338 <div class="card"> 348 339 <h2>Create New Game</h2> ··· 412 403 <span class="move-count">{moveCounts[game.id] != null ? `${moveCounts[game.id]} moves` : '...'}</span> 413 404 </div> 414 405 <div class="game-players"> 415 - <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> 406 + <span class:current-turn={whoseTurn === 'black'}>⚫ <a href="/profile/{game.player_one}" class="player-link">{handles[game.player_one] || game.player_one}</a></span> 416 407 {#if game.player_two} 417 408 <span class="vs">vs</span> 418 - <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> 409 + <span class:current-turn={whoseTurn === 'white'}>⚪ <a href="/profile/{game.player_two}" class="player-link">{handles[game.player_two] || game.player_two}</a></span> 419 410 {/if} 420 411 </div> 421 412 </div> ··· 438 429 439 430 <!-- Waiting Games --> 440 431 <div class="card waiting-games"> 441 - <h2>Waiting for Players</h2> 432 + <div class="section-header"> 433 + <h2>Waiting for Players</h2> 434 + </div> 442 435 {#if waitingGames.length > 0} 443 436 <div class="waiting-games-grid"> 444 437 {#each waitingGames as game} ··· 530 523 padding: 2rem; 531 524 } 532 525 533 - header { 534 - text-align: center; 535 - margin-bottom: 2rem; 536 - } 537 - 538 - .header-card { 539 - background: linear-gradient( 540 - 135deg, 541 - rgba(255, 255, 255, 0.95) 0%, 542 - rgba(245, 248, 250, 0.9) 50%, 543 - rgba(232, 239, 244, 0.85) 100% 544 - ); 545 - border: none; 546 - border-radius: 2rem 2.5rem 2rem 2.2rem; 547 - padding: 2rem 2.5rem; 548 - box-shadow: 549 - 0 0 20px rgba(255, 255, 255, 0.8), 550 - 0 0 40px rgba(255, 255, 255, 0.4), 551 - 0 8px 32px rgba(90, 122, 144, 0.12), 552 - inset 0 1px 1px rgba(255, 255, 255, 0.9); 553 - backdrop-filter: blur(8px); 554 - position: relative; 555 - } 556 - 557 - .header-card::before { 558 - content: ''; 559 - position: absolute; 560 - inset: -2px; 561 - border-radius: inherit; 562 - background: linear-gradient( 563 - 135deg, 564 - rgba(255, 255, 255, 0.6) 0%, 565 - rgba(212, 229, 239, 0.3) 50%, 566 - rgba(255, 255, 255, 0.4) 100% 567 - ); 568 - filter: blur(4px); 569 - z-index: -1; 570 - } 571 - 572 - h1 { 573 - font-size: 2.75rem; 574 - margin: 0; 575 - color: var(--sky-slate-dark); 576 - font-weight: 700; 577 - letter-spacing: -0.02em; 578 - } 579 - 580 - .subtitle { 581 - color: var(--sky-slate); 582 - font-size: 1.125rem; 583 - margin-top: 0.5rem; 584 - font-weight: 500; 585 - } 586 - 587 - .tagline { 588 - color: var(--sky-gray); 589 - font-size: 0.95rem; 590 - margin-top: 0.5rem; 591 - } 592 - 593 526 .login-card { 594 527 background: linear-gradient( 595 528 135deg, ··· 672 605 margin-top: 0; 673 606 color: var(--sky-slate-dark); 674 607 font-weight: 600; 675 - } 676 - 677 - .user-section { 678 - display: flex; 679 - justify-content: space-between; 680 - align-items: center; 681 - padding: 1rem 1.5rem; 682 - background: linear-gradient(135deg, var(--sky-apricot-light) 0%, var(--sky-rose-light) 100%); 683 - border-radius: 1rem; 684 - margin-bottom: 2rem; 685 - border: 1px solid var(--sky-apricot); 686 - } 687 - 688 - .user-section p { 689 - color: var(--sky-slate-dark); 690 608 } 691 609 692 610 .create-game-form {
-2
src/routes/rules/+page.svelte
··· 76 76 77 77 <div class="rules-container"> 78 78 <div class="cloud-card"> 79 - <h1>Go Rules Reference</h1> 80 - 81 79 <section> 82 80 <h2>What is Go?</h2> 83 81 <p>