refactor: Update frontend components and Dockerfile for production #5

Merged
mo merged 1 commits from ph4 into prod 2025-06-01 14:59:49 +02:00
4 changed files with 301 additions and 154 deletions

View File

@ -32,7 +32,7 @@ COPY . .
ENV NODE_ENV=production ENV NODE_ENV=production
# Build the application # Build the application
RUN npm run build RUN npm run build-only
# Production stage # Production stage
FROM node:slim AS production FROM node:slim AS production

View File

@ -84,7 +84,7 @@ export default defineComponent({
// Add other common styling for list item content area // Add other common styling for list item content area
// e.g., text color, font size // e.g., text color, font size
background-color: inherit; // Inherit from .list-item, can be overridden background-color: inherit; // Inherit from .list-item, can be overridden
// Useful so that when it slides, it has the right bg // Useful so that when it slides, it has the right bg
} }
.swipe-actions { .swipe-actions {
@ -108,7 +108,8 @@ export default defineComponent({
transform: translateX(-100%); // Initially hidden to the left transform: translateX(-100%); // Initially hidden to the left
transition: transform 0.3s ease-out; transition: transform 0.3s ease-out;
.list-item.is-swiped & { // When swiped to reveal left actions .list-item.is-swiped & {
// When swiped to reveal left actions
transform: translateX(0); transform: translateX(0);
} }
} }
@ -161,9 +162,10 @@ export default defineComponent({
// If wrapper translates right, it reveals the list-item's left-actions. // If wrapper translates right, it reveals the list-item's left-actions.
// Reveal right actions by translating the list-item-content-wrapper to the left // Reveal right actions by translating the list-item-content-wrapper to the left
.list-item.is-swiped & { // This assumes isSwiped means revealing RIGHT actions. .list-item.is-swiped & {
// Need differentiation if both left/right can be revealed independently. // This assumes isSwiped means revealing RIGHT actions.
// For now, isSwiped reveals right. // Need differentiation if both left/right can be revealed independently.
// For now, isSwiped reveals right.
// This class is on .list-item. The .swipe-actions-right is inside the wrapper. // This class is on .list-item. The .swipe-actions-right is inside the wrapper.
// So, the wrapper needs to translate. // So, the wrapper needs to translate.
// No, this is fine. .list-item.is-swiped controls the transform of list-item-content-wrapper. // No, this is fine. .list-item.is-swiped controls the transform of list-item-content-wrapper.
@ -175,6 +177,7 @@ export default defineComponent({
// This logic should be on .list-item-content-wrapper based on .is-swiped of parent. // This logic should be on .list-item-content-wrapper based on .is-swiped of parent.
} }
} }
// Adjusting transform on list-item-content-wrapper based on parent .is-swiped // Adjusting transform on list-item-content-wrapper based on parent .is-swiped
.list-item.is-swiped .list-item-content-wrapper { .list-item.is-swiped .list-item-content-wrapper {
// This needs to be dynamic based on which actions are shown and their width. // This needs to be dynamic based on which actions are shown and their width.
@ -231,15 +234,18 @@ export default defineComponent({
// A true swipe needs JS to measure or fixed widths. // A true swipe needs JS to measure or fixed widths.
// Let's go with a fixed transform for now for demo purposes. // Let's go with a fixed transform for now for demo purposes.
.list-item.is-swiped .list-item-content-wrapper { .list-item.is-swiped .list-item-content-wrapper {
transform: translateX(-80px); // Assumes right actions are 80px. transform: translateX(-80px); // Assumes right actions are 80px.
} }
// And left actions (if any)
.list-item.is-left-swiped .list-item-content-wrapper { // Hypothetical class // And left actions (if any)
transform: translateX(80px); // Assumes left actions are 80px. .list-item.is-left-swiped .list-item-content-wrapper {
// Hypothetical class
transform: translateX(80px); // Assumes left actions are 80px.
} }
// Since `isSwiped` is boolean, it can only control one state. // Since `isSwiped` is boolean, it can only control one state.
// Let's assume `isSwiped` means "the right actions are visible". // Let's assume `isSwiped` means "the right actions are visible".
}
.list-item.completed { .list-item.completed {
.list-item-content { .list-item-content {
@ -248,6 +254,7 @@ export default defineComponent({
// text-decoration: line-through; // text-decoration: line-through;
background-color: #f0f8ff; // Light blue background for completed background-color: #f0f8ff; // Light blue background for completed
} }
// You might want to disable swipe on completed items or style them differently // You might want to disable swipe on completed items or style them differently
&.swipable .list-item-content { &.swipable .list-item-content {
// Specific style for swipable AND completed // Specific style for swipable AND completed

View File

@ -5,17 +5,21 @@
</div> </div>
<VAlert v-else-if="error && !list" type="error" :message="error" class="mb-4"> <VAlert v-else-if="error && !list" type="error" :message="error" class="mb-4">
<template #actions> <VButton @click="fetchListDetails">Retry</VButton> </template> <template #actions>
<VButton @click="fetchListDetails">Retry</VButton>
</template>
</VAlert> </VAlert>
<template v-else-if="list"> <template v-else-if="list">
<!-- Header --> <!-- Header -->
<div class="neo-list-header"> <div class="neo-list-header">
<VHeading level="1" :text="list.name" class="mb-3 neo-title" /> {/* Kept neo-title for existing style */} <VHeading :level="1" :text="list.name" class="mb-3 neo-title" />
<div class="neo-header-actions"> <div class="neo-header-actions">
<VButton @click="showCostSummaryDialog = true" :disabled="!isOnline" icon-left="clipboard">Cost Summary</VButton> <VButton @click="showCostSummaryDialog = true" :disabled="!isOnline" icon-left="clipboard">Cost Summary
</VButton>
<VButton @click="openOcrDialog" :disabled="!isOnline" icon-left="plus">Add via OCR</VButton> <VButton @click="openOcrDialog" :disabled="!isOnline" icon-left="plus">Add via OCR</VButton>
<VBadge :text="list.group_id ? 'Group List' : 'Personal List'" :variant="list.group_id ? 'info' : 'success'" class="neo-status" /> {/* Kept neo-status for existing style */} <VBadge :text="list.group_id ? 'Group List' : 'Personal List'" :variant="list.group_id ? 'accent' : 'settled'"
class="neo-status" />
</div> </div>
</div> </div>
<p v-if="list.description" class="neo-description">{{ list.description }}</p> <p v-if="list.description" class="neo-description">{{ list.description }}</p>
@ -24,41 +28,34 @@
<VCard v-if="itemsAreLoading" class="py-10 text-center mt-4"> <VCard v-if="itemsAreLoading" class="py-10 text-center mt-4">
<VSpinner label="Loading items..." size="lg" /> <VSpinner label="Loading items..." size="lg" />
</VCard> </VCard>
<VCard v-else-if="!itemsAreLoading && list.items.length === 0" variant="empty-state" empty-icon="clipboard" empty-title="No Items Yet!" empty-message="Add some items using the form below." class="mt-4" /> <VCard v-else-if="!itemsAreLoading && list.items.length === 0" variant="empty-state" empty-icon="clipboard"
empty-title="No Items Yet!" empty-message="Add some items using the form below." class="mt-4" />
<VCard v-else class="mt-4"> <VCard v-else class="mt-4">
<VList class="item-list-tight"> {/* Assuming item-list-tight might be needed or VList default is fine */} <VList class="item-list-tight">
<VListItem v-for="item in list.items" :key="item.id" class="item-with-actions" :class="{ 'bg-gray-100 opacity-70': item.is_complete }"> <VListItem v-for="item in list.items" :key="item.id" class="item-with-actions"
:class="{ 'bg-gray-100 opacity-70': item.is_complete }">
<template #default> <template #default>
<div class="flex items-center flex-grow gap-2"> <div class="flex items-center flex-grow gap-2">
<VCheckbox <VCheckbox :model-value="item.is_complete" @update:modelValue="confirmUpdateItem(item, $event)"
:model-value="item.is_complete" :disabled="item.updating" :aria-label="item.name" />
@update:modelValue="confirmUpdateItem(item, $event)"
:disabled="item.updating"
:aria-label="item.name"
/>
<div class="flex-grow"> <div class="flex-grow">
<span class="item-name" :class="{'line-through': item.is_complete}">{{ item.name }}</span> <span class="item-name" :class="{ 'line-through': item.is_complete }">{{ item.name }}</span>
<span v-if="item.quantity" class="text-sm text-gray-500 ml-1">× {{ item.quantity }}</span> <span v-if="item.quantity" class="text-sm text-gray-500 ml-1">× {{ item.quantity }}</span>
<div v-if="item.is_complete" class="mt-1"> <div v-if="item.is_complete" class="mt-1">
<VInput <VInput type="number" :model-value="item.priceInput || ''"
type="number" @update:modelValue="item.priceInput = $event" placeholder="Price" size="sm" class="w-24"
:model-value="item.priceInput" step="0.01" @blur="updateItemPrice(item)"
@update:modelValue="item.priceInput = $event" @keydown.enter.prevent="($event.target as HTMLInputElement).blur()" />
placeholder="Price"
size="sm"
class="w-24"
step="0.01"
@blur="updateItemPrice(item)"
@keydown.enter.prevent="($event.target as HTMLInputElement).blur()"
/>
</div> </div>
</div> </div>
</div> </div>
<div class="flex items-center gap-1 ml-2"> <div class="flex items-center gap-1 ml-2">
<VButton icon-only="true" size="sm" variant="ghost" @click.stop="editItem(item)" aria-label="Edit item"> <VButton :icon-only="true" size="sm" variant="neutral" @click.stop="editItem(item)"
aria-label="Edit item">
<VIcon name="edit" /> <VIcon name="edit" />
</VButton> </VButton>
<VButton icon-only="true" size="sm" variant="ghost" color="danger" @click.stop="confirmDeleteItem(item)" :disabled="item.deleting" aria-label="Delete item"> <VButton :icon-only="true" size="sm" variant="neutral" color="danger"
@click.stop="confirmDeleteItem(item)" :disabled="item.deleting" aria-label="Delete item">
<VIcon name="trash" /> <VIcon name="trash" />
</VButton> </VButton>
</div> </div>
@ -69,24 +66,15 @@
<!-- Add New Item Form --> <!-- Add New Item Form -->
<form @submit.prevent="onAddItem" class="add-item-form mt-4 p-4 border rounded-lg shadow flex items-center gap-2"> <form @submit.prevent="onAddItem" class="add-item-form mt-4 p-4 border rounded-lg shadow flex items-center gap-2">
<VIcon name="plus-circle" class="text-gray-400 shrink-0" /> {/* Added shrink-0 */} <VIcon name="plus-circle" class="text-gray-400 shrink-0" />
<VFormField class="flex-grow" label="New item name" :label-sr-only="true"> <VFormField class="flex-grow" label="New item name" :label-sr-only="true">
<VInput <VInput v-model="newItem.name" placeholder="Add a new item" required ref="itemNameInputRef" />
v-model="newItem.name"
placeholder="Add a new item"
required
ref="itemNameInputRef"
/>
</VFormField> </VFormField>
<VFormField label="Quantity" :label-sr-only="true" class="w-24 shrink-0"> {/* Added shrink-0 and changed w-20 to w-24 for better fit */} <VFormField label="Quantity" :label-sr-only="true" class="w-24 shrink-0">
<VInput <VInput type="number" :model-value="newItem.quantity || ''" @update:modelValue="newItem.quantity = $event"
type="number" placeholder="Qty" min="1" />
v-model="newItem.quantity"
placeholder="Qty"
min="1"
/>
</VFormField> </VFormField>
<VButton type="submit" :disabled="addingItem" class="shrink-0"> {/* Added shrink-0 */} <VButton type="submit" :disabled="addingItem" class="shrink-0">
<VSpinner v-if="addingItem" size="sm" /> <VSpinner v-if="addingItem" size="sm" />
<span v-else>Add</span> <span v-else>Add</span>
</VButton> </VButton>
@ -167,32 +155,38 @@
<!-- OCR Dialog --> <!-- OCR Dialog -->
<VModal v-model="showOcrDialogState" title="Add Items via OCR" @update:modelValue="!$event && closeOcrDialog()"> <VModal v-model="showOcrDialogState" title="Add Items via OCR" @update:modelValue="!$event && closeOcrDialog()">
<template #default> <template #default>
<div v-if="ocrLoading" class="text-center"><VSpinner label="Processing image..." /></div> <div v-if="ocrLoading" class="text-center">
<VSpinner label="Processing image..." />
</div>
<VList v-else-if="ocrItems.length > 0"> <VList v-else-if="ocrItems.length > 0">
<VListItem v-for="(ocrItem, index) in ocrItems" :key="index"> <VListItem v-for="(ocrItem, index) in ocrItems" :key="index">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<VInput type="text" v-model="ocrItem.name" class="flex-grow" required /> <VInput type="text" v-model="ocrItem.name" class="flex-grow" required />
<VButton variant="danger" size="sm" :icon-only="true" iconLeft="trash" @click="ocrItems.splice(index, 1)" /> <VButton variant="danger" size="sm" :icon-only="true" iconLeft="trash"
@click="ocrItems.splice(index, 1)" />
</div> </div>
</VListItem> </VListItem>
</VList> </VList>
<VFormField v-else label="Upload Image" :error-message="ocrError"> <VFormField v-else label="Upload Image" :error-message="ocrError || undefined">
<VInput type="file" id="ocrFile" accept="image/*" @change="handleOcrFileUpload" ref="ocrFileInputRef" /> <VInput type="file" id="ocrFile" accept="image/*" @change="handleOcrFileUpload" ref="ocrFileInputRef"
:model-value="''" />
</VFormField> </VFormField>
</template> </template>
<template #footer> <template #footer>
<VButton variant="neutral" @click="closeOcrDialog">Cancel</VButton> <VButton variant="neutral" @click="closeOcrDialog">Cancel</VButton>
<VButton v-if="ocrItems.length > 0" type="button" variant="primary" @click="addOcrItems" :disabled="addingOcrItems"> <VButton v-if="ocrItems.length > 0" type="button" variant="primary" @click="addOcrItems"
<VSpinner v-if="addingOcrItems" size="sm"/> Add Items :disabled="addingOcrItems">
<VSpinner v-if="addingOcrItems" size="sm" /> Add Items
</VButton> </VButton>
</template> </template>
</VModal> </VModal>
<!-- Confirmation Dialog --> <!-- Confirmation Dialog -->
<VModal v-model="showConfirmDialogState" title="Confirmation" @update:modelValue="!$event && cancelConfirmation()" size="sm"> <VModal v-model="showConfirmDialogState" title="Confirmation" @update:modelValue="!$event && cancelConfirmation()"
size="sm">
<template #default> <template #default>
<div class="text-center"> <div class="text-center">
<VIcon name="alert-triangle" size="lg" class="text-yellow-500 mb-2" /> {/* Ensure text-yellow-500 is defined or use VAlert type="warning" */} <VIcon name="alert-triangle" size="lg" class="text-yellow-500 mb-2" />
<p>{{ confirmDialogMessage }}</p> <p>{{ confirmDialogMessage }}</p>
</div> </div>
</template> </template>
@ -203,9 +197,12 @@
</VModal> </VModal>
<!-- Cost Summary Dialog --> <!-- Cost Summary Dialog -->
<VModal v-model="showCostSummaryDialog" title="List Cost Summary" @update:modelValue="showCostSummaryDialog = false" size="lg"> <VModal v-model="showCostSummaryDialog" title="List Cost Summary" @update:modelValue="showCostSummaryDialog = false"
size="lg">
<template #default> <template #default>
<div v-if="costSummaryLoading" class="text-center"><VSpinner label="Loading summary..." /></div> <div v-if="costSummaryLoading" class="text-center">
<VSpinner label="Loading summary..." />
</div>
<VAlert v-else-if="costSummaryError" type="error" :message="costSummaryError" /> <VAlert v-else-if="costSummaryError" type="error" :message="costSummaryError" />
<div v-else-if="listCostSummary"> <div v-else-if="listCostSummary">
<div class="mb-3 cost-overview"> <div class="mb-3 cost-overview">
@ -230,7 +227,8 @@
<td class="text-right">{{ formatCurrency(userShare.items_added_value) }}</td> <td class="text-right">{{ formatCurrency(userShare.items_added_value) }}</td>
<td class="text-right">{{ formatCurrency(userShare.amount_due) }}</td> <td class="text-right">{{ formatCurrency(userShare.amount_due) }}</td>
<td class="text-right"> <td class="text-right">
<VBadge :text="formatCurrency(userShare.balance)" :variant="parseFloat(String(userShare.balance)) >= 0 ? 'success' : 'pending'" /> <VBadge :text="formatCurrency(userShare.balance)"
:variant="parseFloat(String(userShare.balance)) >= 0 ? 'settled' : 'pending'" />
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -245,19 +243,23 @@
</VModal> </VModal>
<!-- Settle Share Modal --> <!-- Settle Share Modal -->
<VModal v-model="showSettleModal" title="Settle Share" @update:modelValue="!$event && closeSettleShareModal()" size="md"> <VModal v-model="showSettleModal" title="Settle Share" @update:modelValue="!$event && closeSettleShareModal()"
size="md">
<template #default> <template #default>
<div v-if="isSettlementLoading" class="text-center"><VSpinner label="Processing settlement..." /></div> <div v-if="isSettlementLoading" class="text-center">
<VSpinner label="Processing settlement..." />
</div>
<VAlert v-else-if="settleAmountError" type="error" :message="settleAmountError" /> <VAlert v-else-if="settleAmountError" type="error" :message="settleAmountError" />
<div v-else> <div v-else>
<p>Settle amount for {{ selectedSplitForSettlement?.user?.name || selectedSplitForSettlement?.user?.email || `User ID: ${selectedSplitForSettlement?.user_id}` }}:</p> <p>Settle amount for {{ selectedSplitForSettlement?.user?.name || selectedSplitForSettlement?.user?.email ||
<VFormField label="Amount" :error-message="settleAmountError"> {/* Error message was shown above, consider if needed here too */} `User ID: ${selectedSplitForSettlement?.user_id}` }}:</p>
<VFormField label="Amount" :error-message="settleAmountError || undefined">
<VInput type="number" v-model="settleAmount" id="settleAmount" required /> <VInput type="number" v-model="settleAmount" id="settleAmount" required />
</VFormField> </VFormField>
</div> </div>
</template> </template>
<template #footer> <template #footer>
<VButton variant="neutral" @click="closeSettleShareModal">Cancel</VButton> {/* Added Cancel for consistency */} <VButton variant="neutral" @click="closeSettleShareModal">Cancel</VButton>
<VButton variant="primary" @click="handleConfirmSettle">Confirm</VButton> <VButton variant="primary" @click="handleConfirmSettle">Confirm</VButton>
</template> </template>
</VModal> </VModal>
@ -265,20 +267,22 @@
<!-- Edit Item Dialog --> <!-- Edit Item Dialog -->
<VModal v-model="showEditDialog" title="Edit Item" @update:modelValue="!$event && closeEditDialog()"> <VModal v-model="showEditDialog" title="Edit Item" @update:modelValue="!$event && closeEditDialog()">
<template #default> <template #default>
<VFormField v-if="editingItem" label="Item Name" class="mb-4"> {/* Added margin */} <VFormField v-if="editingItem" label="Item Name" class="mb-4">
<VInput type="text" id="editItemName" v-model="editingItem.name" required /> <VInput type="text" id="editItemName" v-model="editingItem.name" required />
</VFormField> </VFormField>
<VFormField v-if="editingItem" label="Quantity"> <VFormField v-if="editingItem" label="Quantity">
<VInput type="number" id="editItemQuantity" v-model.number="editingItem.quantity" min="1" /> <VInput type="number" id="editItemQuantity" :model-value="editingItem.quantity || ''"
@update:modelValue="editingItem.quantity = $event" min="1" />
</VFormField> </VFormField>
</template> </template>
<template #footer> <template #footer>
<VButton variant="neutral" @click="closeEditDialog">Cancel</VButton> <VButton variant="neutral" @click="closeEditDialog">Cancel</VButton>
<VButton variant="primary" @click="handleConfirmEdit" :disabled="!editingItem?.name.trim()">Save Changes</VButton> <VButton variant="primary" @click="handleConfirmEdit" :disabled="!editingItem?.name.trim()">Save Changes
</VButton>
</template> </template>
</VModal> </VModal>
<VAlert v-else type="info" message="Group not found or an error occurred." /> <VAlert v-if="!list && !pageInitialLoad" type="info" message="Group not found or an error occurred." />
</main> </main>
</template> </template>
@ -533,7 +537,9 @@ const isItemPendingSync = (item: Item) => {
const onAddItem = async () => { const onAddItem = async () => {
if (!list.value || !newItem.value.name.trim()) { if (!list.value || !newItem.value.name.trim()) {
notificationStore.addNotification({ message: 'Please enter an item name.', type: 'warning' }); notificationStore.addNotification({ message: 'Please enter an item name.', type: 'warning' });
itemNameInputRef.value?.focus?.(); // Updated focus call if (itemNameInputRef.value?.$el) {
(itemNameInputRef.value.$el as HTMLElement).focus();
}
return; return;
} }
addingItem.value = true; addingItem.value = true;
@ -569,7 +575,9 @@ const onAddItem = async () => {
}; };
list.value.items.push(optimisticItem); list.value.items.push(optimisticItem);
newItem.value = { name: '' }; newItem.value = { name: '' };
itemNameInputRef.value?.focus?.(); // Updated focus call if (itemNameInputRef.value?.$el) {
(itemNameInputRef.value.$el as HTMLElement).focus();
}
addingItem.value = false; addingItem.value = false;
return; return;
} }
@ -585,7 +593,9 @@ const onAddItem = async () => {
const addedItem = response.data as Item; const addedItem = response.data as Item;
list.value.items.push(processListItems([addedItem])[0]); list.value.items.push(processListItems([addedItem])[0]);
newItem.value = { name: '' }; newItem.value = { name: '' };
itemNameInputRef.value?.focus?.(); // Updated focus call if (itemNameInputRef.value?.$el) {
(itemNameInputRef.value.$el as HTMLElement).focus();
}
} catch (err) { } catch (err) {
notificationStore.addNotification({ message: (err instanceof Error ? err.message : String(err)) || 'Failed to add item.', type: 'error' }); notificationStore.addNotification({ message: (err instanceof Error ? err.message : String(err)) || 'Failed to add item.', type: 'error' });
} finally { } finally {
@ -728,10 +738,10 @@ const openOcrDialog = () => {
// For VInput type file, direct .value = '' might not work or be needed. // 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. // 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 if (ocrFileInputRef.value && ocrFileInputRef.value.$el) { // Assuming VInput exposes $el
const inputElement = ocrFileInputRef.value.$el.querySelector('input[type="file"]') || ocrFileInputRef.value.$el; const inputElement = ocrFileInputRef.value.$el.querySelector('input[type="file"]') || ocrFileInputRef.value.$el;
if(inputElement) (inputElement as HTMLInputElement).value = ''; if (inputElement) (inputElement as HTMLInputElement).value = '';
} else if (ocrFileInputRef.value) { // Fallback if ref is native input } else if (ocrFileInputRef.value) { // Fallback if ref is native input
(ocrFileInputRef.value as any).value = ''; (ocrFileInputRef.value as any).value = '';
} }
}); });
}; };
@ -774,11 +784,11 @@ const handleOcrUpload = async (file: File) => {
ocrError.value = (err instanceof Error ? err.message : String(err)) || 'Failed to process image.'; ocrError.value = (err instanceof Error ? err.message : String(err)) || 'Failed to process image.';
} finally { } finally {
ocrLoading.value = false; ocrLoading.value = false;
if (ocrFileInputRef.value && ocrFileInputRef.value.$el) { if (ocrFileInputRef.value && ocrFileInputRef.value.$el) {
const inputElement = ocrFileInputRef.value.$el.querySelector('input[type="file"]') || ocrFileInputRef.value.$el; const inputElement = ocrFileInputRef.value.$el.querySelector('input[type="file"]') || ocrFileInputRef.value.$el;
if(inputElement) (inputElement as HTMLInputElement).value = ''; if (inputElement) (inputElement as HTMLInputElement).value = '';
} else if (ocrFileInputRef.value) { } else if (ocrFileInputRef.value) {
(ocrFileInputRef.value as any).value = ''; (ocrFileInputRef.value as any).value = '';
} }
} }
}; };
@ -872,7 +882,9 @@ useEventListener(window, 'keydown', (event: KeyboardEvent) => {
return; return;
} }
event.preventDefault(); event.preventDefault();
itemNameInputRef.value?.focus?.(); // Updated focus call if (itemNameInputRef.value?.$el) {
(itemNameInputRef.value.$el as HTMLElement).focus();
}
} }
}); });
@ -1340,12 +1352,14 @@ const handleExpenseCreated = (expense: any) => {
flex-direction: column; flex-direction: column;
} }
.item-name { /* Added for VListItem content */ .item-name {
/* Added for VListItem content */
font-size: 1.1rem; font-size: 1.1rem;
font-weight: 700; font-weight: 700;
} }
.neo-item-complete .item-name { /* Adjusted for VListItem */ .neo-item-complete .item-name {
/* Adjusted for VListItem */
text-decoration: line-through; text-decoration: line-through;
/* opacity: 0.6; Combined with bg-gray-100 opacity-70 on VListItem */ /* opacity: 0.6; Combined with bg-gray-100 opacity-70 on VListItem */
} }
@ -1409,17 +1423,14 @@ const handleExpenseCreated = (expense: any) => {
} }
.neo-action-button { .neo-action-button {
background: #fff; /* General button, mostly replaced by VButton */
border: 3px solid #111; background: #111;
color: white;
border: none;
border-radius: 8px; border-radius: 8px;
padding: 0.6rem 1rem; padding: 0.6rem 1rem;
font-weight: 700; font-weight: 700;
cursor: pointer; cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
box-shadow: 3px 3px 0 #111;
transition: transform 0.1s ease-in-out, box-shadow 0.1s ease-in-out;
} }
.neo-action-button:hover { .neo-action-button:hover {
@ -1437,7 +1448,8 @@ const handleExpenseCreated = (expense: any) => {
cursor: not-allowed; cursor: not-allowed;
} }
.add-item-form { /* Added for new form styling */ .add-item-form {
/* Added for new form styling */
/* display: flex; (already on class) */ /* display: flex; (already on class) */
/* gap: 0.5rem; (already on class) */ /* gap: 0.5rem; (already on class) */
/* margin-top: 1rem; (original was 2rem, now mt-4) */ /* margin-top: 1rem; (original was 2rem, now mt-4) */
@ -1449,12 +1461,14 @@ const handleExpenseCreated = (expense: any) => {
} }
.neo-new-item-form { /* Kept for reference, but form tag itself is now styled */ .neo-new-item-form {
/* Kept for reference, but form tag itself is now styled */
width: 100%; width: 100%;
gap: 10px; gap: 10px;
} }
.neo-text-input { /* Not directly used by VInput, but kept for reference */ .neo-text-input {
/* Not directly used by VInput, but kept for reference */
flex-grow: 1; flex-grow: 1;
border: 2px solid #111; border: 2px solid #111;
border-radius: 8px; border-radius: 8px;
@ -1463,7 +1477,8 @@ const handleExpenseCreated = (expense: any) => {
font-weight: 500; font-weight: 500;
} }
.neo-new-item-input { /* Not directly used by VInput, but kept for reference */ .neo-new-item-input {
/* Not directly used by VInput, but kept for reference */
background: transparent; background: transparent;
border: none; border: none;
outline: none; outline: none;
@ -1475,13 +1490,16 @@ const handleExpenseCreated = (expense: any) => {
flex-grow: 1; flex-grow: 1;
} }
.neo-new-item-input::placeholder { /* VInput handles its own placeholder styling */ .neo-new-item-input::placeholder {
/* VInput handles its own placeholder styling */
color: #999; color: #999;
font-weight: 500; font-weight: 500;
} }
.neo-quantity-input { /* Not directly used by VInput, but kept for reference */ .neo-quantity-input {
width: 80px; /* This specific width is now on VFormField for quantity */ /* 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: 2px solid #111;
border-radius: 8px; border-radius: 8px;
padding: 0.4rem; padding: 0.4rem;
@ -1489,7 +1507,8 @@ const handleExpenseCreated = (expense: any) => {
font-weight: 500; font-weight: 500;
} }
.neo-number-input { /* For price input, now VInput with class="w-24" */ .neo-number-input {
/* For price input, now VInput with class="w-24" */
border: 2px solid #111; border: 2px solid #111;
border-radius: 6px; border-radius: 6px;
padding: 0.5rem; padding: 0.5rem;
@ -1497,7 +1516,8 @@ const handleExpenseCreated = (expense: any) => {
width: 100px; width: 100px;
} }
.neo-add-button { /* Replaced by VButton */ .neo-add-button {
/* Replaced by VButton */
background: #111; background: #111;
color: white; color: white;
border: none; border: none;
@ -1509,7 +1529,8 @@ const handleExpenseCreated = (expense: any) => {
height: 2rem; height: 2rem;
} }
.neo-button { /* General button, mostly replaced by VButton */ .neo-button {
/* General button, mostly replaced by VButton */
background: #111; background: #111;
color: white; color: white;
border: none; border: none;
@ -1520,7 +1541,8 @@ const handleExpenseCreated = (expense: any) => {
cursor: pointer; cursor: pointer;
} }
.new-item-input { /* Styling for the old li wrapper of add item form, can be removed */ .new-item-input {
/* Styling for the old li wrapper of add item form, can be removed */
margin-top: 0.5rem; margin-top: 0.5rem;
padding: 0.5rem; padding: 0.5rem;
} }
@ -1561,7 +1583,8 @@ const handleExpenseCreated = (expense: any) => {
gap: 0.5rem; gap: 0.5rem;
} }
.neo-action-button { /* VButton has its own sizing */ .neo-action-button {
/* VButton has its own sizing */
padding: 0.8rem; padding: 0.8rem;
font-size: 0.9rem; font-size: 0.9rem;
} }
@ -1575,7 +1598,8 @@ const handleExpenseCreated = (expense: any) => {
padding: 1rem; padding: 1rem;
} */ } */
.item-name { /* Adjusted for VListItem */ .item-name {
/* Adjusted for VListItem */
font-size: 1rem; font-size: 1rem;
} }
@ -1588,11 +1612,13 @@ const handleExpenseCreated = (expense: any) => {
height: 1.4em; height: 1.4em;
} */ } */
.neo-icon-button { /* VButton icon-only replaces this */ .neo-icon-button {
/* VButton icon-only replaces this */
padding: 0.6rem; padding: 0.6rem;
} }
.add-item-form { /* Adjusted form class */ .add-item-form {
/* Adjusted form class */
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.5rem; gap: 0.5rem;
} }
@ -1617,21 +1643,25 @@ const handleExpenseCreated = (expense: any) => {
} */ } */
/* Optimize modals for mobile */ /* Optimize modals for mobile */
.modal-container { /* VModal has its own responsive sizing via props/CSS */ .modal-container {
/* VModal has its own responsive sizing via props/CSS */
width: 95%; width: 95%;
max-height: 85vh; max-height: 85vh;
margin: 1rem; margin: 1rem;
} }
.modal-header { /* VModal slot */ .modal-header {
/* VModal slot */
padding: 1rem; padding: 1rem;
} }
.modal-body { /* VModal slot */ .modal-body {
/* VModal slot */
padding: 1rem; padding: 1rem;
} }
.modal-footer { /* VModal slot */ .modal-footer {
/* VModal slot */
padding: 1rem; padding: 1rem;
} }
@ -1643,17 +1673,20 @@ const handleExpenseCreated = (expense: any) => {
} */ } */
/* Optimize loading states for mobile */ /* Optimize loading states for mobile */
.neo-loading-state { /* VSpinner used instead */ .neo-loading-state {
/* VSpinner used instead */
padding: 2rem 1rem; padding: 2rem 1rem;
} }
.spinner-dots span { /* VSpinner has its own dot styling */ .spinner-dots span {
/* VSpinner has its own dot styling */
width: 10px; width: 10px;
height: 10px; height: 10px;
} }
/* Improve scrolling performance */ /* Improve scrolling performance */
.item-list-tight { /* Assuming VList with this class */ .item-list-tight {
/* Assuming VList with this class */
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
@ -1689,7 +1722,8 @@ const handleExpenseCreated = (expense: any) => {
} */ } */
/* Improve scrolling performance */ /* Improve scrolling performance */
.item-list-tight { /* Assuming VList with this class */ .item-list-tight {
/* Assuming VList with this class */
will-change: transform; will-change: transform;
transform: translateZ(0); transform: translateZ(0);
} }
@ -1725,42 +1759,152 @@ const handleExpenseCreated = (expense: any) => {
/* @keyframes dot-pulse { ... } */ /* @keyframes dot-pulse { ... } */
/* Utility classes that might still be used or can be replaced by Tailwind/global equivalents */ /* Utility classes that might still be used or can be replaced by Tailwind/global equivalents */
.flex { display: flex; } .flex {
.items-center { align-items: center; } display: flex;
.justify-between { justify-content: space-between; } }
.gap-1 { gap: 0.25rem; }
.gap-2 { gap: 0.5rem; } .items-center {
.ml-1 { margin-left: 0.25rem; } align-items: center;
.ml-2 { margin-left: 0.5rem; } }
.mt-1 { margin-top: 0.25rem; }
.mt-2 { margin-top: 0.5rem; } .justify-between {
.mt-4 { margin-top: 1rem; } justify-content: space-between;
.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; } .gap-1 {
.py-10 { padding-top: 2.5rem; padding-bottom: 2.5rem; } gap: 0.25rem;
.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 */ } .gap-2 {
.rounded-lg { border-radius: 0.5rem; } gap: 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 */ .ml-1 {
.text-sm { font-size: 0.875rem; } margin-left: 0.25rem;
.text-gray-500 { color: #6b7280; } /* Tailwind gray-500 */ }
.text-gray-400 { color: #9ca3af; } /* Tailwind gray-400 */
.text-green-600 { color: #16a34a; } /* Tailwind green-600 */ .ml-2 {
.text-yellow-500 { color: #eab308; } /* Tailwind yellow-500 */ margin-left: 0.5rem;
.line-through { text-decoration: line-through; } }
.opacity-50 { opacity: 0.5; }
.opacity-60 { opacity: 0.6; } /* Added for completed item name */ .mt-1 {
.opacity-70 { opacity: 0.7; } /* Added for completed item background */ margin-top: 0.25rem;
.shrink-0 { flex-shrink: 0; } }
.bg-gray-100 { background-color: #f3f4f6; } /* Tailwind gray-100 */
.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 */
/* Styles for .neo-list-card, .neo-item-list, .neo-item might be replaced by VCard/VList/VListItem defaults or props */ /* 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. */ /* 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 */ .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 */ /* Default VListItem is display:flex, so this might not be needed or just for minor tweaks */
} }
</style> </style>

View File

@ -41,11 +41,7 @@ const initializeSW = async () => {
// Use with precache injection // Use with precache injection
// vite-plugin-pwa will populate self.__WB_MANIFEST // vite-plugin-pwa will populate self.__WB_MANIFEST
if (self.__WB_MANIFEST) { precacheAndRoute(self.__WB_MANIFEST);
precacheAndRoute(self.__WB_MANIFEST);
} else {
console.warn('No manifest found for precaching');
}
cleanupOutdatedCaches(); cleanupOutdatedCaches();