From 92708f3039bf2de6d88bb9b0373a65cb50cd6eda Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 22:59:16 +0000 Subject: [PATCH 1/2] Initial plan From d8c0baaa9d71d7d58e8cd3a08183a51d4825352e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 23:16:40 +0000 Subject: [PATCH 2/2] Changes before error encountered Co-authored-by: JoeProgrammer88 <7156063+JoeProgrammer88@users.noreply.github.com> Agent-Logs-Url: https://github.com/SpeakingInBits/TaskManagerWeb/sessions/edd3d4a1-2773-45ab-8b8c-ee3756ea0eed --- css/styles.css | 91 +++++++++++++++++++++++++++++++++ index.html | 43 ++++++++++++++++ src/app.ts | 115 +++++++++++++++++++++++++++++++++++++++++- src/storage.ts | 62 ++++++++++++++++++++++- tests/storage.test.ts | 75 +++++++++++++++++++++++++++ 5 files changed, 384 insertions(+), 2 deletions(-) diff --git a/css/styles.css b/css/styles.css index cec25fd..0caad20 100644 --- a/css/styles.css +++ b/css/styles.css @@ -1677,3 +1677,94 @@ html, body { font-family: inherit; font-size: 0.95rem; } + +/* ======================== + Shopping List + ======================== */ +.shopping-container { + max-width: 800px; + margin: 0 auto; +} + +.shopping-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.shopping-header-actions { + display: flex; + gap: 0.5rem; +} + +.shopping-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.shopping-item { + display: flex; + align-items: center; + gap: 0.75rem; + background: #fff; + border: 1px solid var(--border-color, #e0e0e0); + border-radius: 8px; + padding: 0.75rem 1rem; + transition: box-shadow 0.2s, opacity 0.2s; +} + +.shopping-item:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.shopping-item-checkbox { + width: 1.1rem; + height: 1.1rem; + flex-shrink: 0; + cursor: pointer; + accent-color: var(--primary-color, #4CAF50); +} + +.shopping-item-content { + flex: 1; + min-width: 0; +} + +.shopping-item-name { + font-weight: 600; + font-size: 1rem; + word-break: break-word; +} + +.shopping-item-quantity { + font-size: 0.875rem; + color: #666; + margin-top: 0.1rem; +} + +.shopping-item.completed .shopping-item-name { + text-decoration: line-through; + color: #aaa; +} + +.shopping-item.completed { + opacity: 0.7; +} + +@media (max-width: 480px) { + .shopping-header { + flex-direction: column; + align-items: flex-start; + gap: 0.75rem; + } + + .shopping-header-actions { + width: 100%; + } + + .shopping-header-actions .btn { + flex: 1; + } +} diff --git a/index.html b/index.html index 0e31231..a27f6b6 100644 --- a/index.html +++ b/index.html @@ -61,6 +61,7 @@

📋 Task Manager

+ @@ -631,6 +632,48 @@

Add Note

+ +
+
+
+

🛒 Shopping List

+
+ + +
+
+ +
+

No items in your shopping list. Add one to get started!

+
+
+ + + +
+
diff --git a/src/app.ts b/src/app.ts index 18ac418..ea27cd0 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,7 +2,7 @@ // Main Application Logic // ======================== -import { StorageManager, storage, STORAGE_VERSION, Task, Habit, FinanceItem, WishItem, Note, getDaysUntilDueText } from './storage.js'; +import { StorageManager, storage, STORAGE_VERSION, Task, Habit, FinanceItem, WishItem, Note, ShoppingItem, getDaysUntilDueText } from './storage.js'; const FILTER_SETTINGS_KEY = 'taskManagerFilterSettings'; @@ -25,6 +25,7 @@ class TaskManager { currentEditingFinanceType: string | null = null; currentEditingWishItemId: string | null = null; currentEditingNoteId: string | null = null; + currentEditingShoppingItemId: string | null = null; dragSrcWishId: string | null = null; selectedDate: Date = new Date(); tasksExpanded: boolean = false; @@ -137,6 +138,13 @@ class TaskManager { document.getElementById('cancelWishItemBtn')!.addEventListener('click', () => this.closeWishItemModal()); document.getElementById('deleteWishItemBtn')!.addEventListener('click', () => this.deleteWishItem()); + // Shopping List section + document.getElementById('addShoppingItemBtn')!.addEventListener('click', () => this.openShoppingItemModal()); + document.getElementById('shoppingItemForm')!.addEventListener('submit', (e) => this.saveShoppingItem(e)); + document.getElementById('cancelShoppingItemBtn')!.addEventListener('click', () => this.closeShoppingItemModal()); + document.getElementById('deleteShoppingItemBtn')!.addEventListener('click', () => this.deleteShoppingItem()); + document.getElementById('clearCompletedShoppingBtn')!.addEventListener('click', () => this.clearCompletedShoppingItems()); + // Notes section document.getElementById('addNoteBtn')!.addEventListener('click', () => this.openNoteModal()); document.getElementById('noteForm')!.addEventListener('submit', (e) => this.saveNote(e)); @@ -257,6 +265,8 @@ class TaskManager { this.renderFinances(); } else if (tabName === 'wishlist') { this.renderWishList(); + } else if (tabName === 'shopping') { + this.renderShoppingList(); } else if (tabName === 'notes') { this.renderNotes(); } else if (tabName === 'settings') { @@ -1952,6 +1962,109 @@ class TaskManager { } } + // ======================== + // Shopping List + // ======================== + renderShoppingList(): void { + const items = storage.getShoppingItems(); + const container = document.getElementById('shoppingList')!; + + if (items.length === 0) { + container.innerHTML = '

