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.
at master 209 lines 5.2 kB view raw
1<script lang="ts"> 2 import { onMount } from 'svelte'; 3 import { page } from '$app/stores'; 4 import { fetchUserProfile, type UserProfile } from '$lib/atproto-client'; 5 import ProfileDropdown from './ProfileDropdown.svelte'; 6 import { Card, Modal, Input, Button } from '$lib/components/ui'; 7 import { spectating } from '$lib/stores/spectating'; 8 9 type Session = { did: string } | null; 10 11 let { session }: { session: Session } = $props(); 12 13 let userProfile: UserProfile | null = $state(null); 14 let showLoginModal = $state(false); 15 let handle = $state(''); 16 let isLoggingIn = $state(false); 17 18 // Show login button when: not on homepage, OR on homepage but spectating 19 let isHomepage = $derived($page.url.pathname === '/'); 20 let showLoginButton = $derived(!isHomepage || $spectating); 21 22 onMount(async () => { 23 if (session) { 24 userProfile = await fetchUserProfile(session.did); 25 } 26 }); 27 28 async function login() { 29 if (!handle.trim()) return; 30 31 isLoggingIn = true; 32 try { 33 const response = await fetch('/auth/login', { 34 method: 'POST', 35 headers: { 'Content-Type': 'application/json' }, 36 body: JSON.stringify({ handle: handle.trim() }), 37 }); 38 39 if (!response.ok) { 40 throw new Error(`Login failed: ${response.statusText}`); 41 } 42 43 const result = await response.json(); 44 if (result.authorizationUrl) { 45 window.location.href = result.authorizationUrl; 46 } 47 } catch (err) { 48 console.error('Login failed:', err); 49 alert('Login failed. Please try again.'); 50 isLoggingIn = false; 51 } 52 } 53</script> 54 55<div class="header-wrapper"> 56 <Card variant="large" class="header"> 57 <div class="header-content"> 58 <a href="/" class="logo">☁️ Cloud Go ☁️</a> 59 60 <div class="header-right"> 61 {#if session && userProfile} 62 <ProfileDropdown 63 avatar={userProfile.avatar || null} 64 handle={userProfile.handle} 65 did={session.did} 66 /> 67 {:else if session} 68 <!-- Loading profile --> 69 <div class="avatar-placeholder"></div> 70 {:else if showLoginButton} 71 <button class="login-link" onclick={() => showLoginModal = true}>Login</button> 72 {/if} 73 </div> 74 </div> 75 </Card> 76 77 <Modal isOpen={showLoginModal} onClose={() => showLoginModal = false}> 78 <div class="login-modal-content"> 79 <h2>Login with @proto</h2> 80 <form onsubmit={(e) => { e.preventDefault(); login(); }}> 81 <actor-typeahead> 82 <Input 83 value={handle} 84 oninput={(e) => handle = e.currentTarget.value} 85 onchange={(e) => handle = e.currentTarget.value} 86 placeholder="your-handle.bsky.social" 87 disabled={isLoggingIn} 88 class="login-input" 89 /> 90 </actor-typeahead> 91 <Button type="submit" disabled={isLoggingIn} variant="primary" class="login-button"> 92 {isLoggingIn ? 'Logging in...' : 'Login'} 93 </Button> 94 </form> 95 </div> 96 </Modal> 97</div> 98 99<style> 100 .header-wrapper { 101 max-width: 1200px; 102 margin: clamp(1rem, 3vw, 2rem) auto clamp(1.5rem, 4vw, 3rem); 103 padding: 0 clamp(1rem, 3vw, 2rem); 104 position: relative; 105 z-index: 100; 106 } 107 108 .header { 109 position: relative; 110 } 111 112 .header-content { 113 display: flex; 114 align-items: center; 115 justify-content: space-between; 116 padding: clamp(1rem, 2vw, 1.5rem) clamp(1.5rem, 3vw, 2.5rem); 117 max-width: 1200px; 118 margin: 0 auto; 119 } 120 121 .logo { 122 font-size: clamp(1.5rem, 4vw, 2.75rem); 123 font-weight: 700; 124 color: var(--sky-slate-dark); 125 text-decoration: none; 126 letter-spacing: -0.02em; 127 transition: color 0.6s ease, transform 0.6s ease; 128 } 129 130 .logo:hover { 131 color: var(--sky-apricot-dark); 132 filter: drop-shadow(0 0 8px rgba(229, 168, 120, 0.6)); 133 } 134 135 136 137 .header-right { 138 display: flex; 139 align-items: center; 140 } 141 142 .login-link { 143 color: var(--sky-slate); 144 background: transparent; 145 border: none; 146 cursor: pointer; 147 text-decoration: none; 148 font-weight: 500; 149 font-size: clamp(1rem, 2vw, 1.125rem); 150 padding: 0.75rem 1.25rem; 151 border-radius: 0.5rem; 152 transition: all 0.6s ease; 153 } 154 155 .login-link:hover { 156 background: var(--sky-apricot-light); 157 color: var(--sky-apricot-dark); 158 box-shadow: 0 0 12px rgba(229, 168, 120, 0.5); 159 } 160 161 .login-modal-content { 162 padding: 2rem; 163 } 164 165 .login-modal-content h2 { 166 margin: 0 0 1.5rem 0; 167 color: var(--sky-slate-dark); 168 font-size: 1.75rem; 169 text-align: center; 170 } 171 172 .login-modal-content form { 173 display: flex; 174 flex-direction: column; 175 gap: 1rem; 176 } 177 178 .login-modal-content :global(.login-input) { 179 width: 100%; 180 } 181 182 .login-modal-content :global(.login-button) { 183 width: 100%; 184 } 185 186 .avatar-placeholder { 187 width: 80px; 188 height: 80px; 189 border-radius: 50%; 190 background: var(--sky-cloud); 191 animation: pulse 1.5s ease-in-out infinite; 192 } 193 194 @keyframes pulse { 195 0%, 100% { 196 opacity: 0.6; 197 } 198 50% { 199 opacity: 1; 200 } 201 } 202 203 @media (max-width: 768px) { 204 .avatar-placeholder { 205 width: 60px; 206 height: 60px; 207 } 208 } 209</style>