web/src/routes/login/+page.svelte (view raw)
1<script lang="ts">
2 import { login, register } from '$lib/api';
3
4 let username = $state('');
5 let password = $state('');
6 let error = $state('');
7 let loading = $state(false);
8 let isRegister = $state(false);
9
10 async function handleSubmit(e: Event) {
11 e.preventDefault();
12 error = '';
13 loading = true;
14 try {
15 if (isRegister) {
16 await register(username, password);
17 }
18 await login(username, password);
19 window.location.href = '/browse';
20 } catch (err) {
21 error = err instanceof Error ? err.message : 'Login failed';
22 } finally {
23 loading = false;
24 }
25 }
26</script>
27
28<div class="page-head">
29 <h1>{isRegister ? 'REGISTER' : 'LOGIN'}</h1>
30 <div class="head-line"></div>
31</div>
32
33<form onsubmit={handleSubmit}>
34 {#if error}
35 <p class="error">{error}</p>
36 {/if}
37 <label>
38 <span class="label-text">USERNAME</span>
39 <input type="text" bind:value={username} required minlength={3} disabled={loading} />
40 </label>
41 <label>
42 <span class="label-text">PASSWORD</span>
43 <input type="password" bind:value={password} required minlength={8} disabled={loading} />
44 </label>
45 <button type="submit" class="btn" disabled={loading}>
46 {loading ? 'PLEASE WAIT...' : isRegister ? 'REGISTER' : 'LOGIN'}
47 </button>
48</form>
49
50<p class="toggle">
51 <button class="link" onclick={() => { isRegister = !isRegister; error = ''; }}>
52 {isRegister ? 'ALREADY HAVE AN ACCOUNT? LOG IN' : 'NO ACCOUNT? REGISTER'}
53 </button>
54</p>
55
56<style>
57 .page-head {
58 margin-bottom: 1.5rem;
59 }
60 .page-head h1 {
61 margin: 0;
62 }
63 .head-line {
64 height: 3px;
65 width: 3rem;
66 background: #c62828;
67 margin-top: 0.4rem;
68 }
69
70 form {
71 display: flex;
72 flex-direction: column;
73 gap: 1rem;
74 max-width: 340px;
75 }
76 label {
77 display: flex;
78 flex-direction: column;
79 gap: 0.25rem;
80 }
81 .label-text {
82 font-family: 'Oswald', sans-serif;
83 font-weight: 500;
84 font-size: 0.75rem;
85 letter-spacing: 0.15em;
86 color: #888;
87 }
88 input {
89 padding: 0.6rem 0.75rem;
90 border: 2px solid #1a1a1a;
91 border-radius: 0;
92 font-family: 'Inter', sans-serif;
93 font-size: 0.9rem;
94 font-weight: 500;
95 background: #fff;
96 }
97 input:focus {
98 outline: none;
99 border-color: #c62828;
100 }
101
102 .btn {
103 display: inline-flex;
104 align-items: center;
105 justify-content: center;
106 padding: 0.6rem 1.5rem;
107 background: #1a1a1a;
108 color: #fff;
109 border: none;
110 font-family: 'Oswald', sans-serif;
111 font-weight: 500;
112 font-size: 0.85rem;
113 letter-spacing: 0.1em;
114 cursor: pointer;
115 text-transform: uppercase;
116 transition: background 0.15s;
117 align-self: flex-start;
118 }
119 .btn:hover {
120 background: #c62828;
121 }
122 .btn:disabled {
123 opacity: 0.4;
124 cursor: default;
125 }
126
127 .error {
128 color: #c62828;
129 font-family: 'Inter', sans-serif;
130 font-size: 0.85rem;
131 font-weight: 600;
132 }
133
134 .toggle {
135 margin-top: 1rem;
136 }
137 .link {
138 background: none;
139 border: none;
140 font-family: 'Oswald', sans-serif;
141 font-weight: 500;
142 font-size: 0.8rem;
143 letter-spacing: 0.08em;
144 color: #c62828;
145 cursor: pointer;
146 padding: 0;
147 }
148 .link:hover {
149 text-decoration: underline;
150 }
151</style>