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