source: petify-frontend/src/views/LoginView.vue@ ae83647

Last change on this file since ae83647 was ae83647, checked in by veronika-ils <ilioskaveronika@…>, 2 days ago

add functionality so that users can change passwords

  • Property mode set to 100644
File size: 13.4 KB
Line 
1<template>
2 <main class="auth-wrap">
3 <!-- animated blobs -->
4 <div class="blob blob-1" aria-hidden="true"></div>
5 <div class="blob blob-2" aria-hidden="true"></div>
6
7 <div class="auth-card card border-0 shadow-lg">
8 <div class="card-body p-4 p-md-5">
9 <!-- Header -->
10 <div class="d-flex align-items-center gap-3 mb-4">
11 <div class="brand-badge" aria-hidden="true">
12 <!-- paw -->
13 <svg width="22" height="22" viewBox="0 0 24 24" fill="none">
14 <path
15 d="M7.6 10.2c-1.1 0-2-.98-2-2.2s.9-2.2 2-2.2 2 .98 2 2.2-.9 2.2-2 2.2Zm8.8 0c-1.1 0-2-.98-2-2.2s.9-2.2 2-2.2 2 .98 2 2.2-.9 2.2-2 2.2ZM10 8.2c-1 0-1.8-.9-1.8-2s.8-2 1.8-2 1.8.9 1.8 2-.8 2-1.8 2Zm4 0c-1 0-1.8-.9-1.8-2s.8-2 1.8-2 1.8.9 1.8 2-.8 2-1.8 2ZM12 21c-2.6 0-5.6-1.6-5.6-4.5 0-2.2 1.9-3.7 3.9-3.7.8 0 1.4.2 1.7.5.3-.3.9-.5 1.7-.5 2 0 3.9 1.5 3.9 3.7C17.6 19.4 14.6 21 12 21Z"
16 fill="currentColor"
17 opacity=".92"
18 />
19 </svg>
20 </div>
21
22 <div class="flex-grow-1">
23 <h1 class="h4 mb-1 title">Welcome back</h1>
24 <p class="text-muted mb-0">Log in to manage your Petify account.</p>
25 </div>
26 </div>
27
28 <!-- Error -->
29 <div v-if="error" class="alert alert-danger auth-alert" role="alert">
30 <strong class="me-1">Oops.</strong>{{ error }}
31 </div>
32 <div v-if="forgotSuccess" class="alert alert-success auth-alert" role="alert">
33 {{ forgotSuccess }}
34 </div>
35
36 <form @submit.prevent="submit" class="mt-3">
37 <!-- Username -->
38 <div class="mb-3">
39 <label class="form-label" for="username">Username</label>
40 <div class="input-group auth-input">
41 <span class="input-group-text">
42 <!-- user -->
43 <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
44 <path
45 d="M12 12a4.5 4.5 0 1 0-4.5-4.5A4.5 4.5 0 0 0 12 12Zm0 2.25c-4.42 0-8 2.24-8 5v.75h16v-.75c0-2.76-3.58-5-8-5Z"
46 fill="currentColor"
47 opacity=".85"
48 />
49 </svg>
50 </span>
51 <input
52 id="username"
53 v-model.trim="username"
54 class="form-control"
55 type="text"
56 autocomplete="username"
57 required
58 placeholder="e.g. username123"
59 />
60 </div>
61 </div>
62
63 <!-- Password + show/hide -->
64 <div class="mb-2">
65 <label class="form-label" for="password">Password</label>
66
67 <div class="input-group auth-input">
68 <span class="input-group-text">
69 <!-- lock -->
70 <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
71 <path
72 d="M17 10h-1V8a4 4 0 0 0-8 0v2H7a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2Zm-7-2a2 2 0 0 1 4 0v2h-4V8Z"
73 fill="currentColor"
74 opacity=".85"
75 />
76 </svg>
77 </span>
78
79 <input
80 id="password"
81 v-model="password"
82 class="form-control"
83 :type="showPassword ? 'text' : 'password'"
84 autocomplete="current-password"
85 required
86 minlength="6"
87 placeholder="••••••••"
88 />
89
90 <button
91 class="btn btn-eye"
92 type="button"
93 :aria-label="showPassword ? 'Hide password' : 'Show password'"
94 :title="showPassword ? 'Hide password' : 'Show password'"
95 @click="togglePassword"
96 >
97 <!-- eye / eye-off -->
98 <svg v-if="!showPassword" width="18" height="18" viewBox="0 0 24 24" fill="none">
99 <path
100 d="M12 5c5.5 0 9.5 5.5 9.5 7s-4 7-9.5 7S2.5 14.5 2.5 12 6.5 5 12 5Zm0 2C8 7 4.9 10.8 4.9 12S8 17 12 17s7.1-3.8 7.1-5S16 7 12 7Zm0 2.5A2.5 2.5 0 1 1 9.5 12 2.5 2.5 0 0 1 12 9.5Z"
101 fill="currentColor"
102 opacity=".85"
103 />
104 </svg>
105 <svg v-else width="18" height="18" viewBox="0 0 24 24" fill="none">
106 <path
107 d="M3.3 4.7 4.7 3.3l16 16-1.4 1.4-2.3-2.3c-1.4.8-3 1.3-4.9 1.3-5.5 0-9.5-5.5-9.5-7 0-1.2 2.2-4.3 5.7-6.1L3.3 4.7ZM12 7c-.9 0-1.8.2-2.6.5l1.7 1.7c.3-.1.6-.2.9-.2A2.5 2.5 0 0 1 14.5 12c0 .3-.1.6-.2.9l1.7 1.7c.3-.8.5-1.7.5-2.6 0-3-2.4-5-4.5-5Zm0 10c.9 0 1.8-.2 2.6-.5l-1.7-1.7c-.3.1-.6.2-.9.2A2.5 2.5 0 0 1 9.5 12c0-.3.1-.6.2-.9L8 9.4c-.3.8-.5 1.7-.5 2.6 0 3 2.4 5 4.5 5Zm0-12c5.5 0 9.5 5.5 9.5 7 0 .8-1.3 2.8-3.4 4.5l-1.4-1.4c1.7-1.4 2.4-2.8 2.4-3.1 0-1.2-3.1-5-7.1-5-1.2 0-2.3.3-3.3.7L7.2 6.3C8.6 5.5 10.2 5 12 5Z"
108 fill="currentColor"
109 opacity=".85"
110 />
111 </svg>
112 </button>
113 </div>
114 </div>
115
116 <!-- Remember me -->
117 <div class="d-flex justify-content-between align-items-center mt-2 mb-3">
118 <span></span>
119 <button class="link-button small accent" type="button" @click="toggleForgotPassword">
120 Forgot password?
121 </button>
122 </div>
123
124 <!-- CTA -->
125 <button class="btn btn-primary w-100 btn-orange" type="submit" :disabled="loading">
126 <span v-if="loading" class="spinner-border spinner-border-sm me-2" aria-hidden="true"></span>
127 {{ loading ? 'Logging in…' : 'Log in' }}
128 </button>
129 </form>
130
131 <form v-if="showForgotPassword" class="forgot-panel mt-4" @submit.prevent="submitForgotPassword">
132 <label class="form-label" for="forgot-identifier">Username or email</label>
133 <div class="input-group auth-input">
134 <span class="input-group-text">
135 <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
136 <path
137 d="M4 6.5A2.5 2.5 0 0 1 6.5 4h11A2.5 2.5 0 0 1 20 6.5v11a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 4 17.5v-11Zm2.5-.5a.5.5 0 0 0-.5.5v.8l6 3.7 6-3.7v-.8a.5.5 0 0 0-.5-.5h-11Zm11.5 3.6-5.5 3.4a1 1 0 0 1-1 0L6 9.6v7.9a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V9.6Z"
138 fill="currentColor"
139 opacity=".85"
140 />
141 </svg>
142 </span>
143 <input
144 id="forgot-identifier"
145 v-model.trim="forgotIdentifier"
146 class="form-control"
147 type="text"
148 autocomplete="username"
149 required
150 placeholder="username or email"
151 />
152 </div>
153 <div v-if="forgotError" class="text-danger small mt-2">{{ forgotError }}</div>
154 <button class="btn btn-outline-primary w-100 mt-3" type="submit" :disabled="forgotLoading">
155 <span v-if="forgotLoading" class="spinner-border spinner-border-sm me-2" aria-hidden="true"></span>
156 {{ forgotLoading ? 'Sending...' : 'Send temporary password' }}
157 </button>
158 </form>
159
160 <!-- Footer links -->
161 <div class="d-flex justify-content-between mt-4">
162 <RouterLink class="link subtle" to="/">Back to listings</RouterLink>
163 <RouterLink class="link accent" to="/signup">Create account</RouterLink>
164 </div>
165 </div>
166 </div>
167
168 <p class="text-center mt-3 small text-muted mb-0">
169 By logging in you agree to treat pets with kindness 🧡
170 </p>
171 </main>
172</template>
173
174<script setup lang="ts">
175import { ref } from 'vue'
176import { useRoute, useRouter } from 'vue-router'
177import { useAuthStore } from '../stores/auth'
178import { forgotPassword } from '../api/auth'
179
180const auth = useAuthStore()
181const router = useRouter()
182const route = useRoute()
183
184const username = ref('')
185const password = ref('')
186const loading = ref(false)
187const error = ref<string | null>(null)
188const forgotLoading = ref(false)
189const forgotError = ref<string | null>(null)
190const forgotSuccess = ref<string | null>(null)
191const showForgotPassword = ref(false)
192const forgotIdentifier = ref('')
193
194const showPassword = ref(false)
195const rememberMe = ref(true)
196
197function togglePassword() {
198 showPassword.value = !showPassword.value
199}
200
201function toggleForgotPassword() {
202 showForgotPassword.value = !showForgotPassword.value
203 forgotError.value = null
204 forgotSuccess.value = null
205 if (showForgotPassword.value && !forgotIdentifier.value) {
206 forgotIdentifier.value = username.value
207 }
208}
209
210async function submit() {
211 error.value = null
212 loading.value = true
213 try {
214 // If you want to actually persist "remember me", hook this into your auth store
215 // (e.g., store token in localStorage vs sessionStorage).
216 await auth.login({ username: username.value, password: password.value })
217
218 const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/'
219 await router.replace(redirect)
220 } catch (e) {
221 error.value = e instanceof Error ? e.message : String(e)
222 } finally {
223 loading.value = false
224 }
225}
226
227async function submitForgotPassword() {
228 forgotError.value = null
229 forgotSuccess.value = null
230 forgotLoading.value = true
231 try {
232 forgotSuccess.value = await forgotPassword({ identifier: forgotIdentifier.value })
233 showForgotPassword.value = false
234 } catch (e) {
235 forgotError.value = e instanceof Error ? e.message : String(e)
236 } finally {
237 forgotLoading.value = false
238 }
239}
240</script>
241
242<style scoped>
243/* Theme tokens */
244.auth-wrap {
245 --petify-orange: #ff7a18;
246 --petify-orange-2: #ff9a3d;
247 --petify-ink: #1f2937;
248
249 position: relative;
250 overflow: hidden;
251 min-height: 100vh;
252 display: grid;
253 place-items: center;
254 padding: 3rem 1rem;
255
256 background:
257 radial-gradient(1200px 600px at 15% 0%, rgba(255, 122, 24, 0.18), transparent 60%),
258 radial-gradient(900px 500px at 95% 15%, rgba(255, 154, 61, 0.14), transparent 55%),
259 linear-gradient(180deg, #fff7f0 0%, #ffffff 45%, #ffffff 100%);
260}
261
262/* Animated blobs */
263.blob {
264 position: absolute;
265 width: 520px;
266 height: 520px;
267 border-radius: 999px;
268 filter: blur(42px);
269 opacity: 0.55;
270 pointer-events: none;
271 mix-blend-mode: multiply;
272 animation: floaty 10s ease-in-out infinite;
273 background: radial-gradient(circle at 30% 30%, rgba(255, 122, 24, 0.55), rgba(255, 154, 61, 0.12));
274}
275
276.blob-1 {
277 top: -220px;
278 left: -220px;
279}
280
281.blob-2 {
282 bottom: -240px;
283 right: -240px;
284 animation-delay: -3.5s;
285}
286
287@keyframes floaty {
288 0% {
289 transform: translate3d(0, 0, 0) scale(1);
290 }
291 50% {
292 transform: translate3d(28px, 18px, 0) scale(1.06);
293 }
294 100% {
295 transform: translate3d(0, 0, 0) scale(1);
296 }
297}
298
299/* Card */
300.auth-card {
301 width: 100%;
302 max-width: 520px;
303 border-radius: 18px;
304 overflow: hidden;
305 position: relative;
306 z-index: 1;
307 backdrop-filter: blur(6px);
308}
309
310/* top accent strip */
311.auth-card::before {
312 content: "";
313 position: absolute;
314 inset: 0 0 auto 0;
315 height: 6px;
316 background: linear-gradient(90deg, var(--petify-orange), var(--petify-orange-2));
317}
318
319.title {
320 color: var(--petify-ink);
321 letter-spacing: -0.2px;
322}
323
324.brand-badge {
325 width: 46px;
326 height: 46px;
327 border-radius: 14px;
328 display: grid;
329 place-items: center;
330 color: #fff;
331 background: linear-gradient(135deg, var(--petify-orange), var(--petify-orange-2));
332 box-shadow: 0 10px 25px rgba(255, 122, 24, 0.28);
333}
334
335.auth-alert {
336 border-radius: 12px;
337}
338
339.forgot-panel {
340 border-top: 1px solid rgba(31, 41, 55, 0.1);
341 padding-top: 1rem;
342}
343
344/* Input styling */
345.auth-input .input-group-text {
346 border-radius: 12px 0 0 12px;
347 background: #fff;
348 border-color: rgba(31, 41, 55, 0.12);
349 color: rgba(31, 41, 55, 0.7);
350}
351
352.auth-input .form-control {
353 border-radius: 0;
354 border-color: rgba(31, 41, 55, 0.12);
355 padding: 0.75rem 0.9rem;
356}
357
358.auth-input .form-control:focus {
359 box-shadow: 0 0 0 0.25rem rgba(255, 122, 24, 0.18);
360 border-color: rgba(255, 122, 24, 0.55);
361}
362
363/* Eye button */
364.btn-eye {
365 border-radius: 0 12px 12px 0;
366 border: 1px solid rgba(31, 41, 55, 0.12);
367 border-left: 0;
368 background: #fff;
369 color: rgba(31, 41, 55, 0.7);
370 padding: 0 0.85rem;
371}
372
373.btn-eye:hover {
374 color: rgba(31, 41, 55, 0.95);
375}
376
377.btn-eye:focus {
378 box-shadow: 0 0 0 0.25rem rgba(255, 122, 24, 0.18);
379}
380
381/* Fix rounding for group with eye button */
382.auth-input .input-group-text {
383 border-right: 0;
384}
385.auth-input .form-control {
386 border-left: 0;
387}
388.auth-input .form-control:first-child {
389 border-radius: 12px;
390}
391.auth-input .form-control {
392 border-radius: 0;
393}
394.auth-input .btn-eye {
395 border-left: 0;
396}
397
398/* Orange CTA button */
399.btn-orange {
400 border: none;
401 border-radius: 12px;
402 padding: 0.8rem 1rem;
403 font-weight: 600;
404 background: linear-gradient(135deg, var(--petify-orange), var(--petify-orange-2));
405 box-shadow: 0 12px 24px rgba(255, 122, 24, 0.25);
406}
407
408.btn-orange:disabled {
409 opacity: 0.75;
410 box-shadow: none;
411}
412
413.btn-orange:hover {
414 filter: brightness(0.98);
415}
416
417/* Links */
418.link {
419 text-decoration: none;
420}
421
422.link.subtle {
423 color: rgba(31, 41, 55, 0.75);
424}
425
426.link.subtle:hover {
427 color: rgba(31, 41, 55, 0.95);
428}
429
430.link.accent {
431 color: #ff7a18;
432 font-weight: 600;
433}
434
435.link.accent:hover {
436 color: #e76610;
437}
438
439.link-button {
440 border: 0;
441 background: transparent;
442 padding: 0;
443 text-decoration: none;
444}
445
446.link-button.accent {
447 color: #ff7a18;
448 font-weight: 600;
449}
450
451.link-button.accent:hover {
452 color: #e76610;
453}
454
455/* Respect reduced motion */
456@media (prefers-reduced-motion: reduce) {
457 .blob {
458 animation: none;
459 }
460}
461</style>
Note: See TracBrowser for help on using the repository browser.