Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
43 changes: 43 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ <h1>📋 Task Manager</h1>
<button class="nav-tab" data-tab="projects">📁 Projects</button>
<button class="nav-tab" data-tab="habits">⭐ Habits</button>
<button class="nav-tab" data-tab="wishlist">🎁 Wish List</button>
<button class="nav-tab" data-tab="shopping">🛒 Shopping</button>
<button class="nav-tab" data-tab="notes">📝 Notes</button>
<button class="nav-tab" data-tab="finances">💰 Finances</button>
<button class="nav-tab" data-tab="settings">⚙️ Settings</button>
Expand Down Expand Up @@ -631,6 +632,48 @@ <h3 id="noteModalTitle">Add Note</h3>
</div>
</section>

<!-- Shopping List Tab -->
<section class="tab-content" id="shopping-tab">
<div class="shopping-container">
<div class="shopping-header">
<h2>🛒 Shopping List</h2>
<div class="shopping-header-actions">
<button class="btn btn-secondary" id="clearCompletedShoppingBtn">Clear Checked</button>
<button class="btn btn-primary" id="addShoppingItemBtn">+ Add Item</button>
</div>
</div>

<div class="shopping-list" id="shoppingList">
<p class="empty-state">No items in your shopping list. Add one to get started!</p>
</div>
</div>

<!-- Shopping Item Modal -->
<div class="modal" id="shoppingItemModal">
<div class="modal-content">
<div class="modal-header">
<h3 id="shoppingItemModalTitle">Add Shopping Item</h3>
<button class="close-btn">&times;</button>
</div>
<form id="shoppingItemForm">
<div class="form-group">
<label for="shoppingItemName">Item Name *</label>
<input type="text" id="shoppingItemName" required placeholder="e.g. Milk">
</div>
<div class="form-group">
<label for="shoppingItemQuantity">Quantity (optional)</label>
<input type="text" id="shoppingItemQuantity" placeholder="e.g. 2 litres">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" id="deleteShoppingItemBtn" style="display: none;">Delete</button>
<button type="button" class="btn btn-secondary" id="cancelShoppingItemBtn">Cancel</button>
</div>
</form>
</div>
</div>
</section>

<!-- Settings Tab -->
<section class="tab-content" id="settings-tab">
<div class="settings-container">
Expand Down
115 changes: 114 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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;
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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 = '<p class="empty-state">No items in your shopping list. Add one to get started!</p>';
return;
}

container.innerHTML = items.map(item => this.renderShoppingItem(item)).join('');

container.querySelectorAll<HTMLElement>('.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
? `<div class="shopping-item-quantity">${this.escapeHtml(item.quantity)}</div>`
: '';
return `
<div class="shopping-item${completedClass}" data-shopping-id="${item.id}">
<input type="checkbox" class="shopping-item-checkbox" title="Mark as collected"${checkedAttr}>
<div class="shopping-item-content">
<div class="shopping-item-name">${this.escapeHtml(item.name)}</div>
${quantityHtml}
</div>
<button class="btn btn-secondary edit-shopping-btn">Edit</button>
</div>
`;
}

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
// ========================
Expand Down
62 changes: 61 additions & 1 deletion src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -115,6 +123,7 @@ export interface AppData {
settings: Settings;
wishList: WishItem[];
notes: Note[];
shoppingList: ShoppingItem[];
}

export interface ValidationResult {
Expand Down Expand Up @@ -168,7 +177,8 @@ export class StorageManager {
tasksPerLevel: 30
},
wishList: [],
notes: []
notes: [],
shoppingList: []
};

localStorage.setItem(STORAGE_KEY, JSON.stringify(initialData));
Expand Down Expand Up @@ -594,6 +604,55 @@ export class StorageManager {
);
}

// Shopping List Management
addShoppingItem(item: Partial<ShoppingItem>): 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>): 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();
Expand Down Expand Up @@ -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 : [],
};
}

Expand Down
Loading
Loading