diff --git a/fe/src/pages/AccountPage.vue b/fe/src/pages/AccountPage.vue index 2d2a9f3..dff3ff0 100644 --- a/fe/src/pages/AccountPage.vue +++ b/fe/src/pages/AccountPage.vue @@ -1,116 +1,84 @@ @@ -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; diff --git a/fe/src/pages/GroupDetailPage.vue b/fe/src/pages/GroupDetailPage.vue index 934bdb2..1c9ca17 100644 --- a/fe/src/pages/GroupDetailPage.vue +++ b/fe/src/pages/GroupDetailPage.vue @@ -1,82 +1,57 @@ @@ -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 = { - one_time: 'grey', - daily: 'blue', - weekly: 'green', - monthly: 'purple', - custom: 'orange' - } - return colors[frequency] -} +const getFrequencyBadgeVariant = (frequency: ChoreFrequency): string => { + const colorMap: Record = { + 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 = { - equal: 'blue', - exact_amounts: 'green', - percentage: 'purple', - shares: 'orange', - item_based: 'teal' - } - return colors[type] || 'grey' -} +const getSplitTypeBadgeVariant = (type: string): string => { + const colorMap: Record = { + 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(); diff --git a/fe/src/pages/ListDetailPage.vue b/fe/src/pages/ListDetailPage.vue index f50b1da..44c863d 100644 --- a/fe/src/pages/ListDetailPage.vue +++ b/fe/src/pages/ListDetailPage.vue @@ -1,106 +1,99 @@ @@ -365,7 +286,7 @@ import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue'; import { useRoute } from 'vue-router'; import { apiClient, API_ENDPOINTS } from '@/config/api'; // Keep for item management -import { onClickOutside, useEventListener, useFileDialog, useNetwork } from '@vueuse/core'; +import { useEventListener, useFileDialog, useNetwork } from '@vueuse/core'; // onClickOutside removed import { useNotificationStore } from '@/stores/notifications'; import { useOfflineStore, type CreateListItemPayload } from '@/stores/offline'; import { useListDetailStore } from '@/stores/listDetailStore'; @@ -378,6 +299,19 @@ import type { SettlementActivityCreate } from '@/types/expense'; import SettleShareModal from '@/components/SettleShareModal.vue'; import CreateExpenseForm from '@/components/CreateExpenseForm.vue'; import type { Item } from '@/types/item'; +import VHeading from '@/components/valerie/VHeading.vue'; +import VSpinner from '@/components/valerie/VSpinner.vue'; +import VAlert from '@/components/valerie/VAlert.vue'; +import VButton from '@/components/valerie/VButton.vue'; +import VBadge from '@/components/valerie/VBadge.vue'; +import VIcon from '@/components/valerie/VIcon.vue'; +import VModal from '@/components/valerie/VModal.vue'; +import VFormField from '@/components/valerie/VFormField.vue'; +import VInput from '@/components/valerie/VInput.vue'; +import VList from '@/components/valerie/VList.vue'; +import VListItem from '@/components/valerie/VListItem.vue'; +import VCheckbox from '@/components/valerie/VCheckbox.vue'; +// VTextarea and VSelect are not used in this part of the refactor for ListDetailPage // UI-specific properties that we add to items @@ -430,16 +364,16 @@ const lastListUpdate = ref(null); const lastItemUpdate = ref(null); const newItem = ref<{ name: string; quantity?: number }>({ name: '' }); -const itemNameInputRef = ref(null); +const itemNameInputRef = ref | null>(null); // Changed type // OCR const showOcrDialogState = ref(false); -const ocrModalRef = ref(null); +// const ocrModalRef = ref(null); // Removed const ocrLoading = ref(false); const ocrItems = ref<{ name: string }[]>([]); // Items extracted from OCR const addingOcrItems = ref(false); const ocrError = ref(null); -const ocrFileInputRef = ref(null); +const ocrFileInputRef = ref | null>(null); // Changed to VInput ref type const { files: ocrFiles, reset: resetOcrFileDialog } = useFileDialog({ accept: 'image/*', multiple: false, @@ -448,13 +382,13 @@ const { files: ocrFiles, reset: resetOcrFileDialog } = useFileDialog({ // Confirmation Dialog const showConfirmDialogState = ref(false); -const confirmModalRef = ref(null); +// const confirmModalRef = ref(null); // Removed const confirmDialogMessage = ref(''); const pendingAction = ref<(() => Promise) | null>(null); // Cost Summary const showCostSummaryDialog = ref(false); -const costSummaryModalRef = ref(null); +// const costSummaryModalRef = ref(null); // Removed const listCostSummary = ref(null); const costSummaryLoading = ref(false); const costSummaryError = ref(null); @@ -462,7 +396,7 @@ const costSummaryError = ref(null); // Settle Share const authStore = useAuthStore(); const showSettleModal = ref(false); -const settleModalRef = ref(null); +// const settleModalRef = ref(null); // Removed const selectedSplitForSettlement = ref(null); const parentExpenseOfSelectedSplit = ref(null); const settleAmount = ref(''); @@ -474,14 +408,10 @@ const showCreateExpenseForm = ref(false); // Edit Item const showEditDialog = ref(false); -const editModalRef = ref(null); +// const editModalRef = ref(null); // Removed const editingItem = ref(null); -onClickOutside(ocrModalRef, () => { showOcrDialogState.value = false; }); -onClickOutside(costSummaryModalRef, () => { showCostSummaryDialog.value = false; }); -onClickOutside(confirmModalRef, () => { showConfirmDialogState.value = false; pendingAction.value = null; }); -onClickOutside(settleModalRef, () => { showSettleModal.value = false; }); -onClickOutside(editModalRef, () => { showEditDialog.value = false; }); +// onClickOutside for ocrModalRef, costSummaryModalRef, etc. are removed as VModal handles this. const formatCurrency = (value: string | number | undefined | null): string => { @@ -603,7 +533,7 @@ const isItemPendingSync = (item: Item) => { const onAddItem = async () => { if (!list.value || !newItem.value.name.trim()) { notificationStore.addNotification({ message: 'Please enter an item name.', type: 'warning' }); - itemNameInputRef.value?.focus(); + itemNameInputRef.value?.focus?.(); // Updated focus call return; } addingItem.value = true; @@ -639,7 +569,7 @@ const onAddItem = async () => { }; list.value.items.push(optimisticItem); newItem.value = { name: '' }; - itemNameInputRef.value?.focus(); + itemNameInputRef.value?.focus?.(); // Updated focus call addingItem.value = false; return; } @@ -655,7 +585,7 @@ const onAddItem = async () => { const addedItem = response.data as Item; list.value.items.push(processListItems([addedItem])[0]); newItem.value = { name: '' }; - itemNameInputRef.value?.focus(); + itemNameInputRef.value?.focus?.(); // Updated focus call } catch (err) { notificationStore.addNotification({ message: (err instanceof Error ? err.message : String(err)) || 'Failed to add item.', type: 'error' }); } finally { @@ -795,8 +725,13 @@ const openOcrDialog = () => { resetOcrFileDialog(); showOcrDialogState.value = true; nextTick(() => { - if (ocrFileInputRef.value) { - ocrFileInputRef.value.value = ''; + // For VInput type file, direct .value = '' might not work or be needed. + // VInput should handle its own reset if necessary, or this ref might target the native input inside. + if (ocrFileInputRef.value && ocrFileInputRef.value.$el) { // Assuming VInput exposes $el + const inputElement = ocrFileInputRef.value.$el.querySelector('input[type="file"]') || ocrFileInputRef.value.$el; + if(inputElement) (inputElement as HTMLInputElement).value = ''; + } else if (ocrFileInputRef.value) { // Fallback if ref is native input + (ocrFileInputRef.value as any).value = ''; } }); }; @@ -839,7 +774,12 @@ const handleOcrUpload = async (file: File) => { ocrError.value = (err instanceof Error ? err.message : String(err)) || 'Failed to process image.'; } finally { ocrLoading.value = false; - if (ocrFileInputRef.value) ocrFileInputRef.value.value = ''; + if (ocrFileInputRef.value && ocrFileInputRef.value.$el) { + const inputElement = ocrFileInputRef.value.$el.querySelector('input[type="file"]') || ocrFileInputRef.value.$el; + if(inputElement) (inputElement as HTMLInputElement).value = ''; + } else if (ocrFileInputRef.value) { + (ocrFileInputRef.value as any).value = ''; + } } }; @@ -852,7 +792,7 @@ const addOcrItems = async () => { if (!item.name.trim()) continue; const response = await apiClient.post( API_ENDPOINTS.LISTS.ITEMS(String(list.value.id)), - { name: item.name, quantity: "1" } + { name: item.name, quantity: "1" } // Assuming default quantity 1 for OCR items ); const addedItem = response.data as Item; list.value.items.push(processListItems([addedItem])[0]); @@ -932,7 +872,7 @@ useEventListener(window, 'keydown', (event: KeyboardEvent) => { return; } event.preventDefault(); - itemNameInputRef.value?.focus(); + itemNameInputRef.value?.focus?.(); // Updated focus call } }); @@ -1400,16 +1340,17 @@ const handleExpenseCreated = (expense: any) => { flex-direction: column; } -.neo-item-name { +.item-name { /* Added for VListItem content */ font-size: 1.1rem; font-weight: 700; } -.neo-item-complete .neo-item-name { +.neo-item-complete .item-name { /* Adjusted for VListItem */ text-decoration: line-through; - opacity: 0.6; + /* opacity: 0.6; Combined with bg-gray-100 opacity-70 on VListItem */ } + .neo-item-quantity { font-size: 0.9rem; color: #555; @@ -1496,23 +1437,24 @@ const handleExpenseCreated = (expense: any) => { cursor: not-allowed; } -.neo-add-item-form { - display: flex; - gap: 0.5rem; - margin-top: 2rem; - border: 3px solid #111; - border-radius: 12px; - padding: 1rem; - background: #f9f9f9; - box-shadow: 4px 4px 0 #111; +.add-item-form { /* Added for new form styling */ + /* display: flex; (already on class) */ + /* gap: 0.5rem; (already on class) */ + /* margin-top: 1rem; (original was 2rem, now mt-4) */ + /* padding: 1rem; (original was 1rem) */ + /* border: 3px solid #111; (original was 3px) */ + /* border-radius: 12px; (original was 12px) */ + /* background: #f9f9f9; (original was #f9f9f9) */ + /* box-shadow: 4px 4px 0 #111; (original was 4px) */ } -.neo-new-item-form { + +.neo-new-item-form { /* Kept for reference, but form tag itself is now styled */ width: 100%; gap: 10px; } -.neo-text-input { +.neo-text-input { /* Not directly used by VInput, but kept for reference */ flex-grow: 1; border: 2px solid #111; border-radius: 8px; @@ -1521,7 +1463,7 @@ const handleExpenseCreated = (expense: any) => { font-weight: 500; } -.neo-new-item-input { +.neo-new-item-input { /* Not directly used by VInput, but kept for reference */ background: transparent; border: none; outline: none; @@ -1533,13 +1475,13 @@ const handleExpenseCreated = (expense: any) => { flex-grow: 1; } -.neo-new-item-input::placeholder { +.neo-new-item-input::placeholder { /* VInput handles its own placeholder styling */ color: #999; font-weight: 500; } -.neo-quantity-input { - width: 80px; +.neo-quantity-input { /* Not directly used by VInput, but kept for reference */ + width: 80px; /* This specific width is now on VFormField for quantity */ border: 2px solid #111; border-radius: 8px; padding: 0.4rem; @@ -1547,7 +1489,7 @@ const handleExpenseCreated = (expense: any) => { font-weight: 500; } -.neo-number-input { +.neo-number-input { /* For price input, now VInput with class="w-24" */ border: 2px solid #111; border-radius: 6px; padding: 0.5rem; @@ -1555,7 +1497,7 @@ const handleExpenseCreated = (expense: any) => { width: 100px; } -.neo-add-button { +.neo-add-button { /* Replaced by VButton */ background: #111; color: white; border: none; @@ -1567,7 +1509,7 @@ const handleExpenseCreated = (expense: any) => { height: 2rem; } -.neo-button { +.neo-button { /* General button, mostly replaced by VButton */ background: #111; color: white; border: none; @@ -1578,7 +1520,7 @@ const handleExpenseCreated = (expense: any) => { cursor: pointer; } -.new-item-input { +.new-item-input { /* Styling for the old li wrapper of add item form, can be removed */ margin-top: 0.5rem; padding: 0.5rem; } @@ -1593,9 +1535,9 @@ const handleExpenseCreated = (expense: any) => { font-size: 1.8rem; } - .neo-item { + /* .neo-item { // VListItem might have its own padding padding: 1rem; - } + } */ } @media (max-width: 600px) { @@ -1619,7 +1561,7 @@ const handleExpenseCreated = (expense: any) => { gap: 0.5rem; } - .neo-action-button { + .neo-action-button { /* VButton has its own sizing */ padding: 0.8rem; font-size: 0.9rem; } @@ -1629,11 +1571,11 @@ const handleExpenseCreated = (expense: any) => { margin-bottom: 1.5rem; } - .neo-item { + /* .neo-item { // VListItem padding: 1rem; - } + } */ - .neo-item-name { + .item-name { /* Adjusted for VListItem */ font-size: 1rem; } @@ -1641,75 +1583,77 @@ const handleExpenseCreated = (expense: any) => { font-size: 0.85rem; } - .neo-checkbox-label input[type="checkbox"] { + /* .neo-checkbox-label input[type="checkbox"] { // VCheckbox has its own styling width: 1.4em; height: 1.4em; - } + } */ - .neo-icon-button { + .neo-icon-button { /* VButton icon-only replaces this */ padding: 0.6rem; } - .neo-new-item-form { + .add-item-form { /* Adjusted form class */ flex-wrap: wrap; gap: 0.5rem; } - .neo-new-item-input { + /* VInput placeholder styling is internal to VInput */ + /* .neo-new-item-input { width: 100%; font-size: 1rem; - } + } */ - .neo-quantity-input { + /* VInput type number styling is internal or via props */ + /* .neo-quantity-input { width: 80px; font-size: 0.9rem; - } + } */ - .neo-add-button { + /* VButton styling replaces this */ + /* .neo-add-button { width: 100%; margin-top: 0.5rem; padding: 0.8rem; - } + } */ /* Optimize modals for mobile */ - .modal-container { + .modal-container { /* VModal has its own responsive sizing via props/CSS */ width: 95%; max-height: 85vh; margin: 1rem; } - .modal-header { + .modal-header { /* VModal slot */ padding: 1rem; } - .modal-body { + .modal-body { /* VModal slot */ padding: 1rem; } - .modal-footer { + .modal-footer { /* VModal slot */ padding: 1rem; } - /* Improve touch targets */ - button, + /* Improve touch targets - general principle, components should handle this */ + /* button, input[type="checkbox"], .neo-checkbox-label { min-height: 44px; - /* Apple's recommended minimum touch target size */ - } + } */ /* Optimize loading states for mobile */ - .neo-loading-state { + .neo-loading-state { /* VSpinner used instead */ padding: 2rem 1rem; } - .spinner-dots span { + .spinner-dots span { /* VSpinner has its own dot styling */ width: 10px; height: 10px; } /* Improve scrolling performance */ - .neo-item-list { + .item-list-tight { /* Assuming VList with this class */ -webkit-overflow-scrolling: touch; } @@ -1727,8 +1671,8 @@ const handleExpenseCreated = (expense: any) => { } } -/* Add smooth transitions for all interactive elements */ -.neo-action-button, +/* Add smooth transitions for all interactive elements - VComponents have their own */ +/* .neo-action-button, .neo-icon-button, .neo-checkbox-label, .neo-add-button { @@ -1742,83 +1686,28 @@ const handleExpenseCreated = (expense: any) => { .neo-add-button:active { transform: scale(0.98); opacity: 0.9; -} +} */ /* Improve scrolling performance */ -.neo-item-list { +.item-list-tight { /* Assuming VList with this class */ will-change: transform; transform: translateZ(0); } -.modal-backdrop { - background-color: rgba(0, 0, 0, 0.5); - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; -} +/* Modal styles are now handled by VModal component */ +/* .modal-backdrop { ... } */ +/* .modal-container { ... } */ +/* .modal-header { ... } */ +/* .modal-body { ... } */ +/* .modal-footer { ... } */ +/* .close-button { ... } */ -.modal-container { - background: white; - border-radius: 18px; - border: 3px solid #111; - box-shadow: 6px 6px 0 #111; - width: 90%; - max-width: 500px; - max-height: 90vh; - overflow-y: auto; - padding: 0; -} -.modal-header { - padding: 1.5rem; - border-bottom: 1px solid #eee; - display: flex; - justify-content: space-between; - align-items: center; -} +/* Item badge styles are now handled by VBadge */ +/* .item-badge { ... } */ +/* .badge-settled { ... } */ +/* .badge-pending { ... } */ -.modal-body { - padding: 1.5rem; -} - -.modal-footer { - padding: 1.5rem; - border-top: 1px solid #eee; - display: flex; - justify-content: flex-end; - gap: 0.5rem; -} - -.close-button { - background: none; - border: none; - cursor: pointer; - color: #666; -} - -.item-badge { - display: inline-block; - padding: 0.25rem 0.5rem; - border-radius: 16px; - font-weight: 700; - font-size: 0.9rem; -} - -.badge-settled { - background-color: #d4f7dd; - color: #2c784c; -} - -.badge-pending { - background-color: #ffe1d6; - color: #c64600; -} .text-right { text-align: right; @@ -1828,56 +1717,50 @@ const handleExpenseCreated = (expense: any) => { text-align: center; } -.spinner-dots { - display: flex; - align-items: center; - justify-content: center; - gap: 0.3rem; - margin: 0 auto; -} +/* Spinner styles are now handled by VSpinner */ +/* .spinner-dots { ... } */ +/* .spinner-dots span { ... } */ +/* .spinner-dots-sm { ... } */ +/* .spinner-dots-sm span { ... } */ +/* @keyframes dot-pulse { ... } */ -.spinner-dots span { - width: 8px; - height: 8px; - background-color: #555; - border-radius: 50%; - animation: dot-pulse 1.4s infinite ease-in-out both; -} +/* Utility classes that might still be used or can be replaced by Tailwind/global equivalents */ +.flex { display: flex; } +.items-center { align-items: center; } +.justify-between { justify-content: space-between; } +.gap-1 { gap: 0.25rem; } +.gap-2 { gap: 0.5rem; } +.ml-1 { margin-left: 0.25rem; } +.ml-2 { margin-left: 0.5rem; } +.mt-1 { margin-top: 0.25rem; } +.mt-2 { margin-top: 0.5rem; } +.mt-4 { margin-top: 1rem; } +.mb-2 { margin-bottom: 0.5rem; } +.mb-3 { margin-bottom: 1rem; } /* Adjusted from 1.5rem to match common spacing */ +.mb-4 { margin-bottom: 1.5rem; } +.py-10 { padding-top: 2.5rem; padding-bottom: 2.5rem; } +.py-4 { padding-top: 1rem; padding-bottom: 1rem; } +.p-4 { padding: 1rem; } +.border { border-width: 1px; /* Assuming default border color from global styles or Tailwind */ } +.rounded-lg { border-radius: 0.5rem; } +.shadow { box-shadow: 0 1px 3px 0 rgba(0,0,0,0.1), 0 1px 2px 0 rgba(0,0,0,0.06); /* Example shadow */} +.flex-grow { flex-grow: 1; } +.w-24 { width: 6rem; } /* Tailwind w-24 */ +.text-sm { font-size: 0.875rem; } +.text-gray-500 { color: #6b7280; } /* Tailwind gray-500 */ +.text-gray-400 { color: #9ca3af; } /* Tailwind gray-400 */ +.text-green-600 { color: #16a34a; } /* Tailwind green-600 */ +.text-yellow-500 { color: #eab308; } /* Tailwind yellow-500 */ +.line-through { text-decoration: line-through; } +.opacity-50 { opacity: 0.5; } +.opacity-60 { opacity: 0.6; } /* Added for completed item name */ +.opacity-70 { opacity: 0.7; } /* Added for completed item background */ +.shrink-0 { flex-shrink: 0; } +.bg-gray-100 { background-color: #f3f4f6; } /* Tailwind gray-100 */ -.spinner-dots-sm { - display: inline-flex; - align-items: center; - gap: 0.2rem; -} - -.spinner-dots-sm span { - width: 4px; - height: 4px; - background-color: white; - border-radius: 50%; - animation: dot-pulse 1.4s infinite ease-in-out both; -} - -.spinner-dots span:nth-child(1), -.spinner-dots-sm span:nth-child(1) { - animation-delay: -0.32s; -} - -.spinner-dots span:nth-child(2), -.spinner-dots-sm span:nth-child(2) { - animation-delay: -0.16s; -} - -@keyframes dot-pulse { - - 0%, - 80%, - 100% { - transform: scale(0); - } - - 40% { - transform: scale(1); - } +/* Styles for .neo-list-card, .neo-item-list, .neo-item might be replaced by VCard/VList/VListItem defaults or props */ +/* Keeping some specific styles for .neo-item-details, .item-name, etc. if they are distinct. */ +.item-with-actions { /* Custom class for VListItem if needed for specific layout */ + /* Default VListItem is display:flex, so this might not be needed or just for minor tweaks */ }