v0.1 64c03e0

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

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