No items in your shopping list. Add one to get started!

'; + return; + } + + container.innerHTML = items.map(item => this.renderShoppingItem(item)).join(''); + + container.querySelectorAll('.shopping-item').forEach(el => { + el.querySelector('.shopping-item-checkbox')!.addEventListener('change', (e) => { + const checkbox = e.target as HTMLInputElement; + storage.updateShoppingItem(el.dataset.shoppingId!, { completed: checkbox.checked }); + this.renderShoppingList(); + }); + + el.querySelector('.edit-shopping-btn')!.addEventListener('click', () => { + this.openShoppingItemModal(el.dataset.shoppingId!); + }); + }); + } + + renderShoppingItem(item: ShoppingItem): string { + const completedClass = item.completed ? ' completed' : ''; + const checkedAttr = item.completed ? ' checked' : ''; + const quantityHtml = item.quantity + ? `
${this.escapeHtml(item.quantity)}
` + : ''; + return ` +
+ +
+
${this.escapeHtml(item.name)}
+ ${quantityHtml} +
+ +
+ `; + } + + openShoppingItemModal(itemId: string | null = null): void { + this.currentEditingShoppingItemId = itemId; + const modal = document.getElementById('shoppingItemModal')!; + const form = document.getElementById('shoppingItemForm') as HTMLFormElement; + const deleteBtn = document.getElementById('deleteShoppingItemBtn') as HTMLElement; + + form.reset(); + deleteBtn.style.display = 'none'; + + document.getElementById('shoppingItemModalTitle')!.textContent = itemId ? 'Edit Shopping Item' : 'Add Shopping Item'; + + if (itemId) { + const item = storage.getShoppingItems().find(s => s.id === itemId); + if (item) { + (document.getElementById('shoppingItemName') as HTMLInputElement).value = item.name; + (document.getElementById('shoppingItemQuantity') as HTMLInputElement).value = item.quantity || ''; + deleteBtn.style.display = 'inline-block'; + } + } + + modal.classList.add('active'); + } + + closeShoppingItemModal(): void { + document.getElementById('shoppingItemModal')!.classList.remove('active'); + this.currentEditingShoppingItemId = null; + } + + saveShoppingItem(e: Event): void { + e.preventDefault(); + const name = (document.getElementById('shoppingItemName') as HTMLInputElement).value.trim(); + const quantity = (document.getElementById('shoppingItemQuantity') as HTMLInputElement).value.trim() || undefined; + + if (this.currentEditingShoppingItemId) { + storage.updateShoppingItem(this.currentEditingShoppingItemId, { name, quantity }); + } else { + storage.addShoppingItem({ name, quantity }); + } + + this.closeShoppingItemModal(); + this.renderShoppingList(); + } + + deleteShoppingItem(): void { + if (this.currentEditingShoppingItemId) { + if (confirm('Are you sure you want to delete this shopping item?')) { + storage.deleteShoppingItem(this.currentEditingShoppingItemId); + this.closeShoppingItemModal(); + this.renderShoppingList(); + } + } + } + + clearCompletedShoppingItems(): void { + storage.clearCompletedShoppingItems(); + this.renderShoppingList(); + } + // ======================== // Notes // ======================== diff --git a/src/storage.ts b/src/storage.ts index ef0fa53..96d30eb 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -84,6 +84,14 @@ export interface Note { updatedDate?: string; } +export interface ShoppingItem { + id: string; + name: string; + quantity?: string; + completed: boolean; + createdDate: string; +} + export interface UserStats { level: number; dailyStreak: number; @@ -115,6 +123,7 @@ export interface AppData { settings: Settings; wishList: WishItem[]; notes: Note[]; + shoppingList: ShoppingItem[]; } export interface ValidationResult { @@ -168,7 +177,8 @@ export class StorageManager { tasksPerLevel: 30 }, wishList: [], - notes: [] + notes: [], + shoppingList: [] }; localStorage.setItem(STORAGE_KEY, JSON.stringify(initialData)); @@ -594,6 +604,55 @@ export class StorageManager { ); } + // Shopping List Management + addShoppingItem(item: Partial): ShoppingItem { + const data = this.getData(); + if (!data.shoppingList) data.shoppingList = []; + const newItem: ShoppingItem = { + id: this.generateId(), + name: item.name || '', + quantity: item.quantity, + completed: false, + createdDate: new Date().toISOString(), + }; + data.shoppingList.push(newItem); + this.saveData(data); + return newItem; + } + + updateShoppingItem(itemId: string, updates: Partial): ShoppingItem | undefined { + const data = this.getData(); + if (!data.shoppingList) data.shoppingList = []; + const item = data.shoppingList.find(s => s.id === itemId); + if (item) { + Object.assign(item, updates); + this.saveData(data); + } + return item; + } + + deleteShoppingItem(itemId: string): void { + const data = this.getData(); + if (!data.shoppingList) data.shoppingList = []; + data.shoppingList = data.shoppingList.filter(s => s.id !== itemId); + this.saveData(data); + } + + getShoppingItems(): ShoppingItem[] { + const data = this.getData(); + if (!data.shoppingList) return []; + return data.shoppingList.slice().sort((a, b) => + new Date(a.createdDate).getTime() - new Date(b.createdDate).getTime() + ); + } + + clearCompletedShoppingItems(): void { + const data = this.getData(); + if (!data.shoppingList) return; + data.shoppingList = data.shoppingList.filter(s => !s.completed); + this.saveData(data); + } + updateLevel(): void { const data = this.getData(); const settings = this.getSettings(); @@ -720,6 +779,7 @@ export class StorageManager { settings: data.settings || { tasksPerLevel: 30 }, wishList: Array.isArray(data.wishList) ? data.wishList : [], notes: Array.isArray(data.notes) ? data.notes : [], + shoppingList: Array.isArray(data.shoppingList) ? data.shoppingList : [], }; } diff --git a/tests/storage.test.ts b/tests/storage.test.ts index 3681e0c..3bd884f 100644 --- a/tests/storage.test.ts +++ b/tests/storage.test.ts @@ -748,6 +748,81 @@ describe('StorageManager', () => { expect(storage.getNotes()).toEqual([]); }); }); + + // ======================== + // Shopping List Management + // ======================== + describe('shopping list management', () => { + it('should add a shopping item', () => { + const item = storage.addShoppingItem({ name: 'Milk', quantity: '2 litres' }); + expect(item.id).toBeDefined(); + expect(item.name).toBe('Milk'); + expect(item.quantity).toBe('2 litres'); + expect(item.completed).toBe(false); + expect(item.createdDate).toBeDefined(); + }); + + it('should add a shopping item with only name', () => { + const item = storage.addShoppingItem({ name: 'Bread' }); + expect(item.name).toBe('Bread'); + expect(item.quantity).toBeUndefined(); + }); + + it('should set default name to empty string when not provided', () => { + const item = storage.addShoppingItem({}); + expect(item.name).toBe(''); + expect(item.completed).toBe(false); + }); + + it('should get all shopping items sorted by creation date', () => { + storage.addShoppingItem({ name: 'Item A' }); + storage.addShoppingItem({ name: 'Item B' }); + storage.addShoppingItem({ name: 'Item C' }); + const items = storage.getShoppingItems(); + expect(items.length).toBe(3); + }); + + it('should update a shopping item', () => { + const item = storage.addShoppingItem({ name: 'Old Name', quantity: '1' }); + const updated = storage.updateShoppingItem(item.id, { name: 'New Name', quantity: '3' }); + expect(updated?.name).toBe('New Name'); + expect(updated?.quantity).toBe('3'); + }); + + it('should return undefined when updating non-existent shopping item', () => { + const result = storage.updateShoppingItem('nonexistent', { name: 'Test' }); + expect(result).toBeUndefined(); + }); + + it('should toggle completed on a shopping item', () => { + const item = storage.addShoppingItem({ name: 'Eggs' }); + expect(item.completed).toBe(false); + const checked = storage.updateShoppingItem(item.id, { completed: true }); + expect(checked?.completed).toBe(true); + const unchecked = storage.updateShoppingItem(item.id, { completed: false }); + expect(unchecked?.completed).toBe(false); + }); + + it('should delete a shopping item', () => { + const item = storage.addShoppingItem({ name: 'To delete' }); + storage.deleteShoppingItem(item.id); + expect(storage.getShoppingItems().length).toBe(0); + }); + + it('should clear only completed shopping items', () => { + storage.addShoppingItem({ name: 'Keep me' }); + const done = storage.addShoppingItem({ name: 'Done item' }); + storage.updateShoppingItem(done.id, { completed: true }); + storage.clearCompletedShoppingItems(); + const items = storage.getShoppingItems(); + expect(items.length).toBe(1); + expect(items[0].name).toBe('Keep me'); + }); + + it('should return empty array when no shopping items exist', () => { + expect(storage.getShoppingItems()).toEqual([]); + }); + }); }); // ========================