1<!-- AIM Login Component Updated for Svelte5 with Sign In/Sign Up Tabs -->
2<script lang="ts">
3 import { draggable } from '$lib/actions/draggable';
4 import type { RegisterResponse, RegisterResponseError } from '$lib/types/payloads';
5 import type { User } from '$lib/types/chat';
6 import { invalidate } from '$app/navigation';
7 import { createEventDispatcher } from 'svelte';
8
9 let { onLoginSuccess }: { onLoginSuccess?: (user: User) => void } = $props();
10 const dispatch = createEventDispatcher();
11
12 let activeTab = $state('signin');
13
14 // Sign In state
15 let siUsername = $state('');
16 let siPassword = $state('');
17 let savePassword = $state(true);
18 let autoLogin = $state(false);
19 let error = $state('');
20 let loginStatus = $state('idle');
21
22 // Sign Up state
23 let suUsername = $state('');
24 let suPassword = $state('');
25 let suConfirmPassword = $state('');
26 let captchaAnswer = $state('');
27 let signupError = $state('');
28 let signupStatus = $state('idle');
29
30 async function handleSigninSubmit() {
31 error = '';
32 loginStatus = 'loading';
33 if (!siUsername || !siPassword) {
34 error = 'Please enter both username and password';
35 loginStatus = 'error';
36 return;
37 }
38
39 console.debug("Attempting login for", siUsername);
40 try {
41 const res = await fetch('/api/session/login', {
42 method: 'POST',
43 headers: { 'Content-Type': 'application/json' },
44 body: JSON.stringify({
45 username: siUsername,
46 password: siPassword
47 })
48 });
49
50 const data = await res.json();
51 if (!res.ok) {
52 error = data.error || "Login failed.";
53 console.debug("Login failed:", data.error);
54 loginStatus = 'error';
55 return;
56 }
57
58 console.debug("Login succeeded:", data);
59 loginStatus = 'success';
60
61 // Invalidate all session-related dependencies
62 await Promise.all([
63 invalidate('app:session'),
64 invalidate('app:chat'),
65 invalidate('custom:chat:session'),
66 invalidate('chat:session')
67 ]);
68
69 // Call the callback prop if provided
70 onLoginSuccess?.(data.user);
71 // Auto close login window after 3 seconds on successful login
72 setTimeout(() => {
73 console.debug("Auto closing AIM Login component after successful login");
74 handleClose();
75 }, 3000);
76
77 return;
78 } catch (err) {
79 console.debug("Login error:", err);
80 error = "Login error. Please try again.";
81 loginStatus = 'error';
82 }
83 }
84
85 async function handleSignupSubmit() {
86 signupError = '';
87 signupStatus = 'loading';
88 if (!suUsername || !suPassword || !suConfirmPassword || !captchaAnswer) {
89 signupError = 'Please fill all fields';
90 signupStatus = 'error';
91 return;
92 }
93 if (suPassword !== suConfirmPassword) {
94 signupError = 'Passwords do not match';
95 signupStatus = 'error';
96 return;
97 }
98
99 console.debug("Attempting registration for", suUsername);
100 try {
101 const res = await fetch('/api/register', {
102 method: 'POST',
103 headers: { 'Content-Type': 'application/json' },
104 body: JSON.stringify({ suUsername, suPassword, suConfirmPassword, captchaAnswer })
105 });
106
107 const data = await res.json() as RegisterResponse;
108 if (!res.ok) {
109 const errorData = data as RegisterResponseError;
110 signupError = errorData.error || "Registration failed.";
111 signupStatus = 'error';
112 return;
113 }
114 console.debug("Registration succeeded:", data);
115 signupStatus = 'success';
116
117 // Auto login after registration
118 await handleAutoLoginSignup();
119 return;
120 } catch (err) {
121 console.debug("Registration error", err);
122 signupError = "Registration error. Please try again.";
123 signupStatus = 'error';
124 }
125 }
126
127 async function handleAutoLoginSignup() {
128 try {
129 const res = await fetch('/api/session/login', {
130 method: 'POST',
131 headers: { 'Content-Type': 'application/json' },
132 body: JSON.stringify({ username: suUsername, password: suPassword })
133 });
134 const data = await res.json();
135 if (!res.ok) {
136 signupError = data.error || "Login failed after registration.";
137 signupStatus = 'error';
138 return;
139 }
140 console.debug("Auto login after registration succeeded:", data);
141 await Promise.all([
142 invalidate('app:session'),
143 invalidate('app:chat'),
144 invalidate('custom:chat:session'),
145 invalidate('chat:session')
146 ]);
147 onLoginSuccess?.(data.user);
148 setTimeout(() => {
149 console.debug("Auto closing AIM Login component after auto login on registration");
150 handleClose();
151 }, 3000);
152 } catch (err) {
153 console.debug("Auto login error:", err);
154 signupError = "Auto login error. Please try to login manually.";
155 signupStatus = 'error';
156 }
157 }
158
159 function switchTab(tab: string) {
160 activeTab = tab;
161 error = '';
162 signupError = '';
163 }
164
165 function handleClose() {
166 console.debug("Closing AIM Login component.");
167 dispatch('close');
168 }
169
170 function handleKeydown(event: KeyboardEvent) {
171 if (event.key === 'Escape') {
172 console.debug("Escape key pressed, closing AIM Login component.");
173 handleClose();
174 }
175 }
176
177 // Added dragging state and handler similar to chat-room
178 let windowX = $state(0);
179 let windowY = $state(0);
180
181 function handleDragMove(event: CustomEvent<{ x: number; y: number }>) {
182 windowX = event.detail.x;
183 windowY = event.detail.y;
184 }
185</script>
186
187<svelte:window onkeydown={handleKeydown} />
188
189<!-- Updated outer div to handle dragging with inline style and drag event -->
190<div class="login-window window" use:draggable={{ handle: '.title-bar' }} ondragmove={handleDragMove} style="left: {windowX}px; top: {windowY}px;">
191 <div class="title-bar">
192 <div class="title-bar-text">
193 <img src="/desktop/aim-desktop-icon.png" alt="AIM" class="title-icon" width="16" height="16" />
194 {activeTab === 'signin' ? 'Sign In' : 'Sign Up'}
195 </div>
196 <div class="title-bar-controls">
197 <button aria-label="Close" onclick={handleClose}>×</button>
198 </div>
199 </div>
200
201 <div class="tabs">
202 <button class:active={activeTab === 'signin'} onclick={() => switchTab('signin')}>Sign In</button>
203 <button class:active={activeTab === 'signup'} onclick={() => switchTab('signup')}>Sign Up</button>
204 </div>
205
206 <div class="window-content">
207 {#if activeTab === 'signin'}
208 <form onsubmit={handleSigninSubmit}>
209 <div class="form-group">
210 <label for="si-username">Username</label>
211 <input type="text" id="si-username" bind:value={siUsername} placeholder="Username" />
212 </div>
213 <div class="form-group">
214 <label for="si-password">Password</label>
215 <input type="password" id="si-password" bind:value={siPassword} placeholder="Password" />
216 </div>
217 <div class="checkboxes">
218 <label>
219 <input type="checkbox" bind:checked={savePassword} />
220 Save password
221 </label>
222 <label>
223 <input type="checkbox" bind:checked={autoLogin} />
224 Auto-login
225 </label>
226 </div>
227 {#if error}
228 <div class="error">{error}</div>
229 {/if}
230 <div class="button-bar">
231 <button type="submit" class="icon-button {loginStatus === 'success' ? 'success-border' : loginStatus === 'error' ? 'error-border' : ''}">
232 {#if loginStatus === 'loading'}
233 <svg width="24" height="24" viewBox="0 0 24 24">
234 <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none" opacity="0.25"></circle>
235 <path d="M12 2a10 10 0 0 1 0 20" stroke="currentColor" stroke-width="2" fill="none">
236 <animateTransform attributeName="transform" type="rotate" values="0 12 12;360 12 12" dur="1s" repeatCount="indefinite"/>
237 </path>
238 </svg>
239 {:else if loginStatus === 'success'}
240 <svg width="24" height="24" viewBox="0 0 24 24">
241 <path d="M20 6L9 17l-5-5" stroke="green" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
242 </svg>
243 {:else if loginStatus === 'error'}
244 <svg width="24" height="24" viewBox="0 0 24 24">
245 <circle cx="12" cy="12" r="10" stroke="red" stroke-width="2" fill="none"/>
246 <line x1="12" y1="8" x2="12" y2="13" stroke="red" stroke-width="2" stroke-linecap="round"/>
247 <circle cx="12" cy="17" r="1" fill="red"/>
248 </svg>
249 {:else}
250 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
251 <path d="M12 4C9.24 4 7 6.24 7 9c0 2.76 2.24 5 5 5s5-2.24 5-5c0-2.76-2.24-5-5-5zm0 2c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 10c-3.33 0-7 1.67-7 5v1h14v-1c0-3.33-3.67-5-7-5zm0 2c2.67 0 5 1.33 5 3H7c0-1.67 2.33-3 5-3z" fill="currentColor"/>
252 </svg>
253 {/if}
254 Sign In
255 </button>
256 </div>
257 </form>
258 {:else}
259 <form onsubmit={handleSignupSubmit}>
260 <div class="form-group">
261 <label for="su-username">Username</label>
262 <input type="text" id="su-username" bind:value={suUsername} placeholder="Choose a username" />
263 </div>
264 <div class="form-group">
265 <label for="su-password">Password</label>
266 <input type="password" id="su-password" bind:value={suPassword} placeholder="Password" />
267 </div>
268 <div class="form-group">
269 <label for="su-confirm-password">Confirm Password</label>
270 <input type="password" id="su-confirm-password" bind:value={suConfirmPassword} placeholder="Confirm Password" />
271 </div>
272 <div class="form-group">
273 <label for="captcha">What's the meaning of PDR?</label>
274 <input type="text" id="captcha" bind:value={captchaAnswer} placeholder="Enter your answer" />
275 </div>
276 {#if signupError}
277 <div class="error">{signupError}</div>
278 {/if}
279 <div class="button-bar">
280 <button type="submit" class="icon-button {signupStatus === 'success' ? 'success-border' : signupStatus === 'error' ? 'error-border' : ''}">
281 {#if signupStatus === 'loading'}
282 <svg width="24" height="24" viewBox="0 0 24 24">
283 <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none" opacity="0.25"></circle>
284 <path d="M12 2a10 10 0 0 1 0 20" stroke="currentColor" stroke-width="2" fill="none">
285 <animateTransform attributeName="transform" type="rotate" values="0 12 12;360 12 12" dur="1s" repeatCount="indefinite"></animateTransform>
286 </path>
287 </svg>
288 {:else if signupStatus === 'success'}
289 <svg width="24" height="24" viewBox="0 0 24 24">
290 <path d="M20 6L9 17l-5-5" stroke="green" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"></path>
291 </svg>
292 {:else if signupStatus === 'error'}
293 <svg width="24" height="24" viewBox="0 0 24 24">
294 <circle cx="12" cy="12" r="10" stroke="red" stroke-width="2" fill="none"></circle>
295 <line x1="12" y1="8" x2="12" y2="13" stroke="red" stroke-width="2" stroke-linecap="round"></line>
296 <circle cx="12" cy="17" r="1" fill="red"></circle>
297 </svg>
298 {:else}
299 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
300 <path d="M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" fill="currentColor"></path>
301 </svg>
302 {/if}
303 Sign Up
304 </button>
305 </div>
306 </form>
307 {/if}
308 </div>
309</div>
310
311<style>
312 .login-window {
313 width: 300px;
314 background: #ECE9D8;
315 /* Changed to fixed positioning for correct dragging */
316 position: fixed;
317 }
318 .title-bar {
319 background: linear-gradient(180deg, #0054E3 0%, #0047AB 100%);
320 padding: 4px 6px;
321 display: flex;
322 justify-content: space-between;
323 align-items: center;
324 color: white;
325 font-weight: bold;
326 user-select: none;
327 position: relative;
328 }
329 .title-bar-text {
330 display: flex;
331 align-items: center;
332 gap: 4px;
333 font-size: 0.875rem;
334 }
335
336 .tabs {
337 padding: 0 3px;
338 display: flex;
339 border-bottom: 1px solid #0054E3;
340 }
341 .tabs button {
342 flex: 1;
343 padding: 8px;
344 border: none;
345 background: #ECE9D8;
346 font-size: 1rem;
347 cursor: pointer;
348 }
349 .tabs button.active {
350 background: #fff;
351 border-bottom: 2px solid #0054E3;
352 font-weight: bold;
353 }
354 .window-content {
355 padding: 16px;
356 }
357 .form-group {
358 margin-bottom: 16px;
359 }
360 .form-group label {
361 display: block;
362 margin-bottom: 4px;
363 font-size: 1rem;
364 }
365 .form-group input {
366 width: 100%;
367 padding: 4px;
368 font-size: 1rem;
369 }
370 .checkboxes {
371 display: flex;
372 gap: 16px;
373 margin-bottom: 16px;
374 font-size: 1rem;
375 }
376 .checkboxes label {
377 display: flex;
378 align-items: center;
379 gap: 4px;
380 }
381 .button-bar {
382 display: flex;
383 justify-content: center;
384 margin-top: 20px;
385 }
386 .icon-button {
387 display: flex;
388 flex-direction: column;
389 align-items: center;
390 gap: 4px;
391 background: none;
392 border: none;
393 cursor: pointer;
394 font-size: 1rem;
395 color: black;
396 }
397 .error {
398 color: red;
399 font-size: 1rem;
400 margin-top: 8px;
401 }
402 .success-border {
403 border: 2px solid green;
404 }
405 .error-border {
406 border: 2px solid red;
407 }
408</style>