v0.1.3 ff61cc9

aim-login.svelte RawHistoryFolder

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> 

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 8

↑↓ moveenter openesc close