mitlist/fe/src/pages/AccountPage.vue
mohamad 229f6b7b1c feat: Introduce activity tracking and management features
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.
2025-06-28 19:14:51 +02:00

291 lines
11 KiB
Vue

<template>
<main class="container page-padding">
<Heading :level="1" class="mb-3">{{ $t('accountPage.title') }}</Heading>
<div v-if="loading" class="text-center">
<Spinner :label="$t('accountPage.loadingProfile')" />
</div>
<Alert v-else-if="error" color="danger" class="mb-3">
{{ error }}
<template #actions>
<Button color="danger" size="sm" @click="fetchProfile">{{ $t('accountPage.retryButton') }}</Button>
</template>
</Alert>
<form v-else @submit.prevent="onSubmitProfile">
<!-- Profile Section -->
<Card class="mb-3">
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
<Heading :level="3">{{ $t('accountPage.profileSection.header') }}</Heading>
</div>
<div class="px-4 py-5 sm:p-6">
<div class="flex flex-wrap" style="gap: 1rem">
<div class="flex-grow">
<label for="profileName" class="block text-sm font-medium text-gray-700">{{
$t('accountPage.profileSection.nameLabel')
}}</label>
<Input id="profileName" v-model="profile.name" required class="mt-1" />
</div>
<div class="flex-grow">
<label for="profileEmail" class="block text-sm font-medium text-gray-700">{{
$t('accountPage.profileSection.emailLabel')
}}</label>
<Input type="email" id="profileEmail" v-model="profile.email" required readonly class="mt-1" />
</div>
</div>
</div>
<div class="px-4 py-3 bg-gray-50 sm:px-6">
<Button type="submit" color="primary" :disabled="saving">
<Spinner v-if="saving" size="sm" class="mr-1" />
{{ $t('accountPage.profileSection.saveButton') }}
</Button>
</div>
</Card>
</form>
<!-- Password Section -->
<form @submit.prevent="onChangePassword">
<Card class="mb-3">
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
<Heading :level="3">{{ $t('accountPage.passwordSection.header') }}</Heading>
</div>
<div class="px-4 py-5 sm:p-6">
<div class="flex flex-wrap" style="gap: 1rem">
<div class="flex-grow">
<label for="currentPassword" class="block text-sm font-medium text-gray-700">{{
$t('accountPage.passwordSection.currentPasswordLabel')
}}</label>
<Input type="password" id="currentPassword" v-model="password.current" required class="mt-1" />
</div>
<div class="flex-grow">
<label for="newPassword" class="block text-sm font-medium text-gray-700">{{
$t('accountPage.passwordSection.newPasswordLabel')
}}</label>
<Input type="password" id="newPassword" v-model="password.newPassword" required class="mt-1" />
</div>
</div>
</div>
<div class="px-4 py-3 bg-gray-50 sm:px-6">
<Button type="submit" color="primary" :disabled="changingPassword">
<Spinner v-if="changingPassword" size="sm" class="mr-1" />
{{ $t('accountPage.passwordSection.changeButton') }}
</Button>
</div>
</Card>
</form>
<!-- Notifications Section -->
<Card>
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
<Heading :level="3">{{ $t('accountPage.notificationsSection.header') }}</Heading>
</div>
<ul class="divide-y divide-gray-200">
<li class="flex items-center justify-between p-4">
<div class="flex flex-col">
<span class="font-medium text-gray-900">{{
$t('accountPage.notificationsSection.emailNotificationsLabel')
}}</span>
<small class="text-sm text-gray-500">{{
$t('accountPage.notificationsSection.emailNotificationsDescription')
}}</small>
</div>
<Switch v-model="preferences.emailNotifications" @change="onPreferenceChange"
:label="$t('accountPage.notificationsSection.emailNotificationsLabel')" id="emailNotificationsToggle" />
</li>
<li class="flex items-center justify-between p-4">
<div class="flex flex-col">
<span class="font-medium text-gray-900">{{ $t('accountPage.notificationsSection.listUpdatesLabel') }}</span>
<small class="text-sm text-gray-500">{{
$t('accountPage.notificationsSection.listUpdatesDescription')
}}</small>
</div>
<Switch v-model="preferences.listUpdates" @change="onPreferenceChange"
:label="$t('accountPage.notificationsSection.listUpdatesLabel')" id="listUpdatesToggle" />
</li>
<li class="flex items-center justify-between p-4">
<div class="flex flex-col">
<span class="font-medium text-gray-900">{{
$t('accountPage.notificationsSection.groupActivitiesLabel')
}}</span>
<small class="text-sm text-gray-500">{{
$t('accountPage.notificationsSection.groupActivitiesDescription')
}}</small>
</div>
<Switch v-model="preferences.groupActivities" @change="onPreferenceChange"
:label="$t('accountPage.notificationsSection.groupActivitiesLabel')" id="groupActivitiesToggle" />
</li>
</ul>
</Card>
</main>
</template>
<script setup lang="ts">
import { ref, onMounted, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useAuthStore } from '@/stores/auth';
import { apiClient, API_ENDPOINTS } from '@/services/api'; // Assuming path
import { useNotificationStore } from '@/stores/notifications';
import { Heading, Spinner, Alert, Card, Input, Button, Switch } from '@/components/ui';
const { t } = useI18n();
interface Profile {
name: string;
email: string;
}
interface PasswordForm {
current: string;
newPassword: string; // Renamed from 'new' to avoid conflict
}
interface Preferences {
emailNotifications: boolean;
listUpdates: boolean;
groupActivities: boolean;
}
const loading = ref(true);
const saving = ref(false);
const changingPassword = ref(false);
const error = ref<string | null>(null);
const notificationStore = useNotificationStore();
const profile = ref<Profile>({ name: '', email: '' });
const password = ref<PasswordForm>({ current: '', newPassword: '' });
const preferences = ref<Preferences>({
emailNotifications: true,
listUpdates: true,
groupActivities: true,
});
const authStore = useAuthStore();
const fetchProfile = async () => {
loading.value = true;
error.value = null;
try {
const response = await apiClient.get(API_ENDPOINTS.USERS.PROFILE);
profile.value = response.data;
// Assume preferences are also fetched or part of profile
// preferences.value = response.data.preferences || preferences.value;
} catch (err: unknown) {
const apiMessage = err instanceof Error ? err.message : t('accountPage.notifications.profileLoadFailed');
error.value = apiMessage; // Show translated or API error message in the VAlert
console.error('Failed to fetch profile:', err);
// For the notification pop-up, always use the translated generic message
notificationStore.addNotification({ message: t('accountPage.notifications.profileLoadFailed'), type: 'error' });
} finally {
loading.value = false;
}
};
const onSubmitProfile = async () => {
saving.value = true;
try {
await apiClient.put(API_ENDPOINTS.USERS.UPDATE_PROFILE, profile.value);
notificationStore.addNotification({ message: t('accountPage.notifications.profileUpdateSuccess'), type: 'success' });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : t('accountPage.notifications.profileUpdateFailed');
console.error('Failed to update profile:', err);
notificationStore.addNotification({ message: t('accountPage.notifications.profileUpdateFailed'), type: 'error' });
} finally {
saving.value = false;
}
};
const onChangePassword = async () => {
if (!password.value.current || !password.value.newPassword) {
notificationStore.addNotification({ message: t('accountPage.notifications.passwordFieldsRequired'), type: 'warning' });
return;
}
if (password.value.newPassword.length < 8) {
notificationStore.addNotification({ message: t('accountPage.notifications.passwordTooShort'), type: 'warning' });
return;
}
changingPassword.value = true;
try {
// API endpoint expects 'new' not 'newPassword'
await apiClient.put(API_ENDPOINTS.USERS.PASSWORD, {
current: password.value.current,
new: password.value.newPassword
});
password.value = { current: '', newPassword: '' };
notificationStore.addNotification({ message: t('accountPage.notifications.passwordChangeSuccess'), type: 'success' });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : t('accountPage.notifications.passwordChangeFailed');
console.error('Failed to change password:', err);
notificationStore.addNotification({ message: t('accountPage.notifications.passwordChangeFailed'), type: 'error' });
} finally {
changingPassword.value = false;
}
};
const onPreferenceChange = async () => {
// This will be called for each toggle change.
// Consider debouncing or providing a "Save Preferences" button if API calls are expensive.
try {
await apiClient.put(API_ENDPOINTS.USERS.PREFERENCES, preferences.value);
notificationStore.addNotification({ message: t('accountPage.notifications.preferencesUpdateSuccess'), type: 'success' });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : t('accountPage.notifications.preferencesUpdateFailed');
console.error('Failed to update preferences:', err);
notificationStore.addNotification({ message: t('accountPage.notifications.preferencesUpdateFailed'), type: 'error' });
// Optionally revert the toggle if the API call fails
// await fetchProfile(); // Or manage state more granularly
}
};
onMounted(() => {
fetchProfile();
});
</script>
<style scoped>
.page-padding {
padding: 1rem;
/* Or use var(--padding-page) if defined in Valerie UI */
}
.mb-3 {
margin-bottom: 1.5rem;
}
/* From Valerie UI */
.flex-grow {
flex-grow: 1;
}
.preference-list {
list-style: none;
padding: 0;
margin: 0;
}
.preference-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
border-bottom: 1px solid #eee;
/* Softer border for list items */
}
.preference-item:last-child {
border-bottom: none;
}
.preference-label {
display: flex;
flex-direction: column;
margin-right: 1rem;
}
.preference-label small {
font-size: 0.85rem;
opacity: 0.7;
margin-top: 0.2rem;
}
</style>