Okay, I've made some changes to integrate the Valerie UI components into the Account, Group Detail, and List Detail pages. This is part of the ongoing effort to standardize the UI and make the code easier to maintain.

Here's a breakdown of the changes:

1.  **`AccountPage.vue`**:
    *   I replaced the main heading with `VHeading`.
    *   I updated the loading spinner to `VSpinner`.
    *   I converted the error alert to `VAlert` with an action button.
    *   I refactored the Profile, Password, and Notifications sections to use `VCard` for their structure.
    *   The form elements within these cards (name, email, passwords) now use `VFormField` and `VInput`.
    *   Action buttons like "Save Changes" and "Change Password" are now `VButton` with an integrated `VSpinner` for loading states.
    *   The notification preferences list uses `VList` and `VListItem`, with each preference toggle converted to `VToggleSwitch`.

2.  **`GroupDetailPage.vue`**:
    *   I updated the page-level loading spinner, error alert, and main heading to `VSpinner`, `VAlert`, and `VHeading`.
    *   I refactored the "Group Members", "Invite Members", "Chores", and "Expenses" sections from custom "neo-card" styling to use `VCard`.
    *   Headers within these cards use `VHeading` and action buttons use `VButton` (I kept Material Icons where `VIcon` wasn't a direct replacement).
    *   Lists of members, chores, and expenses now use `VList` and `VListItem`.
    *   Buttons within list items (e.g., "Remove member") are `VButton` with `VSpinner`.
    *   Role indicators and frequency/split type "chips" are now `VBadge` components, and I updated the helper functions to return VBadge-compatible variants.
    *   The "Invite Members" form elements (input for code, copy button) use `VFormField`, `VInput`, and `VButton`.
    *   I simplified empty states within card bodies using `VIcon` and text.

3.  **`ListDetailPage.vue`**: This complex page required several steps to refactor:
    *   **Page-Level & Header:** I updated the loading state to `VSpinner`, the error alert to `VAlert`, and the main title to `VHeading`. Header action buttons are `VButton` with icons, and the list status is `VBadge`.
    *   **Modals:** I converted all five custom modals (OCR, Confirmation, Edit Item, Settle Share, Cost Summary shell) to use `VModal`. Internal forms and actions within these modals now use `VFormField`, `VInput`, `VButton`, `VSpinner`, `VList`, `VListItem`, and `VAlert` as appropriate. I removed the `onClickOutside` logic.
    *   **Main Items List:** The loading state uses `VCard` with `VSpinner`, and the empty state uses `VCard variant="empty-state"`. The list itself is now a `VCard` containing a `VList`. Each item is a `VListItem` with internal content refactored to use `VCheckbox`, `VInput` (for price), and `VButton` with `VIcon` for actions.
    *   **Add Item Form:** I re-structured this below the items list, using `VFormField`, `VInput`, and `VButton` with `VIcon`.
    *   **Expenses Section:** The main card uses `VCard` with `VHeading` and `VButton` in the header. Loading/error/empty states use `VSpinner`, `VAlert`, `VIcon`. The expenses list is `VList`, with each expense item as a `VListItem`. Statuses are `VBadge`.

This refactoring significantly increases the usage of the Valerie UI component library across these key application pages. This should help create a more consistent experience for you and make development smoother. Next, I'll focus on the Chores-related pages.
This commit is contained in:
google-labs-jules[bot] 2025-06-01 09:47:23 +00:00 committed by mohamad
parent 272e5abe41
commit 813ed911f1
3 changed files with 515 additions and 685 deletions

View File

@ -1,116 +1,84 @@
<template>
<main class="container page-padding">
<h1 class="mb-3">Account Settings</h1>
<VHeading level="1" text="Account Settings" class="mb-3" />
<div v-if="loading" class="text-center">
<div class="spinner-dots" role="status"><span /><span /><span /></div>
<p>Loading profile...</p>
<VSpinner label="Loading profile..." />
</div>
<div v-else-if="error" class="alert alert-error mb-3" role="alert">
<div class="alert-content">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-alert-triangle" />
</svg>
{{ error }}
</div>
<button type="button" class="btn btn-sm btn-danger" @click="fetchProfile">Retry</button>
</div>
<VAlert v-else-if="error" type="error" :message="error" class="mb-3">
<template #actions>
<VButton variant="danger" size="sm" @click="fetchProfile">Retry</VButton>
</template>
</VAlert>
<form v-else @submit.prevent="onSubmitProfile">
<!-- Profile Section -->
<div class="card mb-3">
<div class="card-header">
<h3>Profile Information</h3>
<VCard class="mb-3">
<template #header><VHeading level="3">Profile Information</VHeading></template>
<div class="flex flex-wrap" style="gap: 1rem;">
<VFormField label="Name" class="flex-grow">
<VInput id="profileName" v-model="profile.name" required />
</VFormField>
<VFormField label="Email" class="flex-grow">
<VInput type="email" id="profileEmail" v-model="profile.email" required readonly />
</VFormField>
</div>
<div class="card-body">
<div class="flex flex-wrap" style="gap: 1rem;">
<div class="form-group flex-grow">
<label for="profileName" class="form-label">Name</label>
<input type="text" id="profileName" v-model="profile.name" class="form-input" required />
</div>
<div class="form-group flex-grow">
<label for="profileEmail" class="form-label">Email</label>
<input type="email" id="profileEmail" v-model="profile.email" class="form-input" required readonly />
</div>
</div>
</div>
<div class="card-footer">
<button type="submit" class="btn btn-primary" :disabled="saving">
<span v-if="saving" class="spinner-dots-sm" role="status"><span /><span /><span /></span>
Save Changes
</button>
</div>
</div>
<template #footer>
<VButton type="submit" variant="primary" :disabled="saving">
<VSpinner v-if="saving" size="sm" /> Save Changes
</VButton>
</template>
</VCard>
</form>
<!-- Password Section -->
<form @submit.prevent="onChangePassword">
<div class="card mb-3">
<div class="card-header">
<h3>Change Password</h3>
<VCard class="mb-3">
<template #header><VHeading level="3">Change Password</VHeading></template>
<div class="flex flex-wrap" style="gap: 1rem;">
<VFormField label="Current Password" class="flex-grow">
<VInput type="password" id="currentPassword" v-model="password.current" required />
</VFormField>
<VFormField label="New Password" class="flex-grow">
<VInput type="password" id="newPassword" v-model="password.newPassword" required />
</VFormField>
</div>
<div class="card-body">
<div class="flex flex-wrap" style="gap: 1rem;">
<div class="form-group flex-grow">
<label for="currentPassword" class="form-label">Current Password</label>
<input type="password" id="currentPassword" v-model="password.current" class="form-input" required />
</div>
<div class="form-group flex-grow">
<label for="newPassword" class="form-label">New Password</label>
<input type="password" id="newPassword" v-model="password.newPassword" class="form-input" required />
</div>
</div>
</div>
<div class="card-footer">
<button type="submit" class="btn btn-primary" :disabled="changingPassword">
<span v-if="changingPassword" class="spinner-dots-sm" role="status"><span /><span /><span /></span>
Change Password
</button>
</div>
</div>
<template #footer>
<VButton type="submit" variant="primary" :disabled="changingPassword">
<VSpinner v-if="changingPassword" size="sm" /> Change Password
</VButton>
</template>
</VCard>
</form>
<!-- Notifications Section -->
<div class="card">
<div class="card-header">
<h3>Notification Preferences</h3>
</div>
<div class="card-body">
<ul class="item-list preference-list">
<li class="preference-item">
<div class="preference-label">
<span>Email Notifications</span>
<small>Receive email notifications for important updates</small>
</div>
<label class="switch-container">
<input type="checkbox" v-model="preferences.emailNotifications" @change="onPreferenceChange" />
<span class="switch" aria-hidden="true"></span>
</label>
</li>
<li class="preference-item">
<div class="preference-label">
<span>List Updates</span>
<small>Get notified when lists are updated</small>
</div>
<label class="switch-container">
<input type="checkbox" v-model="preferences.listUpdates" @change="onPreferenceChange" />
<span class="switch" aria-hidden="true"></span>
</label>
</li>
<li class="preference-item">
<div class="preference-label">
<span>Group Activities</span>
<small>Receive notifications for group activities</small>
</div>
<label class="switch-container">
<input type="checkbox" v-model="preferences.groupActivities" @change="onPreferenceChange" />
<span class="switch" aria-hidden="true"></span>
</label>
</li>
</ul>
</div>
</div>
<VCard>
<template #header><VHeading level="3">Notification Preferences</VHeading></template>
<VList class="preference-list">
<VListItem class="preference-item">
<div class="preference-label">
<span>Email Notifications</span>
<small>Receive email notifications for important updates</small>
</div>
<VToggleSwitch v-model="preferences.emailNotifications" @change="onPreferenceChange" label="Email Notifications" id="emailNotificationsToggle" />
</VListItem>
<VListItem class="preference-item">
<div class="preference-label">
<span>List Updates</span>
<small>Get notified when lists are updated</small>
</div>
<VToggleSwitch v-model="preferences.listUpdates" @change="onPreferenceChange" label="List Updates" id="listUpdatesToggle"/>
</VListItem>
<VListItem class="preference-item">
<div class="preference-label">
<span>Group Activities</span>
<small>Receive notifications for group activities</small>
</div>
<VToggleSwitch v-model="preferences.groupActivities" @change="onPreferenceChange" label="Group Activities" id="groupActivitiesToggle"/>
</VListItem>
</VList>
</VCard>
</main>
</template>
@ -118,6 +86,16 @@
import { ref, onMounted } from 'vue';
import { apiClient, API_ENDPOINTS } from '@/config/api'; // Assuming path
import { useNotificationStore } from '@/stores/notifications';
import VHeading from '@/components/valerie/VHeading.vue';
import VSpinner from '@/components/valerie/VSpinner.vue';
import VAlert from '@/components/valerie/VAlert.vue';
import VCard from '@/components/valerie/VCard.vue';
import VFormField from '@/components/valerie/VFormField.vue';
import VInput from '@/components/valerie/VInput.vue';
import VButton from '@/components/valerie/VButton.vue';
import VToggleSwitch from '@/components/valerie/VToggleSwitch.vue';
import VList from '@/components/valerie/VList.vue';
import VListItem from '@/components/valerie/VListItem.vue';
interface Profile {
name: string;

View File

@ -1,82 +1,57 @@
<template>
<main class="container page-padding">
<div v-if="loading" class="text-center">
<div class="spinner-dots" role="status"><span /><span /><span /></div>
<p>Loading group details...</p>
</div>
<div v-else-if="error" class="alert alert-error mb-3" role="alert">
<div class="alert-content">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-alert-triangle" />
</svg>
{{ error }}
</div>
<button type="button" class="btn btn-sm btn-danger" @click="fetchGroupDetails">Retry</button>
<VSpinner label="Loading group details..." />
</div>
<VAlert v-else-if="error" type="error" :message="error" class="mb-3">
<template #actions>
<VButton variant="danger" size="sm" @click="fetchGroupDetails">Retry</VButton>
</template>
</VAlert>
<div v-else-if="group">
<h1 class="mb-3">{{ group.name }}</h1>
<VHeading level="1" :text="group.name" class="mb-3" />
<div class="neo-grid">
<!-- Group Members Section -->
<div class="neo-card">
<div class="neo-card-header">
<h3>Group Members</h3>
</div>
<div class="neo-card-body">
<div v-if="group.members && group.members.length > 0" class="neo-members-list">
<div v-for="member in group.members" :key="member.id" class="neo-member-item">
<div class="neo-member-info">
<span class="neo-member-name">{{ member.email }}</span>
<span class="neo-member-role" :class="member.role?.toLowerCase()">{{ member.role || 'Member' }}</span>
</div>
<button v-if="canRemoveMember(member)" class="btn btn-danger btn-sm" @click="removeMember(member.id)"
:disabled="removingMember === member.id">
<span v-if="removingMember === member.id" class="spinner-dots-sm"
role="status"><span /><span /><span /></span>
Remove
</button>
<VCard>
<template #header><VHeading level="3">Group Members</VHeading></template>
<VList v-if="group.members && group.members.length > 0">
<VListItem v-for="member in group.members" :key="member.id" class="flex justify-between items-center">
<div class="neo-member-info">
<span class="neo-member-name">{{ member.email }}</span>
<VBadge :text="member.role || 'Member'" :variant="member.role?.toLowerCase() === 'owner' ? 'primary' : 'secondary'" />
</div>
</div>
<div v-else class="neo-empty-state">
<svg class="icon icon-lg" aria-hidden="true">
<use xlink:href="#icon-users" />
</svg>
<p>No members found.</p>
</div>
<VButton v-if="canRemoveMember(member)" variant="danger" size="sm" @click="removeMember(member.id)" :disabled="removingMember === member.id">
<VSpinner v-if="removingMember === member.id" size="sm"/> Remove
</VButton>
</VListItem>
</VList>
<div v-else class="text-center py-4">
<VIcon name="users" size="lg" class="opacity-50 mb-2" />
<p>No members found.</p>
</div>
</div>
</VCard>
<!-- Invite Members Section -->
<div class="neo-card">
<div class="neo-card-header">
<h3>Invite Members</h3>
</div>
<div class="neo-card-body">
<button class="btn btn-primary w-full" @click="generateInviteCode" :disabled="generatingInvite">
<span v-if="generatingInvite" class="spinner-dots-sm" role="status"><span /><span /><span /></span>
{{ inviteCode ? 'Regenerate Invite Code' : 'Generate Invite Code' }}
</button>
<div v-if="inviteCode" class="neo-invite-code mt-3">
<label for="inviteCodeInput" class="neo-label">Current Active Invite Code:</label>
<div class="neo-input-group">
<input id="inviteCodeInput" type="text" :value="inviteCode" class="neo-input" readonly />
<button class="btn btn-neutral btn-icon-only" @click="copyInviteCodeHandler"
aria-label="Copy invite code">
<svg class="icon">
<use xlink:href="#icon-clipboard"></use>
</svg>
</button>
<VCard>
<template #header><VHeading level="3">Invite Members</VHeading></template>
<VButton variant="primary" class="w-full" @click="generateInviteCode" :disabled="generatingInvite">
<VSpinner v-if="generatingInvite" size="sm"/> {{ inviteCode ? 'Regenerate Invite Code' : 'Generate Invite Code' }}
</VButton>
<div v-if="inviteCode" class="neo-invite-code mt-3">
<VFormField label="Current Active Invite Code:" :label-sr-only="false">
<div class="flex items-center gap-2">
<VInput id="inviteCodeInput" :model-value="inviteCode" readonly class="flex-grow" />
<VButton variant="neutral" :icon-only="true" iconLeft="clipboard" @click="copyInviteCodeHandler" aria-label="Copy invite code" />
</div>
<p v-if="copySuccess" class="neo-success-text">Invite code copied to clipboard!</p>
</div>
<div v-else class="neo-empty-state mt-3">
<svg class="icon icon-lg" aria-hidden="true">
<use xlink:href="#icon-link" />
</svg>
<p>No active invite code. Click the button above to generate one.</p>
</div>
</VFormField>
<p v-if="copySuccess" class="text-sm text-green-600 mt-1">Invite code copied to clipboard!</p>
</div>
</div>
<div v-else class="text-center py-4 mt-3">
<VIcon name="link" size="lg" class="opacity-50 mb-2" />
<p>No active invite code. Click the button above to generate one.</p>
</div>
</VCard>
</div>
<!-- Lists Section -->
@ -85,78 +60,61 @@
</div>
<!-- Chores Section -->
<div class="mt-4">
<div class="neo-card">
<div class="neo-card-header">
<h3>Group Chores</h3>
<router-link :to="`/groups/${groupId}/chores`" class="btn btn-primary">
<span class="material-icons">cleaning_services</span>
Manage Chores
</router-link>
<VCard class="mt-4">
<template #header>
<div class="flex justify-between items-center w-full">
<VHeading level="3">Group Chores</VHeading>
<VButton :to="`/groups/${groupId}/chores`" variant="primary">
<span class="material-icons" style="margin-right: 0.25em;">cleaning_services</span> Manage Chores
</VButton>
</div>
<div class="neo-card-body">
<div v-if="upcomingChores.length > 0" class="neo-chores-list">
<div v-for="chore in upcomingChores" :key="chore.id" class="neo-chore-item">
<div class="neo-chore-info">
<span class="neo-chore-name">{{ chore.name }}</span>
<span class="neo-chore-due">Due: {{ formatDate(chore.next_due_date) }}</span>
</div>
<span class="neo-chip" :class="getFrequencyColor(chore.frequency)">
{{ formatFrequency(chore.frequency) }}
</span>
</div>
</template>
<VList v-if="upcomingChores.length > 0">
<VListItem v-for="chore in upcomingChores" :key="chore.id" class="flex justify-between items-center">
<div class="neo-chore-info">
<span class="neo-chore-name">{{ chore.name }}</span>
<span class="neo-chore-due">Due: {{ formatDate(chore.next_due_date) }}</span>
</div>
<div v-else class="neo-empty-state">
<svg class="icon icon-lg" aria-hidden="true">
<use xlink:href="#icon-cleaning_services" />
</svg>
<p>No chores scheduled. Click "Manage Chores" to create some!</p>
</div>
</div>
<VBadge :text="formatFrequency(chore.frequency)" :variant="getFrequencyBadgeVariant(chore.frequency)" />
</VListItem>
</VList>
<div v-else class="text-center py-4">
<VIcon name="cleaning_services" size="lg" class="opacity-50 mb-2" /> {/* Assuming cleaning_services is a valid VIcon name or will be added */}
<p>No chores scheduled. Click "Manage Chores" to create some!</p>
</div>
</div>
</VCard>
<!-- Expenses Section -->
<div class="mt-4">
<div class="neo-card">
<div class="neo-card-header">
<h3>Group Expenses</h3>
<router-link :to="`/groups/${groupId}/expenses`" class="btn btn-primary">
<span class="material-icons">payments</span>
Manage Expenses
</router-link>
<VCard class="mt-4">
<template #header>
<div class="flex justify-between items-center w-full">
<VHeading level="3">Group Expenses</VHeading>
<VButton :to="`/groups/${groupId}/expenses`" variant="primary">
<span class="material-icons" style="margin-right: 0.25em;">payments</span> Manage Expenses
</VButton>
</div>
<div class="neo-card-body">
<div v-if="recentExpenses.length > 0" class="neo-expenses-list">
<div v-for="expense in recentExpenses" :key="expense.id" class="neo-expense-item">
<div class="neo-expense-info">
<span class="neo-expense-name">{{ expense.description }}</span>
<span class="neo-expense-date">{{ formatDate(expense.expense_date) }}</span>
</div>
<div class="neo-expense-details">
<span class="neo-expense-amount">{{ expense.currency }} {{ formatAmount(expense.total_amount)
}}</span>
<span class="neo-chip" :class="getSplitTypeColor(expense.split_type)">
{{ formatSplitType(expense.split_type) }}
</span>
</div>
</div>
</template>
<VList v-if="recentExpenses.length > 0">
<VListItem v-for="expense in recentExpenses" :key="expense.id" class="flex justify-between items-center">
<div class="neo-expense-info">
<span class="neo-expense-name">{{ expense.description }}</span>
<span class="neo-expense-date">{{ formatDate(expense.expense_date) }}</span>
</div>
<div v-else class="neo-empty-state">
<svg class="icon icon-lg" aria-hidden="true">
<use xlink:href="#icon-payments" />
</svg>
<p>No expenses recorded. Click "Manage Expenses" to add some!</p>
<div class="neo-expense-details">
<span class="neo-expense-amount">{{ expense.currency }} {{ formatAmount(expense.total_amount) }}</span>
<VBadge :text="formatSplitType(expense.split_type)" :variant="getSplitTypeBadgeVariant(expense.split_type)" />
</div>
</div>
</VListItem>
</VList>
<div v-else class="text-center py-4">
<VIcon name="payments" size="lg" class="opacity-50 mb-2" /> {/* Assuming payments is a valid VIcon name or will be added */}
<p>No expenses recorded. Click "Manage Expenses" to add some!</p>
</div>
</div>
</VCard>
</div>
<div v-else class="alert alert-info" role="status">
<div class="alert-content">Group not found or an error occurred.</div>
</div>
<VAlert v-else type="info" message="Group not found or an error occurred." />
</main>
</template>
@ -171,6 +129,17 @@ import { choreService } from '../services/choreService'
import type { Chore, ChoreFrequency } from '../types/chore'
import { format } from 'date-fns'
import type { Expense } from '@/types/expense'
import VHeading from '@/components/valerie/VHeading.vue';
import VSpinner from '@/components/valerie/VSpinner.vue';
import VAlert from '@/components/valerie/VAlert.vue';
import VCard from '@/components/valerie/VCard.vue';
import VList from '@/components/valerie/VList.vue';
import VListItem from '@/components/valerie/VListItem.vue';
import VButton from '@/components/valerie/VButton.vue';
import VBadge from '@/components/valerie/VBadge.vue';
import VInput from '@/components/valerie/VInput.vue';
import VFormField from '@/components/valerie/VFormField.vue';
import VIcon from '@/components/valerie/VIcon.vue';
interface Group {
id: string | number;
@ -355,16 +324,16 @@ const formatFrequency = (frequency: ChoreFrequency) => {
return options[frequency] || frequency
}
const getFrequencyColor = (frequency: ChoreFrequency) => {
const colors: Record<ChoreFrequency, string> = {
one_time: 'grey',
daily: 'blue',
weekly: 'green',
monthly: 'purple',
custom: 'orange'
}
return colors[frequency]
}
const getFrequencyBadgeVariant = (frequency: ChoreFrequency): string => {
const colorMap: Record<ChoreFrequency, string> = {
one_time: 'neutral',
daily: 'info',
weekly: 'success',
monthly: 'accent', // Using accent for purple as an example
custom: 'warning'
};
return colorMap[frequency] || 'secondary';
};
// Add new methods for expenses
const loadRecentExpenses = async () => {
@ -387,16 +356,16 @@ const formatSplitType = (type: string) => {
).join(' ')
}
const getSplitTypeColor = (type: string) => {
const colors: Record<string, string> = {
equal: 'blue',
exact_amounts: 'green',
percentage: 'purple',
shares: 'orange',
item_based: 'teal'
}
return colors[type] || 'grey'
}
const getSplitTypeBadgeVariant = (type: string): string => {
const colorMap: Record<string, string> = {
equal: 'info',
exact_amounts: 'success',
percentage: 'accent', // Using accent for purple
shares: 'warning',
item_based: 'secondary', // Using secondary for teal as an example
};
return colorMap[type] || 'neutral';
};
onMounted(() => {
fetchGroupDetails();

File diff suppressed because it is too large Load Diff