|
1 | 1 | <script setup lang="ts"> |
2 | 2 | import { computed, onMounted, ref } from "vue"; |
3 | 3 | import { RouterLink, useRoute, useRouter } from "vue-router"; |
| 4 | +import { |
| 5 | + ArrowLeft, |
| 6 | + Fingerprint, |
| 7 | + KeyRound, |
| 8 | + Mail, |
| 9 | + Sparkles, |
| 10 | +} from "lucide-vue-next"; |
4 | 11 |
|
5 | 12 | import { Alert, AlertDescription } from "@/components/ui/alert"; |
6 | 13 | import { Button } from "@/components/ui/button"; |
@@ -76,135 +83,179 @@ async function onSubmit() { |
76 | 83 | </script> |
77 | 84 |
|
78 | 85 | <template> |
79 | | - <div class="mx-auto w-full max-w-md"> |
80 | | - <Card> |
81 | | - <CardHeader> |
82 | | - <CardTitle>Sign in</CardTitle> |
83 | | - <CardDescription> |
84 | | - Use your account email and password, or start a local demo session. |
85 | | - </CardDescription> |
86 | | - </CardHeader> |
87 | | - <CardContent> |
88 | | - <Alert |
89 | | - v-if="registeredOk" |
90 | | - class="mb-4" |
91 | | - role="status" |
92 | | - > |
93 | | - <AlertDescription> |
94 | | - Account created. Sign in with the same email and password. |
95 | | - </AlertDescription> |
96 | | - </Alert> |
97 | | - |
98 | | - <Alert |
99 | | - v-if="lastError" |
100 | | - class="mb-4" |
101 | | - role="alert" |
102 | | - variant="destructive" |
103 | | - > |
104 | | - <AlertDescription> |
105 | | - {{ lastError }} |
106 | | - </AlertDescription> |
107 | | - </Alert> |
108 | | - |
109 | | - <form class="space-y-4" @submit.prevent="onSubmit"> |
110 | | - <div class="space-y-2"> |
111 | | - <Label for="login-email">Email</Label> |
112 | | - <Input |
113 | | - id="login-email" |
114 | | - v-model="email" |
115 | | - autocomplete="username" |
116 | | - :disabled="loading" |
117 | | - name="email" |
118 | | - required |
119 | | - type="email" |
120 | | - /> |
121 | | - </div> |
122 | | - <div class="space-y-2"> |
123 | | - <Label for="login-password">Password</Label> |
124 | | - <Input |
125 | | - id="login-password" |
126 | | - v-model="password" |
127 | | - autocomplete="current-password" |
128 | | - :disabled="loading" |
129 | | - name="password" |
130 | | - required |
131 | | - type="password" |
132 | | - /> |
133 | | - </div> |
134 | | - <div class="flex items-center gap-2"> |
135 | | - <Checkbox |
136 | | - id="remember" |
137 | | - v-model:checked="rememberSession" |
138 | | - :disabled="loading" |
139 | | - /> |
140 | | - <Label |
141 | | - class="text-muted-foreground font-normal" |
142 | | - for="remember" |
143 | | - > |
144 | | - Remember this device |
145 | | - </Label> |
146 | | - </div> |
| 86 | + <div class="flex flex-1 flex-col items-center justify-center px-4 py-12"> |
| 87 | + <div class="w-full max-w-sm"> |
| 88 | + <Card> |
| 89 | + <CardHeader class="pb-4"> |
| 90 | + <CardTitle class="text-lg">Sign in</CardTitle> |
| 91 | + <CardDescription> |
| 92 | + Use your email and password, or start a local demo. |
| 93 | + </CardDescription> |
| 94 | + </CardHeader> |
147 | 95 |
|
148 | | - <div |
149 | | - v-if="twoFactorRequired" |
150 | | - class="border-border space-y-3 border-t pt-4" |
| 96 | + <CardContent class="space-y-4"> |
| 97 | + <Alert |
| 98 | + v-if="registeredOk" |
| 99 | + class="border-emerald-500/20 bg-emerald-500/10 text-emerald-700" |
| 100 | + role="status" |
151 | 101 | > |
152 | | - <p class="text-muted-foreground text-sm"> |
153 | | - Enter a 6-digit code from your authenticator app, or a recovery |
154 | | - code. |
155 | | - </p> |
| 102 | + <AlertDescription> |
| 103 | + Account created successfully. Sign in with the same email and |
| 104 | + password. |
| 105 | + </AlertDescription> |
| 106 | + </Alert> |
| 107 | + |
| 108 | + <Alert |
| 109 | + v-if="lastError" |
| 110 | + class="border-destructive/20 bg-destructive/10 text-destructive" |
| 111 | + role="alert" |
| 112 | + > |
| 113 | + <AlertDescription>{{ lastError }}</AlertDescription> |
| 114 | + </Alert> |
| 115 | + |
| 116 | + <Button |
| 117 | + class="w-full" |
| 118 | + :disabled="loading" |
| 119 | + type="button" |
| 120 | + variant="outline" |
| 121 | + @click="onDemo" |
| 122 | + > |
| 123 | + <Sparkles class="mr-2 size-4" /> |
| 124 | + Try the demo |
| 125 | + </Button> |
| 126 | + |
| 127 | + <div class="relative"> |
| 128 | + <div class="absolute inset-0 flex items-center"> |
| 129 | + <span class="w-full border-t"></span> |
| 130 | + </div> |
| 131 | + <div class="relative flex justify-center text-xs uppercase"> |
| 132 | + <span class="bg-card text-muted-foreground px-2"> |
| 133 | + or continue with email |
| 134 | + </span> |
| 135 | + </div> |
| 136 | + </div> |
| 137 | + |
| 138 | + <form class="space-y-4" @submit.prevent="onSubmit"> |
156 | 139 | <div class="space-y-2"> |
157 | | - <Label for="login-otp">Authenticator code</Label> |
158 | | - <Input |
159 | | - id="login-otp" |
160 | | - v-model="authenticatorToken" |
161 | | - autocomplete="one-time-code" |
162 | | - :disabled="loading" |
163 | | - inputmode="numeric" |
164 | | - maxlength="6" |
165 | | - pattern="[0-9]*" |
166 | | - placeholder="000000" |
167 | | - type="text" |
168 | | - /> |
| 140 | + <Label for="login-email">Email</Label> |
| 141 | + <div class="relative"> |
| 142 | + <Mail |
| 143 | + class="text-muted-foreground absolute top-1/2 left-3 size-4 -translate-y-1/2" |
| 144 | + /> |
| 145 | + <Input |
| 146 | + id="login-email" |
| 147 | + v-model="email" |
| 148 | + autocomplete="username" |
| 149 | + class="pl-9" |
| 150 | + :disabled="loading" |
| 151 | + name="email" |
| 152 | + placeholder="you@example.com" |
| 153 | + required |
| 154 | + type="email" |
| 155 | + /> |
| 156 | + </div> |
169 | 157 | </div> |
| 158 | + |
170 | 159 | <div class="space-y-2"> |
171 | | - <Label for="login-recovery">Recovery code (optional)</Label> |
172 | | - <Input |
173 | | - id="login-recovery" |
174 | | - v-model="recoveryCode" |
| 160 | + <Label for="login-password">Password</Label> |
| 161 | + <div class="relative"> |
| 162 | + <KeyRound |
| 163 | + class="text-muted-foreground absolute top-1/2 left-3 size-4 -translate-y-1/2" |
| 164 | + /> |
| 165 | + <Input |
| 166 | + id="login-password" |
| 167 | + v-model="password" |
| 168 | + autocomplete="current-password" |
| 169 | + class="pl-9" |
| 170 | + :disabled="loading" |
| 171 | + name="password" |
| 172 | + placeholder="••••••••" |
| 173 | + required |
| 174 | + type="password" |
| 175 | + /> |
| 176 | + </div> |
| 177 | + </div> |
| 178 | + |
| 179 | + <div class="flex items-center gap-2"> |
| 180 | + <Checkbox |
| 181 | + id="remember" |
| 182 | + v-model:checked="rememberSession" |
175 | 183 | :disabled="loading" |
176 | | - inputmode="text" |
177 | | - maxlength="32" |
178 | | - placeholder="32 hex characters" |
179 | | - spellcheck="false" |
180 | | - type="text" |
181 | 184 | /> |
| 185 | + <Label |
| 186 | + class="text-muted-foreground text-sm font-normal" |
| 187 | + for="remember" |
| 188 | + > |
| 189 | + Remember this device |
| 190 | + </Label> |
182 | 191 | </div> |
183 | | - </div> |
184 | 192 |
|
185 | | - <div class="flex flex-wrap gap-2"> |
186 | | - <Button :disabled="loading" type="submit" variant="default"> |
| 193 | + <div |
| 194 | + v-if="twoFactorRequired" |
| 195 | + class="border-border space-y-3 border-t pt-4" |
| 196 | + > |
| 197 | + <p class="text-muted-foreground flex items-center gap-2 text-sm"> |
| 198 | + <Fingerprint class="size-4" /> |
| 199 | + Enter a 6-digit code from your authenticator app, or a recovery |
| 200 | + code. |
| 201 | + </p> |
| 202 | + <div class="space-y-2"> |
| 203 | + <Label for="login-otp">Authenticator code</Label> |
| 204 | + <Input |
| 205 | + id="login-otp" |
| 206 | + v-model="authenticatorToken" |
| 207 | + autocomplete="one-time-code" |
| 208 | + :disabled="loading" |
| 209 | + inputmode="numeric" |
| 210 | + maxlength="6" |
| 211 | + pattern="[0-9]*" |
| 212 | + placeholder="000000" |
| 213 | + type="text" |
| 214 | + /> |
| 215 | + </div> |
| 216 | + <div class="space-y-2"> |
| 217 | + <Label for="login-recovery">Recovery code (optional)</Label> |
| 218 | + <Input |
| 219 | + id="login-recovery" |
| 220 | + v-model="recoveryCode" |
| 221 | + :disabled="loading" |
| 222 | + inputmode="text" |
| 223 | + maxlength="32" |
| 224 | + placeholder="32 hex characters" |
| 225 | + spellcheck="false" |
| 226 | + type="text" |
| 227 | + /> |
| 228 | + </div> |
| 229 | + </div> |
| 230 | + |
| 231 | + <Button :disabled="loading" class="w-full" type="submit"> |
| 232 | + <LogIn class="mr-2 size-4" /> |
187 | 233 | {{ loading ? "Signing in…" : "Sign in" }} |
188 | 234 | </Button> |
189 | | - <Button |
190 | | - :disabled="loading" |
191 | | - type="button" |
192 | | - variant="secondary" |
193 | | - @click="onDemo" |
| 235 | + </form> |
| 236 | + </CardContent> |
| 237 | + |
| 238 | + <CardFooter |
| 239 | + class="text-muted-foreground flex flex-col items-center gap-3 text-sm" |
| 240 | + > |
| 241 | + <div> |
| 242 | + Don't have an account? |
| 243 | + <RouterLink |
| 244 | + class="text-primary hover:text-primary/80 font-medium underline underline-offset-4" |
| 245 | + to="/register" |
194 | 246 | > |
195 | | - Try demo |
196 | | - </Button> |
| 247 | + Create one |
| 248 | + </RouterLink> |
197 | 249 | </div> |
198 | | - </form> |
199 | | - </CardContent> |
200 | | - <CardFooter class="flex flex-col items-start gap-2"> |
201 | | - <Button as-child class="p-0" size="sm" variant="link"> |
202 | | - <RouterLink to="/register">Create an account</RouterLink> |
203 | | - </Button> |
204 | | - <Button as-child class="p-0" size="sm" variant="link"> |
205 | | - <RouterLink to="/">← Home</RouterLink> |
206 | | - </Button> |
207 | | - </CardFooter> |
208 | | - </Card> |
| 250 | + <RouterLink |
| 251 | + class="inline-flex items-center gap-1.5 text-xs underline underline-offset-4" |
| 252 | + to="/" |
| 253 | + > |
| 254 | + <ArrowLeft class="size-3.5" /> |
| 255 | + Back to home |
| 256 | + </RouterLink> |
| 257 | + </CardFooter> |
| 258 | + </Card> |
| 259 | + </div> |
209 | 260 | </div> |
210 | 261 | </template> |
0 commit comments