
This commit adds new functionality for tracking user activities within the application, including: - Implementation of a new activity service to fetch and manage group activities. - Creation of a dedicated activity store to handle state management for activities. - Introduction of new API endpoints for retrieving paginated activity data. - Enhancements to the UI with new components for displaying activity feeds and items. - Refactoring of existing components to utilize the new activity features, improving user engagement and interaction. These changes aim to enhance the application's activity tracking capabilities and provide users with a comprehensive view of their interactions.
202 lines
6.5 KiB
Vue
202 lines
6.5 KiB
Vue
<template>
|
|
<main class="flex items-center justify-center page-container">
|
|
<div class="card signup-card">
|
|
<div class="card-header">
|
|
<h3>{{ $t('signupPage.header') }}</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<form @submit.prevent="onSubmit" class="form-layout">
|
|
<div class="form-group mb-2 relative">
|
|
<Input v-model="name" :label="$t('signupPage.fullNameLabel')" id="name" :error="formErrors.name"
|
|
autocomplete="name" />
|
|
</div>
|
|
|
|
<div class="form-group mb-2">
|
|
<Input v-model="email" :label="$t('signupPage.emailLabel')" type="email" id="email"
|
|
:error="formErrors.email" autocomplete="email" />
|
|
</div>
|
|
|
|
<div class="form-group mb-2 relative">
|
|
<Input v-model="password" :label="$t('signupPage.passwordLabel')" :type="isPwdVisible ? 'text' : 'password'"
|
|
id="password" :error="formErrors.password" autocomplete="new-password" />
|
|
<button type="button" class="absolute right-3 top-[34px] text-sm text-gray-500"
|
|
@click="isPwdVisible = !isPwdVisible" :aria-label="$t('signupPage.togglePasswordVisibility')">
|
|
{{ isPwdVisible ? 'Hide' : 'Show' }}
|
|
</button>
|
|
</div>
|
|
|
|
<div class="form-group mb-3">
|
|
<Input v-model="confirmPassword" :label="$t('signupPage.confirmPasswordLabel')"
|
|
:type="isPwdVisible ? 'text' : 'password'" id="confirmPassword" :error="formErrors.confirmPassword"
|
|
autocomplete="new-password" />
|
|
</div>
|
|
|
|
<p v-if="formErrors.general" class="alert alert-error form-error-text">{{ formErrors.general }}</p>
|
|
|
|
<Button type="submit" class="w-full mt-2" :disabled="loading">
|
|
<span v-if="loading" class="animate-pulse">{{ $t('signupPage.submitButton') }}</span>
|
|
<span v-else>{{ $t('signupPage.submitButton') }}</span>
|
|
</Button>
|
|
|
|
<div class="text-center mt-2">
|
|
<router-link to="/auth/login" class="link-styled">{{ $t('signupPage.loginLink') }}</router-link>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useAuthStore } from '@/stores/auth'; // Assuming path is correct
|
|
import { useNotificationStore } from '@/stores/notifications';
|
|
import { Input, Button } from '@/components/ui';
|
|
|
|
const { t } = useI18n();
|
|
const router = useRouter();
|
|
const authStore = useAuthStore();
|
|
const notificationStore = useNotificationStore();
|
|
|
|
const name = ref('');
|
|
const email = ref('');
|
|
const password = ref('');
|
|
const confirmPassword = ref('');
|
|
const isPwdVisible = ref(false);
|
|
const loading = ref(false);
|
|
const formErrors = ref<{ name?: string; email?: string; password?: string; confirmPassword?: string; general?: string }>({});
|
|
|
|
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 (!name.value.trim()) {
|
|
formErrors.value.name = t('signupPage.validation.nameRequired');
|
|
}
|
|
if (!email.value.trim()) {
|
|
formErrors.value.email = t('signupPage.validation.emailRequired');
|
|
} else if (!isValidEmail(email.value)) {
|
|
formErrors.value.email = t('signupPage.validation.emailInvalid');
|
|
}
|
|
if (!password.value) {
|
|
formErrors.value.password = t('signupPage.validation.passwordRequired');
|
|
} else if (password.value.length < 8) {
|
|
formErrors.value.password = t('signupPage.validation.passwordLength');
|
|
}
|
|
if (!confirmPassword.value) {
|
|
formErrors.value.confirmPassword = t('signupPage.validation.confirmPasswordRequired');
|
|
} else if (password.value !== confirmPassword.value) {
|
|
formErrors.value.confirmPassword = t('signupPage.validation.passwordsNoMatch');
|
|
}
|
|
return Object.keys(formErrors.value).length === 0;
|
|
};
|
|
|
|
const onSubmit = async () => {
|
|
if (!validateForm()) {
|
|
return;
|
|
}
|
|
loading.value = true;
|
|
formErrors.value.general = undefined;
|
|
try {
|
|
await authStore.signup({
|
|
name: name.value,
|
|
email: email.value,
|
|
password: password.value,
|
|
});
|
|
notificationStore.addNotification({ message: t('signupPage.notifications.signupSuccess'), type: 'success' });
|
|
router.push('auth/login');
|
|
} catch (error: unknown) {
|
|
// Prefer API error message if available, otherwise use generic translated message for the form
|
|
const errorMessageForForm = error instanceof Error ? error.message : t('signupPage.notifications.signupFailed');
|
|
formErrors.value.general = errorMessageForForm;
|
|
|
|
// For the notification pop-up, always use the generic translated message if API message is not specific enough or not an Error
|
|
const notificationMessage = error instanceof Error && error.message ? error.message : t('signupPage.notifications.signupFailed');
|
|
console.error("Signup error:", error); // Keep detailed log for developers
|
|
notificationStore.addNotification({ message: notificationMessage, type: 'error' });
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Using styles from LoginPage.vue where applicable */
|
|
.page-container {
|
|
min-height: 100vh;
|
|
min-height: 100dvh;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.signup-card {
|
|
width: 100%;
|
|
max-width: 400px;
|
|
}
|
|
|
|
/* form-layout, w-full, mt-2, text-center styles were removed as utility classes from Valerie UI are used directly or are available. */
|
|
|
|
.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 {
|
|
/* For general error message */
|
|
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> |