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

View File

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

File diff suppressed because it is too large Load Diff