
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.
291 lines
11 KiB
Vue
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> |