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.
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>