diff --git a/css/styles.css b/css/styles.css
index 547dba3..4fa700d 100644
--- a/css/styles.css
+++ b/css/styles.css
@@ -1603,12 +1603,50 @@ html, body {
margin-bottom: 1.5rem;
}
-.wish-list {
+.wishlist-header-actions {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.wish-list-group {
+ margin-bottom: 1.5rem;
+ border: 1px solid var(--border-color, #e0e0e0);
+ border-radius: 10px;
+ overflow: hidden;
+}
+
+.wish-list-group-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.75rem 1rem;
+ background: var(--bg-secondary, #f5f5f5);
+ border-bottom: 1px solid var(--border-color, #e0e0e0);
+}
+
+.wish-list-group-name {
+ font-weight: 700;
+ font-size: 1rem;
+ color: var(--text-primary, #333);
+}
+
+.wish-list-group-actions {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.wish-list-group .wish-list {
+ padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
+.wish-list-empty {
+ margin: 0;
+ padding: 0.5rem 0;
+}
+
.wish-item {
display: flex;
align-items: center;
diff --git a/index.html b/index.html
index 43929ca..0284dce 100644
--- a/index.html
+++ b/index.html
@@ -561,11 +561,35 @@
Add Finance Item
-
-
No items in your wish list. Add one to get started!
+
+
No items in your wish list. Add a list or item to get started!
+
+
+
+
+
@@ -581,6 +605,12 @@
Add Wish List Item
+
+
+
+
diff --git a/src/app.ts b/src/app.ts
index a2b350f..a112a84 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, ShoppingItem, getDaysUntilDueText } from './storage.js';
+import { StorageManager, storage, STORAGE_VERSION, Task, Habit, FinanceItem, WishItem, WishList, Note, ShoppingItem, getDaysUntilDueText } from './storage.js';
const FILTER_SETTINGS_KEY = 'taskManagerFilterSettings';
@@ -24,6 +24,7 @@ class TaskManager {
currentEditingFinanceId: string | null = null;
currentEditingFinanceType: string | null = null;
currentEditingWishItemId: string | null = null;
+ currentEditingWishListId: string | null = null;
currentEditingNoteId: string | null = null;
currentEditingShoppingItemId: string | null = null;
dragSrcWishId: string | null = null;
@@ -133,9 +134,13 @@ class TaskManager {
// Wish List section
document.getElementById('addWishItemBtn')!.addEventListener('click', () => this.openWishItemModal());
+ document.getElementById('addWishListBtn')!.addEventListener('click', () => this.openWishListModal());
document.getElementById('wishItemForm')!.addEventListener('submit', (e) => this.saveWishItem(e));
document.getElementById('cancelWishItemBtn')!.addEventListener('click', () => this.closeWishItemModal());
document.getElementById('deleteWishItemBtn')!.addEventListener('click', () => this.deleteWishItem());
+ document.getElementById('wishListForm')!.addEventListener('submit', (e) => this.saveWishList(e));
+ document.getElementById('cancelWishListBtn')!.addEventListener('click', () => this.closeWishListModal());
+ document.getElementById('deleteWishListBtn')!.addEventListener('click', () => this.deleteWishListFromModal());
// Shopping List section
document.getElementById('addShoppingItemBtn')!.addEventListener('click', () => this.openShoppingItemModal());
@@ -1870,16 +1875,85 @@ class TaskManager {
// Wish List
// ========================
renderWishList(): void {
- const items = storage.getWishItems();
- const container = document.getElementById('wishList')!;
+ const lists = storage.getWishLists();
+ const allItems = storage.getWishItems();
+ const container = document.getElementById('wishListContent')!;
- if (items.length === 0) {
- container.innerHTML = '
No items in your wish list. Add one to get started!
';
+ if (lists.length === 0 && allItems.length === 0) {
+ container.innerHTML = '
No items in your wish list. Add a list or item to get started!
';
return;
}
- container.innerHTML = items.map(item => this.renderWishItem(item)).join('');
+ let html = '';
+
+ // Render each named list group
+ lists.forEach(list => {
+ const listItems = allItems.filter(i => i.listId === list.id);
+ html += this.renderWishListGroup(list.id, list.name, listItems, true);
+ });
+
+ // Render uncategorized items (no listId or listId is null)
+ const uncategorized = allItems.filter(i => !i.listId);
+ if (uncategorized.length > 0 || lists.length === 0) {
+ html += this.renderWishListGroup(null, 'Uncategorized', uncategorized, false);
+ }
+
+ container.innerHTML = html;
+
+ // Set up drag-drop for each named list group
+ lists.forEach(list => {
+ const groupEl = container.querySelector
(`.wish-list-group[data-list-id="${list.id}"]`);
+ if (groupEl) {
+ const itemsContainer = groupEl.querySelector('.wish-list')!;
+ this.setupWishItemDragDrop(itemsContainer, list.id);
+ const addBtn = groupEl.querySelector('.wish-list-group-add-btn');
+ if (addBtn) {
+ addBtn.addEventListener('click', () => this.openWishItemModal(null, list.id));
+ }
+ const editBtn = groupEl.querySelector('.wish-list-edit-btn');
+ if (editBtn) {
+ editBtn.addEventListener('click', () => this.openWishListModal(list.id));
+ }
+ }
+ });
+
+ // Set up drag-drop for uncategorized section
+ const uncategorizedGroup = container.querySelector('.wish-list-group[data-list-id="uncategorized"]');
+ if (uncategorizedGroup) {
+ const itemsContainer = uncategorizedGroup.querySelector('.wish-list')!;
+ this.setupWishItemDragDrop(itemsContainer, null);
+ const addBtn = uncategorizedGroup.querySelector('.wish-list-group-add-btn');
+ if (addBtn) {
+ addBtn.addEventListener('click', () => this.openWishItemModal(null, null));
+ }
+ }
+ }
+
+ renderWishListGroup(listId: string | null, name: string, items: WishItem[], hasEditBtn: boolean): string {
+ const dataAttr = listId ? `data-list-id="${listId}"` : 'data-list-id="uncategorized"';
+ const editBtnHtml = hasEditBtn
+ ? ``
+ : '';
+ const itemsHtml = items.length > 0
+ ? items.map(item => this.renderWishItem(item)).join('')
+ : `No items yet. Click "+ Add" to add one.
`;
+ return `
+
+ `;
+ }
+ setupWishItemDragDrop(container: HTMLElement, listId: string | null): void {
container.querySelectorAll('.wish-item').forEach(el => {
const handle = el.querySelector('.wish-drag-handle');
if (!handle) return;
@@ -1916,11 +1990,13 @@ class TaskManager {
e.preventDefault();
const targetId = el.dataset.wishId!;
if (this.dragSrcWishId && this.dragSrcWishId !== targetId) {
- const allItems = storage.getWishItems();
- const srcIdx = allItems.findIndex(i => i.id === this.dragSrcWishId);
- const tgtIdx = allItems.findIndex(i => i.id === targetId);
+ const groupItems = storage.getWishItems().filter(i =>
+ listId ? i.listId === listId : !i.listId
+ );
+ const srcIdx = groupItems.findIndex(i => i.id === this.dragSrcWishId);
+ const tgtIdx = groupItems.findIndex(i => i.id === targetId);
if (srcIdx !== -1 && tgtIdx !== -1) {
- const reordered = [...allItems];
+ const reordered = [...groupItems];
const [moved] = reordered.splice(srcIdx, 1);
reordered.splice(tgtIdx, 0, moved);
storage.reorderWishItems(reordered.map(i => i.id));
@@ -1981,11 +2057,13 @@ class TaskManager {
touchDragOverItem = null;
touchDragActive = false;
if (this.dragSrcWishId && targetId && this.dragSrcWishId !== targetId) {
- const allItems = storage.getWishItems();
- const srcIdx = allItems.findIndex(i => i.id === this.dragSrcWishId);
- const tgtIdx = allItems.findIndex(i => i.id === targetId);
+ const groupItems = storage.getWishItems().filter(i =>
+ listId ? i.listId === listId : !i.listId
+ );
+ const srcIdx = groupItems.findIndex(i => i.id === this.dragSrcWishId);
+ const tgtIdx = groupItems.findIndex(i => i.id === targetId);
if (srcIdx !== -1 && tgtIdx !== -1) {
- const reordered = [...allItems];
+ const reordered = [...groupItems];
const [moved] = reordered.splice(srcIdx, 1);
reordered.splice(tgtIdx, 0, moved);
storage.reorderWishItems(reordered.map(i => i.id));
@@ -2034,15 +2112,21 @@ class TaskManager {
`;
}
- openWishItemModal(itemId: string | null = null): void {
+ openWishItemModal(itemId: string | null = null, preselectedListId: string | null = null): void {
this.currentEditingWishItemId = itemId;
const modal = document.getElementById('wishItemModal')!;
const form = document.getElementById('wishItemForm') as HTMLFormElement;
const deleteBtn = document.getElementById('deleteWishItemBtn') as HTMLElement;
+ const listSelect = document.getElementById('wishItemList') as HTMLSelectElement;
form.reset();
deleteBtn.style.display = 'none';
+ // Populate list selector
+ const lists = storage.getWishLists();
+ listSelect.innerHTML = '' +
+ lists.map(l => ``).join('');
+
document.getElementById('wishItemModalTitle')!.textContent = itemId ? 'Edit Wish List Item' : 'Add Wish List Item';
if (itemId) {
@@ -2052,8 +2136,11 @@ class TaskManager {
(document.getElementById('wishItemUrl') as HTMLInputElement).value = item.url || '';
(document.getElementById('wishItemPrice') as HTMLInputElement).value =
item.price !== undefined && item.price !== null ? String(item.price) : '';
+ listSelect.value = item.listId || '';
deleteBtn.style.display = 'block';
}
+ } else if (preselectedListId) {
+ listSelect.value = preselectedListId;
}
modal.classList.add('active');
@@ -2071,8 +2158,10 @@ class TaskManager {
const url = (document.getElementById('wishItemUrl') as HTMLInputElement).value.trim() || undefined;
const priceVal = (document.getElementById('wishItemPrice') as HTMLInputElement).value;
const price = priceVal !== '' ? parseFloat(priceVal) : undefined;
+ const listIdVal = (document.getElementById('wishItemList') as HTMLSelectElement).value;
+ const listId = listIdVal || null;
- const item = { title, url, price };
+ const item = { title, url, price, listId };
if (this.currentEditingWishItemId) {
storage.updateWishItem(this.currentEditingWishItemId, item);
@@ -2094,6 +2183,58 @@ class TaskManager {
}
}
+ openWishListModal(listId: string | null = null): void {
+ this.currentEditingWishListId = listId;
+ const modal = document.getElementById('wishListModal')!;
+ const form = document.getElementById('wishListForm') as HTMLFormElement;
+ const deleteBtn = document.getElementById('deleteWishListBtn') as HTMLElement;
+
+ form.reset();
+ deleteBtn.style.display = 'none';
+
+ document.getElementById('wishListModalTitle')!.textContent = listId ? 'Edit List' : 'Add List';
+
+ if (listId) {
+ const list = storage.getWishLists().find(l => l.id === listId);
+ if (list) {
+ (document.getElementById('wishListName') as HTMLInputElement).value = list.name;
+ deleteBtn.style.display = 'block';
+ }
+ }
+
+ modal.classList.add('active');
+ }
+
+ closeWishListModal(): void {
+ document.getElementById('wishListModal')!.classList.remove('active');
+ this.currentEditingWishListId = null;
+ }
+
+ saveWishList(e: Event): void {
+ e.preventDefault();
+ const name = (document.getElementById('wishListName') as HTMLInputElement).value.trim();
+ if (!name) return;
+
+ if (this.currentEditingWishListId) {
+ storage.updateWishList(this.currentEditingWishListId, { name });
+ } else {
+ storage.addWishList({ name });
+ }
+
+ this.closeWishListModal();
+ this.renderWishList();
+ }
+
+ deleteWishListFromModal(): void {
+ if (this.currentEditingWishListId) {
+ if (confirm('Are you sure you want to delete this list? Items in this list will become uncategorized.')) {
+ storage.deleteWishList(this.currentEditingWishListId);
+ this.closeWishListModal();
+ this.renderWishList();
+ }
+ }
+ }
+
// ========================
// Shopping List
// ========================
diff --git a/src/storage.ts b/src/storage.ts
index 96d30eb..450fa81 100644
--- a/src/storage.ts
+++ b/src/storage.ts
@@ -4,7 +4,7 @@
const STORAGE_VERSION = '1.0.0';
const STORAGE_KEY = 'taskManagerData';
-const DATA_SCHEMA_VERSION = 2;
+const DATA_SCHEMA_VERSION = 3;
// ========================
// Type Definitions
@@ -74,6 +74,14 @@ export interface WishItem {
order: number;
createdDate: string;
completed?: boolean;
+ listId?: string | null;
+}
+
+export interface WishList {
+ id: string;
+ name: string;
+ order: number;
+ createdDate: string;
}
export interface Note {
@@ -122,6 +130,7 @@ export interface AppData {
userStats: UserStats;
settings: Settings;
wishList: WishItem[];
+ wishLists: WishList[];
notes: Note[];
shoppingList: ShoppingItem[];
}
@@ -177,6 +186,7 @@ export class StorageManager {
tasksPerLevel: 30
},
wishList: [],
+ wishLists: [],
notes: [],
shoppingList: []
};
@@ -562,6 +572,63 @@ export class StorageManager {
this.saveData(data);
}
+ // Named Wish Lists Management
+ getWishLists(): WishList[] {
+ const data = this.getData();
+ if (!data.wishLists) return [];
+ return data.wishLists.slice().sort((a, b) => a.order - b.order);
+ }
+
+ addWishList(list: Partial): WishList {
+ const data = this.getData();
+ if (!data.wishLists) data.wishLists = [];
+ const newList: WishList = {
+ id: this.generateId(),
+ name: list.name || '',
+ order: data.wishLists.length,
+ createdDate: new Date().toISOString(),
+ };
+ data.wishLists.push(newList);
+ this.saveData(data);
+ return newList;
+ }
+
+ updateWishList(listId: string, updates: Partial): WishList | undefined {
+ const data = this.getData();
+ if (!data.wishLists) data.wishLists = [];
+ const list = data.wishLists.find(l => l.id === listId);
+ if (list) {
+ Object.assign(list, updates);
+ this.saveData(data);
+ }
+ return list;
+ }
+
+ deleteWishList(listId: string): void {
+ const data = this.getData();
+ if (!data.wishLists) data.wishLists = [];
+ data.wishLists = data.wishLists.filter(l => l.id !== listId);
+ // Re-index order values
+ data.wishLists.forEach((l, idx) => { l.order = idx; });
+ // Move items in the deleted list to uncategorized
+ if (data.wishList) {
+ data.wishList = data.wishList.map(item =>
+ item.listId === listId ? { ...item, listId: null } : item
+ );
+ }
+ this.saveData(data);
+ }
+
+ reorderWishLists(orderedIds: string[]): void {
+ const data = this.getData();
+ if (!data.wishLists) return;
+ orderedIds.forEach((id, idx) => {
+ const list = data.wishLists.find(l => l.id === id);
+ if (list) list.order = idx;
+ });
+ this.saveData(data);
+ }
+
// Note Management
addNote(note: Partial): Note {
const data = this.getData();
@@ -778,6 +845,7 @@ export class StorageManager {
},
settings: data.settings || { tasksPerLevel: 30 },
wishList: Array.isArray(data.wishList) ? data.wishList : [],
+ wishLists: Array.isArray(data.wishLists) ? data.wishLists : [],
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 6b931ee..b8c2567 100644
--- a/tests/storage.test.ts
+++ b/tests/storage.test.ts
@@ -665,6 +665,89 @@ describe('StorageManager', () => {
});
});
+ // ========================
+ // Named Wish Lists Management
+ // ========================
+ describe('named wish lists management', () => {
+ it('should add a wish list', () => {
+ const list = storage.addWishList({ name: 'Electronics' });
+ expect(list.id).toBeDefined();
+ expect(list.name).toBe('Electronics');
+ expect(list.order).toBe(0);
+ expect(list.createdDate).toBeDefined();
+ });
+
+ it('should get all wish lists sorted by order', () => {
+ storage.addWishList({ name: 'List A' });
+ storage.addWishList({ name: 'List B' });
+ storage.addWishList({ name: 'List C' });
+ const lists = storage.getWishLists();
+ expect(lists.length).toBe(3);
+ expect(lists[0].order).toBeLessThanOrEqual(lists[1].order);
+ expect(lists[1].order).toBeLessThanOrEqual(lists[2].order);
+ });
+
+ it('should update a wish list', () => {
+ const list = storage.addWishList({ name: 'Old Name' });
+ const updated = storage.updateWishList(list.id, { name: 'New Name' });
+ expect(updated?.name).toBe('New Name');
+ });
+
+ it('should return undefined when updating non-existent wish list', () => {
+ const result = storage.updateWishList('nonexistent', { name: 'Test' });
+ expect(result).toBeUndefined();
+ });
+
+ it('should delete a wish list and re-index order', () => {
+ storage.addWishList({ name: 'List A' });
+ const listB = storage.addWishList({ name: 'List B' });
+ storage.addWishList({ name: 'List C' });
+ storage.deleteWishList(listB.id);
+ const lists = storage.getWishLists();
+ expect(lists.length).toBe(2);
+ expect(lists[0].order).toBe(0);
+ expect(lists[1].order).toBe(1);
+ });
+
+ it('should move items to uncategorized when their list is deleted', () => {
+ const list = storage.addWishList({ name: 'To Delete' });
+ const item = storage.addWishItem({ title: 'Item in list', listId: list.id });
+ storage.deleteWishList(list.id);
+ const items = storage.getWishItems();
+ const found = items.find(i => i.id === item.id);
+ expect(found).toBeDefined();
+ expect(found?.listId).toBeNull();
+ });
+
+ it('should reorder wish lists', () => {
+ const a = storage.addWishList({ name: 'List A' });
+ const b = storage.addWishList({ name: 'List B' });
+ const c = storage.addWishList({ name: 'List C' });
+ storage.reorderWishLists([c.id, a.id, b.id]);
+ const lists = storage.getWishLists();
+ expect(lists[0].name).toBe('List C');
+ expect(lists[1].name).toBe('List A');
+ expect(lists[2].name).toBe('List B');
+ });
+
+ it('should associate a wish item with a list', () => {
+ const list = storage.addWishList({ name: 'Tech' });
+ const item = storage.addWishItem({ title: 'Laptop', listId: list.id });
+ expect(item.listId).toBe(list.id);
+ const items = storage.getWishItems();
+ const found = items.find(i => i.id === item.id);
+ expect(found?.listId).toBe(list.id);
+ });
+
+ it('should update listId on a wish item', () => {
+ const list = storage.addWishList({ name: 'Tech' });
+ const item = storage.addWishItem({ title: 'Laptop' });
+ expect(item.listId).toBeUndefined();
+ const updated = storage.updateWishItem(item.id, { listId: list.id });
+ expect(updated?.listId).toBe(list.id);
+ });
+ });
+
// ========================
// Clear All Data
// ========================