mitlist/fe/src/pages/LoginPage.vue
mohamad d6c5e6fcfd chore: Remove package-lock.json and enhance financials API with user summaries
This commit includes the following changes:

- Deleted the `package-lock.json` file to streamline dependency management.
- Updated the `financials.py` endpoint to return a comprehensive user financial summary, including net balance, total group spending, debts, and credits.
- Enhanced the `expense.py` CRUD operations to handle enum values and improve error handling during expense deletion.
- Introduced new schemas in `financials.py` for user financial summaries and debt/credit tracking.
- Refactored the costs service to improve group balance summary calculations.

These changes aim to improve the application's financial tracking capabilities and maintain cleaner dependency management.
2025-06-28 21:37:26 +02:00

233 lines
6.4 KiB
Vue

<template>
<main class="flex items-center justify-center page-container">
<div class="card login-card">
<div class="card-header">
<h3>mitlist</h3>
</div>
<div class="card-body">
<form @submit.prevent="onSubmit" class="form-layout">
<div class="form-group mb-2">
<Input v-model="email" :label="t('loginPage.emailLabel')" type="email" id="email" :error="formErrors.email"
autocomplete="email" />
</div>
<div class="form-group mb-3">
<Input v-model="password" :label="t('loginPage.passwordLabel')" :type="isPwdVisible ? 'text' : 'password'"
id="password" :error="formErrors.password" autocomplete="current-password" />
<button type="button" class="absolute right-3 top-[34px] text-sm text-gray-500"
@click="isPwdVisible = !isPwdVisible" :aria-label="t('loginPage.togglePasswordVisibilityLabel')">
{{ isPwdVisible ? 'Hide' : 'Show' }}
</button>
</div>
<p v-if="formErrors.general" class="alert alert-error form-error-text">{{ formErrors.general }}</p>
<Button type="submit" :disabled="loading" class="w-full mt-2">
<span v-if="loading" class="animate-pulse">{{ t('loginPage.loginButton') }}</span>
<span v-else>{{ t('loginPage.loginButton') }}</span>
</Button>
<div class="divider my-3">or</div>
<Button variant="outline" color="secondary" class="w-full" :disabled="loading" @click="handleGuestLogin">
Continue as Guest
</Button>
<button type="button" class="btn btn-outline w-full mt-2" @click="openSheet">
Email Magic Link
</button>
<AuthenticationSheet v-model="isSheetOpen" ref="sheet" />
<div class="text-center mt-2">
<router-link to="/auth/signup" class="link-styled">{{ t('loginPage.signupLink') }}</router-link>
</div>
<SocialLoginButtons />
</form>
</div>
</div>
</main>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter, useRoute } from 'vue-router';
import { useAuthStore } from '@/stores/auth';
import { useNotificationStore } from '@/stores/notifications';
import SocialLoginButtons from '@/components/SocialLoginButtons.vue';
import AuthenticationSheet from '@/components/AuthenticationSheet.vue';
import { Input, Button } from '@/components/ui';
const router = useRouter();
const route = useRoute();
const authStore = useAuthStore();
const notificationStore = useNotificationStore();
const { t, locale, messages } = useI18n();
const email = ref('');
const password = ref('');
const isPwdVisible = ref(false);
const loading = ref(false);
const isSheetOpen = ref(false);
const formErrors = ref<{ email?: string; password?: string; general?: string }>({});
const sheet = ref<InstanceType<typeof AuthenticationSheet>>()
function openSheet() {
isSheetOpen.value = true;
}
const isValidEmail = (val: string): boolean => {
const emailPattern = /^(?=[a-zA-Z0-9@._%+-]{6,254}$)[a-zA-Z0-9._%+-]{1,64}@(?:[a-zA-Z0-9-]{1,63}\.){1,8}[a-zA-Z]{2,63}$/;
return emailPattern.test(val);
};
const validateForm = (): boolean => {
formErrors.value = {};
if (!email.value.trim()) {
formErrors.value.email = t('loginPage.errors.emailRequired');
} else if (!isValidEmail(email.value)) {
formErrors.value.email = t('loginPage.errors.emailInvalid');
}
if (!password.value) {
formErrors.value.password = t('loginPage.errors.passwordRequired');
}
return Object.keys(formErrors.value).length === 0;
};
const onSubmit = async () => {
if (!validateForm()) {
return;
}
loading.value = true;
formErrors.value.general = undefined;
try {
await authStore.login(email.value, password.value);
notificationStore.addNotification({ message: t('loginPage.notifications.loginSuccess'), type: 'success' });
const redirectPath = (route.query.redirect as string) || '/';
router.push(redirectPath);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : t('loginPage.errors.loginFailed');
formErrors.value.general = message;
console.error(message, error);
notificationStore.addNotification({ message, type: 'error' });
} finally {
loading.value = false;
}
};
const handleGuestLogin = async () => {
loading.value = true;
formErrors.value.general = undefined;
try {
await authStore.loginAsGuest();
notificationStore.addNotification({ message: 'Welcome, Guest!', type: 'success' });
const redirectPath = (route.query.redirect as string) || '/';
router.push(redirectPath);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Failed to login as guest.';
formErrors.value.general = message;
console.error(message, error);
notificationStore.addNotification({ message, type: 'error' });
} finally {
loading.value = false;
}
};
</script>
<style scoped>
.page-container {
min-height: 100vh;
min-height: 100dvh;
padding: 1rem;
}
.login-card {
width: 100%;
max-width: 400px;
}
.divider {
display: flex;
align-items: center;
text-align: center;
color: #ccc;
font-size: 0.8em;
text-transform: uppercase;
}
.divider::before,
.divider::after {
content: '';
flex: 1;
border-bottom: 1px solid #eee;
}
.divider:not(:empty)::before {
margin-right: .5em;
}
.divider:not(:empty)::after {
margin-left: .5em;
}
.link-styled {
color: var(--primary);
text-decoration: none;
border-bottom: 2px solid transparent;
transition: border-color var(--transition-speed) var(--transition-ease-out);
}
.link-styled:hover,
.link-styled:focus {
border-bottom-color: var(--primary);
}
.form-error-text {
color: var(--danger);
font-size: 0.85rem;
margin-top: 0.25rem;
}
.alert.form-error-text {
padding: 0.75rem 1rem;
margin-bottom: 1rem;
}
.input-with-icon-append {
position: relative;
display: flex;
}
.input-with-icon-append .form-input {
padding-right: 3rem;
}
.icon-append-btn {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 3rem;
background: transparent;
border: none;
border-left: var(--border);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--dark);
opacity: 0.7;
}
.icon-append-btn:hover,
.icon-append-btn:focus {
opacity: 1;
background-color: rgba(0, 0, 0, 0.03);
}
.icon-append-btn .icon {
margin: 0;
}
</style>