From 94d8166537bdda5db03e3b27a664252ca325d909 Mon Sep 17 00:00:00 2001 From: Zaid Date: Tue, 5 May 2026 15:01:53 +0800 Subject: [PATCH 01/27] init: instructions --- .gitignore | 5 ++- INSTRUCTION.md | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 INSTRUCTION.md diff --git a/.gitignore b/.gitignore index 6a7d6d8e..68eb1319 100644 --- a/.gitignore +++ b/.gitignore @@ -127,4 +127,7 @@ dist .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz -.pnp.* \ No newline at end of file +.pnp.* + +# Reviewer notes +ANSWERS.md \ No newline at end of file diff --git a/INSTRUCTION.md b/INSTRUCTION.md new file mode 100644 index 00000000..a8dc19bf --- /dev/null +++ b/INSTRUCTION.md @@ -0,0 +1,84 @@ +# 1-Week Sprint: Git, Docker, and a Real Codebase + +**Repo:** Fork of [`docker/getting-started-todo-app`](https://github.com/docker/getting-started-todo-app) +**Goal:** Ship 4 features through PR review. Then rebuild the Compose stack solo. + +--- + +## Setup + +- You're added as contributors. Clone the repo directly. +- `main` is protected: PR required, 1 reviewer, no direct pushes, no force-pushes. +- Verify `docker compose up --watch` works. Flag if not. + +--- + +## Start + +- 1: Priority levels (low/med/high). DB column, API field, colored badge. +- 2: Due dates. DB column, validation, date picker, overdue styling. +- 3: Categories (work/personal/shopping). Enum column, filter dropdown. +- 4: Search + sort by created/priority/due. No schema, heavy UI edits. + +You will all touch the same migration, API handler, and `TodoList` component. **Don't coordinate to avoid this.** Conflicts are the lesson. + +**Review, merge, conflict.** PRs go up. You review someone else's before yours merges. Merge order = ready order. After the first merge, everyone else rebases and resolves their own conflicts. Delete merged branches. + +**Rebuild the Compose stack solo.** New branch `compose/`. Rename the existing `Dockerfile` and `compose.yaml` to `Dockerfile.example` and `compose.example.yaml`. Rebuild from scratch. + +1-hour walkthrough, we cherry-pick the best ideas into a team `compose.yaml`. + +--- + +## Rules + +- No direct commits to `main`. No self-approving PRs. +- AI is encouraged but please try and use git and docker yourself. +- PR description: what changed, how to test, screenshot. +- Commit messages in imperative mood. "Add priority field," not "Added priority field." +- Clear and separated branches +- use feat/, fix/, etc branch conventions (https://conventional-branch.github.io/) + +--- + +## What I'll review on your Compose stack + +**Backend Dockerfile.** Multi-stage. Non-root. `npm install` shouldn't re-run on source-only changes. Healthcheck that proves the app is _up_, not just _alive_. + +**Frontend Dockerfile.** Build stage produces static assets. Serve stage is tiny. No `node_modules` in the final image. + +**compose.yaml.** Frontend, backend, db, phpMyAdmin. Backend doesn't start before the DB is _ready_. DB data survives `docker compose down`. No hardcoded secrets. + +**Dev ergonomics.** One command to a working app with hot reload. `.env.example` tells me what I need without leaking yours. + +--- + +## Deliverables (Friday EOD) + +1. Merged feature PR. +2. `compose/` branch. + +--- + +## The existing stack is your starting point, not your target + +The checked-in `compose.yaml` and `Dockerfile` work, but they fail parts of the rubric above on purpose. Keep them as `.example` files and read them. Your rebuild should beat them on at least: + +- Containers run as root. +- Backend has no healthcheck. Compose has no way to know it's actually serving. +- Secrets are inline in `compose.yaml`. There is no `.env.example`. +- One Dockerfile builds both client and backend, and the prod client is baked into the backend image. Split them. The frontend should build to static assets and be served by something that isn't Node. + +--- + +## Hints (not answers) + +- `git rebase` and `git merge` solve the same problem differently. Pick one. Know why. +- `git pull --rebase` saves you unnecessary merge commits. +- Layers that change often go _after_ layers that don't. +- `depends_on` alone doesn't wait for a DB to be ready. Look up `condition`. +- `COPY package*.json ./` before `COPY . .`. There's a reason. +- A static site doesn't need a Node runtime to be served. `nginx:alpine` is ~50MB. +- A container running as root is one `USER` line away from not being. +- `mysqladmin ping` proves MySQL is alive. What proves your API is _serving_? +- `env_file:` and `${VAR}` substitution exist for a reason. So does `.env.example`. From 97b0d09120f5e521e85832f07a0f6d693bf9fe3a Mon Sep 17 00:00:00 2001 From: ponli550 Date: Wed, 6 May 2026 12:02:43 +0800 Subject: [PATCH 02/27] new file for planning --- PRIORITY_FEATURE.md | 761 ++++++++++++++++++++++++++++++++++++++++++++ planirfan.md | 3 + 2 files changed, 764 insertions(+) create mode 100644 PRIORITY_FEATURE.md create mode 100644 planirfan.md diff --git a/PRIORITY_FEATURE.md b/PRIORITY_FEATURE.md new file mode 100644 index 00000000..5faf3eb5 --- /dev/null +++ b/PRIORITY_FEATURE.md @@ -0,0 +1,761 @@ +# Feature: Priority Levels (Low / Medium / High) + +**Feature summary:** Add a `priority` field to every todo item — stored in the database, returned by the API, and displayed as a colored badge in the UI. + +--- + +## Table of Contents + +1. [Overview & Data Flow](#1-overview--data-flow) +2. [Layer 1 — Database](#2-layer-1--database) +3. [Layer 2 — Backend Persistence](#3-layer-2--backend-persistence) +4. [Layer 3 — API Routes](#4-layer-3--api-routes) +5. [Layer 4 — Frontend Components](#5-layer-4--frontend-components) +6. [Layer 5 — Styling the Badge](#6-layer-5--styling-the-badge) +7. [Testing](#7-testing) +8. [End-to-End Walkthrough](#8-end-to-end-walkthrough) +9. [Quick Reference — All Changes at a Glance](#9-quick-reference--all-changes-at-a-glance) + +--- + +## 1. Overview & Data Flow + +``` +User selects priority in the form + │ + ▼ +AddNewItemForm.jsx ──POST /api/items──► addItem.js ──► storeItem() ──► DB +{ name, priority } validates INSERT SQL todo_items table + & stores (id, name, completed, priority) + +TodoListCard.jsx ──GET /api/items──► getItems.js ──► getItems() ──► DB +receives items[] returns rows SELECT SQL reads priority column + │ + ▼ +ItemDisplay.jsx +renders +``` + +**Allowed values:** `"low"` | `"medium"` | `"high"` +**Default value:** `"medium"` (applied when client sends nothing) + +--- + +## 2. Layer 1 — Database + +### What changes +Add a `priority` column to the existing `todo_items` table. + +### SQLite — `backend/src/persistence/sqlite.js` + +**Before:** +```javascript +await db.run( + 'CREATE TABLE IF NOT EXISTS todo_items (id varchar(36), name varchar(255), completed boolean)' +); +``` + +**After:** +```javascript +await db.run( + 'CREATE TABLE IF NOT EXISTS todo_items ' + + '(id varchar(36), name varchar(255), completed boolean, priority varchar(10))' +); +``` + +> **Why `varchar(10)`?** The longest value is `"medium"` (6 chars). `varchar(10)` gives a small safety margin without waste. + +--- + +### MySQL — `backend/src/persistence/mysql.js` + +**Before:** +```javascript +await connection.query( + 'CREATE TABLE IF NOT EXISTS todo_items ' + + '(id varchar(36), name varchar(255), completed boolean)' +); +``` + +**After:** +```javascript +await connection.query( + 'CREATE TABLE IF NOT EXISTS todo_items ' + + "(id varchar(36), name varchar(255), completed boolean, priority varchar(10) DEFAULT 'medium')" +); +``` + +> **Note:** MySQL supports `DEFAULT` in `CREATE TABLE`; SQLite does too, but since the app recreates the table only if it does not exist, existing rows won't be affected. Handle existing rows via a migration script if needed (see below). + +--- + +### Migration script for existing data (optional) + +If the database already has rows without a `priority` value, run this once: + +```sql +-- SQLite +ALTER TABLE todo_items ADD COLUMN priority varchar(10) DEFAULT 'medium'; +UPDATE todo_items SET priority = 'medium' WHERE priority IS NULL; + +-- MySQL +ALTER TABLE todo_items ADD COLUMN priority varchar(10) DEFAULT 'medium'; +UPDATE todo_items SET priority = 'medium' WHERE priority IS NULL; +``` + +--- + +## 3. Layer 2 — Backend Persistence + +Both `sqlite.js` and `mysql.js` export the same interface. Changes are identical for both. + +### `storeItem(item)` — INSERT a new item + +**Input shape:** +```javascript +{ + id: "a1b2c3d4-...", // UUID generated by the route + name: "Buy groceries", + completed: false, + priority: "high" // NEW FIELD +} +``` + +**Before:** +```javascript +async storeItem(item) { + return new Promise((acc, rej) => { + db.run( + 'INSERT INTO todo_items (id, name, completed) VALUES (?, ?, ?)', + [item.id, item.name, item.completed ? 1 : 0], + err => { if (err) return rej(err); acc(); } + ); + }); +} +``` + +**After:** +```javascript +async storeItem(item) { + return new Promise((acc, rej) => { + db.run( + 'INSERT INTO todo_items (id, name, completed, priority) VALUES (?, ?, ?, ?)', + [item.id, item.name, item.completed ? 1 : 0, item.priority], + err => { if (err) return rej(err); acc(); } + ); + }); +} +``` + +--- + +### `updateItem(id, item)` — UPDATE an existing item + +**Input shape:** +```javascript +{ + name: "Buy groceries", + completed: true, + priority: "low" // NEW FIELD +} +``` + +**Before:** +```javascript +async updateItem(id, item) { + return new Promise((acc, rej) => { + db.run( + 'UPDATE todo_items SET name=?, completed=? WHERE id=?', + [item.name, item.completed ? 1 : 0, id], + err => { if (err) return rej(err); acc(); } + ); + }); +} +``` + +**After:** +```javascript +async updateItem(id, item) { + return new Promise((acc, rej) => { + db.run( + 'UPDATE todo_items SET name=?, completed=?, priority=? WHERE id=?', + [item.name, item.completed ? 1 : 0, item.priority, id], + err => { if (err) return rej(err); acc(); } + ); + }); +} +``` + +--- + +### `getItems()` — SELECT all items + +No SQL change needed — `SELECT *` already returns all columns including `priority` once the column exists. + +**Output shape (per row):** +```javascript +{ + id: "a1b2c3d4-...", + name: "Buy groceries", + completed: 0, // SQLite returns 0/1 for booleans + priority: "high" // NEW FIELD in result +} +``` + +--- + +## 4. Layer 3 — API Routes + +### `POST /api/items` — `backend/src/routes/addItem.js` + +**Input (request body):** +```json +{ + "name": "Buy groceries", + "priority": "high" +} +``` + +**Before:** +```javascript +const { v4: uuidv4 } = require('uuid'); +const db = require('../persistence'); + +module.exports = async (req, res) => { + const item = { + id: uuidv4(), + name: req.body.name, + completed: false, + }; + await db.storeItem(item); + res.send(item); +}; +``` + +**After:** +```javascript +const { v4: uuidv4 } = require('uuid'); +const db = require('../persistence'); + +const VALID_PRIORITIES = ['low', 'medium', 'high']; + +module.exports = async (req, res) => { + const priority = VALID_PRIORITIES.includes(req.body.priority) + ? req.body.priority + : 'medium'; + + const item = { + id: uuidv4(), + name: req.body.name, + completed: false, + priority, + }; + await db.storeItem(item); + res.send(item); +}; +``` + +**Output (response body):** +```json +{ + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "name": "Buy groceries", + "completed": false, + "priority": "high" +} +``` + +--- + +### `PUT /api/items/:id` — `backend/src/routes/updateItem.js` + +**Input (request body):** +```json +{ + "name": "Buy groceries", + "completed": true, + "priority": "low" +} +``` + +**Before:** +```javascript +const db = require('../persistence'); + +module.exports = async (req, res) => { + await db.updateItem(req.params.id, req.body); + const item = await db.getItem(req.params.id); + res.send(item); +}; +``` + +**After:** +```javascript +const db = require('../persistence'); + +const VALID_PRIORITIES = ['low', 'medium', 'high']; + +module.exports = async (req, res) => { + const updates = { + ...req.body, + priority: VALID_PRIORITIES.includes(req.body.priority) + ? req.body.priority + : 'medium', + }; + await db.updateItem(req.params.id, updates); + const item = await db.getItem(req.params.id); + res.send(item); +}; +``` + +**Output (response body):** +```json +{ + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "name": "Buy groceries", + "completed": true, + "priority": "low" +} +``` + +--- + +### `GET /api/items` — `backend/src/routes/getItems.js` + +No change needed. The persistence layer already returns all columns. + +**Output (response body):** +```json +[ + { + "id": "a1b2c3d4-...", + "name": "Buy groceries", + "completed": false, + "priority": "high" + }, + { + "id": "b2c3d4e5-...", + "name": "Walk the dog", + "completed": true, + "priority": "low" + } +] +``` + +--- + +## 5. Layer 4 — Frontend Components + +### `AddNewItemForm.jsx` — Add priority selector to the form + +**What changes:** Add a `` dropdown next to the text input. Send `priority` in the POST body. - -**Before:** -```jsx -import React, { useState } from 'react'; -import { InputGroup, Form, Button } from 'react-bootstrap'; - -export default function AddItemForm({ onNewItem }) { - const [newItem, setNewItem] = useState(''); - const [submitting, setSubmitting] = useState(false); - - const submitNewItem = () => { - setSubmitting(true); - fetch('/api/items', { - method: 'POST', - body: JSON.stringify({ name: newItem }), - headers: { 'Content-Type': 'application/json' }, - }) - .then(r => r.json()) - .then(item => { - onNewItem(item); - setSubmitting(false); - setNewItem(''); - }); - }; - - return ( - - setNewItem(e.target.value)} - onKeyUp={e => { if (e.key === 'Enter') submitNewItem(); }} - /> - - - ); -} -``` - -**After:** -```jsx -import React, { useState } from 'react'; -import { InputGroup, Form, Button } from 'react-bootstrap'; - -export default function AddItemForm({ onNewItem }) { - const [newItem, setNewItem] = useState(''); - const [priority, setPriority] = useState('medium'); // NEW - const [submitting, setSubmitting] = useState(false); - - const submitNewItem = () => { - setSubmitting(true); - fetch('/api/items', { - method: 'POST', - body: JSON.stringify({ name: newItem, priority }), // priority added - headers: { 'Content-Type': 'application/json' }, - }) - .then(r => r.json()) - .then(item => { - onNewItem(item); - setSubmitting(false); - setNewItem(''); - setPriority('medium'); // reset after submit - }); - }; - - return ( - - {/* NEW: priority selector */} - setPriority(e.target.value)} - style={{ maxWidth: '120px' }} - aria-label="Priority" - > - - - - - - setNewItem(e.target.value)} - onKeyUp={e => { if (e.key === 'Enter') submitNewItem(); }} - /> - - - ); -} -``` - ---- - -### `ItemDisplay.jsx` — Show the priority badge - -**What changes:** Render a colored Bootstrap badge next to the item name. - -**Before (simplified):** -```jsx -import React from 'react'; -import { Row, Col } from 'react-bootstrap'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faTrash } from '@fortawesome/free-solid-svg-icons'; -import { faCheckSquare, faSquare } from '@fortawesome/free-regular-svg-icons'; -import './ItemDisplay.scss'; - -export default function ItemDisplay({ item, onItemUpdate, onItemRemoval }) { - const toggleCompletion = () => { - fetch(`/api/items/${item.id}`, { - method: 'PUT', - body: JSON.stringify({ ...item, completed: !item.completed }), - headers: { 'Content-Type': 'application/json' }, - }) - .then(r => r.json()) - .then(updatedItem => onItemUpdate(updatedItem)); - }; - - const removeItem = () => { - fetch(`/api/items/${item.id}`, { method: 'DELETE' }) - .then(() => onItemRemoval(item)); - }; - - return ( -
- - - - - - {item.name} - - - - - -
- ); -} -``` - -**After:** -```jsx -import React from 'react'; -import { Row, Col } from 'react-bootstrap'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faTrash } from '@fortawesome/free-solid-svg-icons'; -import { faCheckSquare, faSquare } from '@fortawesome/free-regular-svg-icons'; -import './ItemDisplay.scss'; - -// Maps priority value → Bootstrap badge variant -const PRIORITY_BADGE = { - low: { variant: 'success', label: 'Low' }, - medium: { variant: 'warning', label: 'Medium' }, - high: { variant: 'danger', label: 'High' }, -}; - -function PriorityBadge({ priority }) { - const config = PRIORITY_BADGE[priority] ?? PRIORITY_BADGE.medium; - return ( - - {config.label} - - ); -} - -export default function ItemDisplay({ item, onItemUpdate, onItemRemoval }) { - const toggleCompletion = () => { - fetch(`/api/items/${item.id}`, { - method: 'PUT', - body: JSON.stringify({ ...item, completed: !item.completed }), - headers: { 'Content-Type': 'application/json' }, - }) - .then(r => r.json()) - .then(updatedItem => onItemUpdate(updatedItem)); - }; - - const removeItem = () => { - fetch(`/api/items/${item.id}`, { method: 'DELETE' }) - .then(() => onItemRemoval(item)); - }; - - return ( -
- - - - - - {/* NEW */} - {item.name} - - - - - -
- ); -} -``` - ---- - -## 6. Layer 5 — Styling the Badge - -Bootstrap's built-in `bg-success / bg-warning / bg-danger` classes handle color automatically. No extra SCSS is required. - -If you want custom colors instead of Bootstrap defaults, add to `ItemDisplay.scss`: - -```scss -// Custom priority badge colors (optional override) -.badge-priority-low { background-color: #28a745 !important; color: #fff; } -.badge-priority-medium { background-color: #ffc107 !important; color: #212529; } -.badge-priority-high { background-color: #dc3545 !important; color: #fff; } -``` - -And update `PriorityBadge` to use the custom class names: -```jsx - -``` - ---- - -## 7. Testing - -### Backend — `backend/spec/routes/addItem.spec.js` - -Add a test for the `priority` field: - -```javascript -// Existing test (keep it) -it('should store item with default priority when not provided', async () => { - const mockStoreItem = jest.fn().mockResolvedValue(); - jest.mock('../../src/persistence', () => ({ storeItem: mockStoreItem })); - - const req = { body: { name: 'Test item' } }; - const res = { send: jest.fn() }; - - await addItem(req, res); - - expect(mockStoreItem).toHaveBeenCalledWith( - expect.objectContaining({ priority: 'medium' }) - ); -}); - -// New test -it('should store item with provided priority', async () => { - const mockStoreItem = jest.fn().mockResolvedValue(); - jest.mock('../../src/persistence', () => ({ storeItem: mockStoreItem })); - - const req = { body: { name: 'Urgent task', priority: 'high' } }; - const res = { send: jest.fn() }; - - await addItem(req, res); - - expect(mockStoreItem).toHaveBeenCalledWith( - expect.objectContaining({ priority: 'high' }) - ); - expect(res.send).toHaveBeenCalledWith( - expect.objectContaining({ priority: 'high' }) - ); -}); - -// Guard against invalid values -it('should default to medium when invalid priority is provided', async () => { - const mockStoreItem = jest.fn().mockResolvedValue(); - jest.mock('../../src/persistence', () => ({ storeItem: mockStoreItem })); - - const req = { body: { name: 'Some task', priority: 'critical' } }; - const res = { send: jest.fn() }; - - await addItem(req, res); - - expect(mockStoreItem).toHaveBeenCalledWith( - expect.objectContaining({ priority: 'medium' }) - ); -}); -``` - -### Backend — `backend/spec/persistence/sqlite.spec.js` - -```javascript -it('should store and retrieve priority', async () => { - const item = { id: 'test-id', name: 'Test', completed: false, priority: 'high' }; - await db.storeItem(item); - const items = await db.getItems(); - expect(items[0].priority).toBe('high'); -}); -``` - ---- - -## 8. End-to-End Walkthrough - -### Creating a high-priority item - -**Step 1** — User fills in the form: -- Selects "High" from the dropdown -- Types "Fix production bug" -- Clicks "Add Item" - -**Step 2** — Frontend sends: -```http -POST /api/items -Content-Type: application/json - -{ - "name": "Fix production bug", - "priority": "high" -} -``` - -**Step 3** — `addItem.js` validates priority and builds the item: -```javascript -// priority = "high" (valid, no fallback needed) -const item = { - id: "f47ac10b-58cc-4372-a567-0e02b2c3d479", - name: "Fix production bug", - completed: false, - priority: "high" -}; -``` - -**Step 4** — SQLite executes: -```sql -INSERT INTO todo_items (id, name, completed, priority) -VALUES ('f47ac10b-...', 'Fix production bug', 0, 'high'); -``` - -**Step 5** — API responds: -```json -{ - "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", - "name": "Fix production bug", - "completed": false, - "priority": "high" -} -``` - -**Step 6** — `TodoListCard` calls `onNewItem(item)`, appending it to the state array. - -**Step 7** — `ItemDisplay` renders: -``` -[ ] [HIGH badge in red] Fix production bug [trash] -``` - ---- - -### Page load — fetching existing items - -**Step 1** — On mount, `TodoListCard` calls `GET /api/items` - -**Step 2** — SQLite executes: -```sql -SELECT * FROM todo_items; -``` - -**Step 3** — API returns: -```json -[ - { "id": "...", "name": "Fix production bug", "completed": false, "priority": "high" }, - { "id": "...", "name": "Walk the dog", "completed": true, "priority": "low" } -] -``` - -**Step 4** — Each item renders its badge: -- `priority: "high"` → red `HIGH` badge -- `priority: "low"` → green `LOW` badge - ---- - -## 9. Quick Reference — All Changes at a Glance - -| File | Change | -|------|--------| -| `backend/src/persistence/sqlite.js` | Add `priority varchar(10)` to `CREATE TABLE`; add `priority` to INSERT and UPDATE SQL | -| `backend/src/persistence/mysql.js` | Same as above + `DEFAULT 'medium'` in CREATE TABLE | -| `backend/src/routes/addItem.js` | Validate and read `req.body.priority`, include in item object | -| `backend/src/routes/updateItem.js` | Validate and pass `priority` through to `updateItem()` | -| `client/src/components/AddNewItemForm.jsx` | Add `` for priority; send `priority` in POST body | -| `client/src/components/ItemDisplay.jsx` | Add `PriorityBadge` component; render it next to item name | -| `backend/spec/routes/addItem.spec.js` | Add tests for valid, missing, and invalid priority values | -| `backend/spec/persistence/sqlite.spec.js` | Add test for priority round-trip | - -**Badge color mapping:** - -| Priority | Bootstrap variant | Color | -|----------|------------------|--------| -| `low` | `bg-success` | Green | -| `medium` | `bg-warning` | Yellow | -| `high` | `bg-danger` | Red | diff --git a/planirfan.md b/planirfan.md deleted file mode 100644 index 1a4f8c93..00000000 --- a/planirfan.md +++ /dev/null @@ -1,3 +0,0 @@ -New feateure: -1: Priority levels (low/med/high). DB column, API field, colored badge. - From 64dbe72beae1d15ba279308cd6890a9d0950efce Mon Sep 17 00:00:00 2001 From: ponli550 Date: Wed, 6 May 2026 14:25:49 +0800 Subject: [PATCH 04/27] add priority field to todo items schema and update persistence layers (storeItems, updateItems) nothing changed on getItems and removeItems --- .gitignore | 1 + backend/src/persistence/mysql.js | 10 +++++----- backend/src/persistence/sqlite.js | 10 +++++----- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 2bc1eb0a..bb31bdb7 100644 --- a/.gitignore +++ b/.gitignore @@ -133,3 +133,4 @@ dist ANSWERS.md PRIORITY_FEATURE.md planIrfan.md +learning.md diff --git a/backend/src/persistence/mysql.js b/backend/src/persistence/mysql.js index 39dc0756..4ea62f98 100644 --- a/backend/src/persistence/mysql.js +++ b/backend/src/persistence/mysql.js @@ -39,7 +39,7 @@ async function init() { return new Promise((acc, rej) => { pool.query( - 'CREATE TABLE IF NOT EXISTS todo_items (id varchar(36), name varchar(255), completed boolean) DEFAULT CHARSET utf8mb4', + "CREATE TABLE IF NOT EXISTS todo_items (id varchar(36), name varchar(255), completed boolean), priority varchar(10) DEFAULT 'medium'", (err) => { if (err) return rej(err); @@ -92,8 +92,8 @@ async function getItem(id) { async function storeItem(item) { return new Promise((acc, rej) => { pool.query( - 'INSERT INTO todo_items (id, name, completed) VALUES (?, ?, ?)', - [item.id, item.name, item.completed ? 1 : 0], + 'INSERT INTO todo_items (id, name, completed,priority) VALUES (?, ?, ?,?)', + [item.id, item.name, item.completed ? 1 : 0, item.priority], (err) => { if (err) return rej(err); acc(); @@ -105,8 +105,8 @@ async function storeItem(item) { async function updateItem(id, item) { return new Promise((acc, rej) => { pool.query( - 'UPDATE todo_items SET name=?, completed=? WHERE id=?', - [item.name, item.completed ? 1 : 0, id], + 'UPDATE todo_items SET name=?, completed=?, priority=? WHERE id=?', + [item.name, item.completed ? 1 : 0, item.priority, id], (err) => { if (err) return rej(err); acc(); diff --git a/backend/src/persistence/sqlite.js b/backend/src/persistence/sqlite.js index cf4a81be..e7207fea 100644 --- a/backend/src/persistence/sqlite.js +++ b/backend/src/persistence/sqlite.js @@ -18,7 +18,7 @@ function init() { console.log(`Using sqlite database at ${location}`); db.run( - 'CREATE TABLE IF NOT EXISTS todo_items (id varchar(36), name varchar(255), completed boolean)', + 'CREATE TABLE IF NOT EXISTS todo_items (id varchar(36), name varchar(255), completed boolean), priority varchar(10))', (err, result) => { if (err) return rej(err); acc(); @@ -70,8 +70,8 @@ async function getItem(id) { async function storeItem(item) { return new Promise((acc, rej) => { db.run( - 'INSERT INTO todo_items (id, name, completed) VALUES (?, ?, ?)', - [item.id, item.name, item.completed ? 1 : 0], + 'INSERT INTO todo_items (id, name, completed,priority) VALUES (?, ?, ?,?)', + [item.id, item.name, item.completed ? 1 : 0, item.priority], (err) => { if (err) return rej(err); acc(); @@ -83,8 +83,8 @@ async function storeItem(item) { async function updateItem(id, item) { return new Promise((acc, rej) => { db.run( - 'UPDATE todo_items SET name=?, completed=? WHERE id = ?', - [item.name, item.completed ? 1 : 0, id], + 'UPDATE todo_items SET name=?, completed=?, priority=? WHERE id = ?', + [item.name, item.completed ? 1 : 0, item.priority, id], (err) => { if (err) return rej(err); acc(); From 462558c89894afb4d3cdbd7c4082d84e6b6d55aa Mon Sep 17 00:00:00 2001 From: ponli550 Date: Wed, 6 May 2026 14:56:26 +0800 Subject: [PATCH 05/27] added priority so output json include priority --- backend/src/routes/addItem.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/src/routes/addItem.js b/backend/src/routes/addItem.js index a8650302..ba6cd6da 100644 --- a/backend/src/routes/addItem.js +++ b/backend/src/routes/addItem.js @@ -1,11 +1,15 @@ const db = require('../persistence'); const { v4: uuid } = require('uuid'); +const VALID_PRIORITIES = ['low', 'medium', 'high']; + module.exports = async (req, res) => { + const priority = VALID_PRIORITIES.includes(req.body.priority) ? req.body.priority : 'medium'; const item = { id: uuid(), name: req.body.name, completed: false, + priority, }; await db.storeItem(item); From 5ea06a7f6cf6a663413f72a896e105cb186c555c Mon Sep 17 00:00:00 2001 From: ponli550 Date: Wed, 6 May 2026 15:35:56 +0800 Subject: [PATCH 06/27] add const updates to include priority levels based on different id --- backend/src/routes/updateItem.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/src/routes/updateItem.js b/backend/src/routes/updateItem.js index c2f5871d..2194cab1 100644 --- a/backend/src/routes/updateItem.js +++ b/backend/src/routes/updateItem.js @@ -1,10 +1,12 @@ const db = require('../persistence'); +const VALID_PRIORITIES = ['low', 'medium', 'high']; module.exports = async (req, res) => { - await db.updateItem(req.params.id, { - name: req.body.name, - completed: req.body.completed, - }); + const updates = { + ...req.body, + priority: VALID_PRIORITIES.includes(req.body.priority) ? req.body.priority : 'medium' + }; + await db.updateItem(req.params.id, updates); const item = await db.getItem(req.params.id); res.send(item); }; From 10b1f71930fd24d7d0da51e734b7c9abac5727d5 Mon Sep 17 00:00:00 2001 From: ponli550 Date: Wed, 6 May 2026 15:56:29 +0800 Subject: [PATCH 07/27] adding form select item to be set priority. wrap inside input form for both select and submit --- client/src/components/AddNewItemForm.jsx | 59 ++++++++++++++---------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/client/src/components/AddNewItemForm.jsx b/client/src/components/AddNewItemForm.jsx index b057ce14..fdabd517 100644 --- a/client/src/components/AddNewItemForm.jsx +++ b/client/src/components/AddNewItemForm.jsx @@ -1,12 +1,11 @@ import { useState } from 'react'; import PropTypes from 'prop-types'; -import Button from 'react-bootstrap/Button'; -import Form from 'react-bootstrap/Form'; -import InputGroup from 'react-bootstrap/InputGroup'; +import { InputGroup, Form, Button } from 'react-bootstrap'; export function AddItemForm({ onNewItem }) { const [newItem, setNewItem] = useState(''); const [submitting, setSubmitting] = useState(false); + const [priority, setPriority] = useState('medium'); const submitNewItem = (e) => { e.preventDefault(); @@ -14,39 +13,49 @@ export function AddItemForm({ onNewItem }) { const options = { method: 'POST', - body: JSON.stringify({ name: newItem }), + body: JSON.stringify({ name: newItem, priority }), headers: { 'Content-Type': 'application/json' }, }; fetch('/api/items', options) - .then((r) => r.json()) - .then((item) => { + .then(r => r.json()) + .then(item => { onNewItem(item); setSubmitting(false); setNewItem(''); + setPriority('medium'); }); }; return ( -
- - setNewItem(e.target.value)} - type="text" - placeholder="New Item" - aria-label="New item" - /> - - -
+ + {/* NEW: priority selector */} + setPriority(e.target.value)} + style={{ maxWidth: '120px' }} + aria-label="Priority" + > + + + + + + setNewItem(e.target.value)} + onKeyUp={e => { if (e.key === 'Enter') submitNewItem(); }} + /> + + ); } From 4346c8b55554f45d53b901caf2a43441aaee5434 Mon Sep 17 00:00:00 2001 From: ponli550 Date: Wed, 6 May 2026 16:24:20 +0800 Subject: [PATCH 08/27] adding priority within item column and added three colors --- client/src/components/ItemDisplay.jsx | 27 +++++++++++++++++++------- client/src/components/ItemDisplay.scss | 12 ++++++++++++ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/client/src/components/ItemDisplay.jsx b/client/src/components/ItemDisplay.jsx index f007932c..b0d09ee6 100644 --- a/client/src/components/ItemDisplay.jsx +++ b/client/src/components/ItemDisplay.jsx @@ -8,19 +8,31 @@ import { faTrash } from '@fortawesome/free-solid-svg-icons/faTrash'; import faCheckSquare from '@fortawesome/fontawesome-free-regular/faCheckSquare'; import faSquare from '@fortawesome/fontawesome-free-regular/faSquare'; import './ItemDisplay.scss'; +import React from 'react'; -export function ItemDisplay({ item, onItemUpdate, onItemRemoval }) { +const PRIORITY_COLORS = { + low: { variant: 'success', label: 'Low' }, + medium: { variant: 'warning', label: 'Medium' }, + high: { variant: 'danger', label: 'High' }, +}; +function PriorityBadge({ priority }) { + const config = PRIORITY_COLORS[priority] ?? PRIORITY_COLORS.medium; + return ( + {config.label} + ); +} +export default function ItemDisplay({ item, onItemUpdate, onItemRemoval }) { const toggleCompletion = () => { fetch(`/api/items/${item.id}`, { method: 'PUT', body: JSON.stringify({ - name: item.name, + ...item, completed: !item.completed, }), headers: { 'Content-Type': 'application/json' }, }) - .then((r) => r.json()) - .then(onItemUpdate); + .then(r => r.json()) + .then(updatedItem => onItemUpdate(updatedItem)); }; const removeItem = () => { @@ -48,10 +60,11 @@ export function ItemDisplay({ item, onItemUpdate, onItemRemoval }) { icon={item.completed ? faCheckSquare : faSquare} /> + + diff --git a/client/src/components/ItemDisplay.scss b/client/src/components/ItemDisplay.scss index 3c009b29..07acadfd 100644 --- a/client/src/components/ItemDisplay.scss +++ b/client/src/components/ItemDisplay.scss @@ -31,3 +31,15 @@ button:focus { border: 1px solid #333; } + +.badge-priority-low { + background-color: green; +} + +.badge-priority-medium { + background-color: yellow; +} + +.badge-priority-high { + background-color: red; +} From e026e9161c3eabfe95cf6dfdcc72da96992ebb11 Mon Sep 17 00:00:00 2001 From: ponli550 Date: Wed, 6 May 2026 16:38:24 +0800 Subject: [PATCH 09/27] reupdate , rerender , debug --- backend/spec/persistence/sqlite.spec.js | 11 +++++++ backend/spec/routes/addItem.spec.js | 44 ++++++++++++++++++------- backend/src/persistence/mysql.js | 2 +- backend/src/persistence/sqlite.js | 2 +- client/src/components/ItemDisplay.jsx | 3 +- 5 files changed, 47 insertions(+), 15 deletions(-) diff --git a/backend/spec/persistence/sqlite.spec.js b/backend/spec/persistence/sqlite.spec.js index 996d4515..f0702c92 100644 --- a/backend/spec/persistence/sqlite.spec.js +++ b/backend/spec/persistence/sqlite.spec.js @@ -6,6 +6,7 @@ const ITEM = { id: '7aef3d7c-d301-4846-8358-2a91ec9d6be3', name: 'Test', completed: false, + priority: 'medium', }; beforeEach(() => { @@ -63,3 +64,13 @@ test('it can get a single item', async () => { const item = await db.getItem(ITEM.id); expect(item).toEqual(ITEM); }); + +test('it stores and retrieves the priority field', async () => { + await db.init(); + + const item = { ...ITEM, priority: 'high' }; + await db.storeItem(item); + + const items = await db.getItems(); + expect(items[0].priority).toBe('high'); +}); diff --git a/backend/spec/routes/addItem.spec.js b/backend/spec/routes/addItem.spec.js index 2901f5a5..86d9ca59 100644 --- a/backend/spec/routes/addItem.spec.js +++ b/backend/spec/routes/addItem.spec.js @@ -1,6 +1,5 @@ const db = require('../../src/persistence'); const addItem = require('../../src/routes/addItem'); -const ITEM = { id: 12345 }; const { v4: uuid } = require('uuid'); jest.mock('uuid', () => ({ v4: jest.fn() })); @@ -11,20 +10,43 @@ jest.mock('../../src/persistence', () => ({ getItem: jest.fn(), })); -test('it stores item correctly', async () => { - const id = 'something-not-a-uuid'; - const name = 'A sample item'; - const req = { body: { name } }; +const ID = 'something-not-a-uuid'; +const NAME = 'A sample item'; + +beforeEach(() => { + jest.clearAllMocks(); + uuid.mockReturnValue(ID); +}); + +test('it stores item with default priority when none is provided', async () => { + const req = { body: { name: NAME } }; const res = { send: jest.fn() }; - uuid.mockReturnValue(id); + await addItem(req, res); + + const expectedItem = { id: ID, name: NAME, completed: false, priority: 'medium' }; + expect(db.storeItem).toHaveBeenCalledWith(expectedItem); + expect(res.send).toHaveBeenCalledWith(expectedItem); +}); + +test('it stores item with the provided valid priority', async () => { + const req = { body: { name: NAME, priority: 'high' } }; + const res = { send: jest.fn() }; await addItem(req, res); - const expectedItem = { id, name, completed: false }; + const expectedItem = { id: ID, name: NAME, completed: false, priority: 'high' }; + expect(db.storeItem).toHaveBeenCalledWith(expectedItem); + expect(res.send).toHaveBeenCalledWith(expectedItem); +}); + +test('it defaults to medium when an invalid priority is provided', async () => { + const req = { body: { name: NAME, priority: 'critical' } }; + const res = { send: jest.fn() }; + + await addItem(req, res); - expect(db.storeItem.mock.calls.length).toBe(1); - expect(db.storeItem.mock.calls[0][0]).toEqual(expectedItem); - expect(res.send.mock.calls[0].length).toBe(1); - expect(res.send.mock.calls[0][0]).toEqual(expectedItem); + expect(db.storeItem).toHaveBeenCalledWith( + expect.objectContaining({ priority: 'medium' }), + ); }); diff --git a/backend/src/persistence/mysql.js b/backend/src/persistence/mysql.js index 4ea62f98..83af5b32 100644 --- a/backend/src/persistence/mysql.js +++ b/backend/src/persistence/mysql.js @@ -39,7 +39,7 @@ async function init() { return new Promise((acc, rej) => { pool.query( - "CREATE TABLE IF NOT EXISTS todo_items (id varchar(36), name varchar(255), completed boolean), priority varchar(10) DEFAULT 'medium'", + "CREATE TABLE IF NOT EXISTS todo_items (id varchar(36), name varchar(255), completed boolean, priority varchar(10) DEFAULT 'medium')", (err) => { if (err) return rej(err); diff --git a/backend/src/persistence/sqlite.js b/backend/src/persistence/sqlite.js index e7207fea..c9b99bdb 100644 --- a/backend/src/persistence/sqlite.js +++ b/backend/src/persistence/sqlite.js @@ -18,7 +18,7 @@ function init() { console.log(`Using sqlite database at ${location}`); db.run( - 'CREATE TABLE IF NOT EXISTS todo_items (id varchar(36), name varchar(255), completed boolean), priority varchar(10))', + 'CREATE TABLE IF NOT EXISTS todo_items (id varchar(36), name varchar(255), completed boolean, priority varchar(10))', (err, result) => { if (err) return rej(err); acc(); diff --git a/client/src/components/ItemDisplay.jsx b/client/src/components/ItemDisplay.jsx index b0d09ee6..f7497285 100644 --- a/client/src/components/ItemDisplay.jsx +++ b/client/src/components/ItemDisplay.jsx @@ -63,11 +63,10 @@ export default function ItemDisplay({ item, onItemUpdate, onItemRemoval }) { className={`far ${item.completed ? 'fa-check-square' : 'fa-square' }`} /> - - + {item.name} From 57bf44d87d804e5e5703b7b874abca689d6194f5 Mon Sep 17 00:00:00 2001 From: Nik Nazmir Nadim Date: Wed, 6 May 2026 16:48:34 +0800 Subject: [PATCH 10/27] added greetings and no items yet phrase --- backend/src/routes/getGreeting.js | 12 ++++++++---- client/src/components/TodoListCard.jsx | 2 +- package-lock.json | 6 ++++++ 3 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 package-lock.json diff --git a/backend/src/routes/getGreeting.js b/backend/src/routes/getGreeting.js index a9b997e9..0ad59470 100644 --- a/backend/src/routes/getGreeting.js +++ b/backend/src/routes/getGreeting.js @@ -1,7 +1,11 @@ -const GREETING = 'Hello world!'; +const GREETINGS = [ + "Welcome1", + "dsadad!", + "yoyo!", +]; module.exports = async (req, res) => { - res.send({ - greeting: GREETING, - }); + res.send({ + greeting: GREETINGS[Math.floor(Math.random() * GREETINGS.length)], + }); }; diff --git a/client/src/components/TodoListCard.jsx b/client/src/components/TodoListCard.jsx index 0092344e..e8cdec2c 100644 --- a/client/src/components/TodoListCard.jsx +++ b/client/src/components/TodoListCard.jsx @@ -44,7 +44,7 @@ export function TodoListCard() { <> {items.length === 0 && ( -

No items yet! Add one above!

+

No items. Add one above!

)} {items.map((item) => ( Date: Wed, 6 May 2026 17:38:34 +0800 Subject: [PATCH 11/27] added priority so that json output corectly with priority --- backend/spec/persistence/sqlite.spec.js | 7 ++++++- backend/spec/routes/updateItem.spec.js | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/spec/persistence/sqlite.spec.js b/backend/spec/persistence/sqlite.spec.js index f0702c92..8ddd8e19 100644 --- a/backend/spec/persistence/sqlite.spec.js +++ b/backend/spec/persistence/sqlite.spec.js @@ -1,6 +1,11 @@ +const os = require('os'); +const path = require('path'); + +process.env.SQLITE_DB_LOCATION = path.join(os.tmpdir(), 'test-todo.db'); + const db = require('../../src/persistence/sqlite'); const fs = require('fs'); -const location = process.env.SQLITE_DB_LOCATION || '/etc/todos/todo.db'; +const location = process.env.SQLITE_DB_LOCATION; const ITEM = { id: '7aef3d7c-d301-4846-8358-2a91ec9d6be3', diff --git a/backend/spec/routes/updateItem.spec.js b/backend/spec/routes/updateItem.spec.js index 640e56e7..eb4afc65 100644 --- a/backend/spec/routes/updateItem.spec.js +++ b/backend/spec/routes/updateItem.spec.js @@ -23,6 +23,7 @@ test('it updates items correctly', async () => { expect(db.updateItem.mock.calls[0][1]).toEqual({ name: 'New title', completed: false, + priority: 'medium', }); expect(db.getItem.mock.calls.length).toBe(1); From e4b39911babb523374e518684768becff253e3dd Mon Sep 17 00:00:00 2001 From: ponli550 Date: Wed, 6 May 2026 19:02:20 +0800 Subject: [PATCH 12/27] wrong word for export function --- client/src/components/ItemDisplay.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/ItemDisplay.jsx b/client/src/components/ItemDisplay.jsx index f7497285..b5cd5b82 100644 --- a/client/src/components/ItemDisplay.jsx +++ b/client/src/components/ItemDisplay.jsx @@ -21,7 +21,7 @@ function PriorityBadge({ priority }) { {config.label} ); } -export default function ItemDisplay({ item, onItemUpdate, onItemRemoval }) { +export function ItemDisplay({ item, onItemUpdate, onItemRemoval }) { const toggleCompletion = () => { fetch(`/api/items/${item.id}`, { method: 'PUT', From 37af2dcb46f6ec699d0c32f9af21d6b8a15da214 Mon Sep 17 00:00:00 2001 From: Nik Nazmir Nadim Date: Thu, 7 May 2026 01:42:12 +0800 Subject: [PATCH 13/27] add categories.js to validate input --- backend/src/categories.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 backend/src/categories.js diff --git a/backend/src/categories.js b/backend/src/categories.js new file mode 100644 index 00000000..00a0c160 --- /dev/null +++ b/backend/src/categories.js @@ -0,0 +1,12 @@ +const CATEGORIES = ['work', 'personal', 'shopping']; +const DEFAULT_CATEGORY = 'personal'; + +function normalizeCategory(category) { + return CATEGORIES.includes(category) ? category : DEFAULT_CATEGORY; +} + +module.exports = { + CATEGORIES, + DEFAULT_CATEGORY, + normalizeCategory, +}; From 022a9cdfddc641230f5cff5e50d389e8808b3e9e Mon Sep 17 00:00:00 2001 From: Nik Nazmir Nadim Date: Thu, 7 May 2026 01:45:07 +0800 Subject: [PATCH 14/27] ccept categories when adding items --- backend/src/routes/addItem.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/routes/addItem.js b/backend/src/routes/addItem.js index a8650302..ed0177c8 100644 --- a/backend/src/routes/addItem.js +++ b/backend/src/routes/addItem.js @@ -1,11 +1,13 @@ const db = require('../persistence'); const { v4: uuid } = require('uuid'); +const { normalizeCategory } = require('../categories'); module.exports = async (req, res) => { const item = { id: uuid(), name: req.body.name, completed: false, + category: normalizeCategory(req.body.category), }; await db.storeItem(item); From f84dbf0f983ace6bb1c0c0539477ec6a1ba883c9 Mon Sep 17 00:00:00 2001 From: Nik Nazmir Nadim Date: Thu, 7 May 2026 01:47:35 +0800 Subject: [PATCH 15/27] preserve / update categories after item changes --- backend/src/routes/updateItem.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/routes/updateItem.js b/backend/src/routes/updateItem.js index c2f5871d..8d7dc53a 100644 --- a/backend/src/routes/updateItem.js +++ b/backend/src/routes/updateItem.js @@ -1,9 +1,11 @@ const db = require('../persistence'); +const { normalizeCategory } = require('../categories'); module.exports = async (req, res) => { await db.updateItem(req.params.id, { name: req.body.name, completed: req.body.completed, + category: normalizeCategory(req.body.category), }); const item = await db.getItem(req.params.id); res.send(item); From 1d9ddc8d4427605869ffa1703e84b3ef5f44c23a Mon Sep 17 00:00:00 2001 From: Nik Nazmir Nadim Date: Thu, 7 May 2026 09:48:43 +0800 Subject: [PATCH 16/27] create table and edit functions --- backend/src/persistence/sqlite.js | 45 ++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/backend/src/persistence/sqlite.js b/backend/src/persistence/sqlite.js index cf4a81be..d8772a06 100644 --- a/backend/src/persistence/sqlite.js +++ b/backend/src/persistence/sqlite.js @@ -1,5 +1,10 @@ const sqlite3 = require('sqlite3').verbose(); const fs = require('fs'); +const { + CATEGORIES, + DEFAULT_CATEGORY, + normalizeCategory, +} = require('../categories'); const location = process.env.SQLITE_DB_LOCATION || '/etc/todos/todo.db'; let db, dbAll, dbRun; @@ -18,10 +23,36 @@ function init() { console.log(`Using sqlite database at ${location}`); db.run( - 'CREATE TABLE IF NOT EXISTS todo_items (id varchar(36), name varchar(255), completed boolean)', - (err, result) => { + `CREATE TABLE IF NOT EXISTS todo_items ( + id varchar(36), + name varchar(255), + completed boolean, + category text NOT NULL DEFAULT '${DEFAULT_CATEGORY}' + CHECK(category IN (${CATEGORIES.map((value) => `'${value}'`).join(', ')})) + )`, + (err) => { if (err) return rej(err); - acc(); + + db.all('PRAGMA table_info(todo_items)', (tableErr, rows) => { + if (tableErr) return rej(tableErr); + + const hasCategoryColumn = rows.some( + (column) => column.name === 'category', + ); + + if (hasCategoryColumn) { + acc(); + return; + } + + db.run( + `ALTER TABLE todo_items ADD COLUMN category text NOT NULL DEFAULT '${DEFAULT_CATEGORY}'`, + (alterErr) => { + if (alterErr) return rej(alterErr); + acc(); + }, + ); + }); }, ); }); @@ -45,6 +76,7 @@ async function getItems() { rows.map((item) => Object.assign({}, item, { completed: item.completed === 1, + category: normalizeCategory(item.category), }), ), ); @@ -60,6 +92,7 @@ async function getItem(id) { rows.map((item) => Object.assign({}, item, { completed: item.completed === 1, + category: normalizeCategory(item.category), }), )[0], ); @@ -70,8 +103,8 @@ async function getItem(id) { async function storeItem(item) { return new Promise((acc, rej) => { db.run( - 'INSERT INTO todo_items (id, name, completed) VALUES (?, ?, ?)', - [item.id, item.name, item.completed ? 1 : 0], + 'INSERT INTO todo_items (id, name, completed, category) VALUES (?, ?, ?, ?)', + [item.id, item.name, item.completed ? 1 : 0, normalizeCategory(item.category),], (err) => { if (err) return rej(err); acc(); @@ -84,7 +117,7 @@ async function updateItem(id, item) { return new Promise((acc, rej) => { db.run( 'UPDATE todo_items SET name=?, completed=? WHERE id = ?', - [item.name, item.completed ? 1 : 0, id], + [item.name, item.completed ? 1 : 0, normalizeCategory(item.category), id], (err) => { if (err) return rej(err); acc(); From 3efbb802a2f53fc0efb9a2703fb6bc5d9957dcb0 Mon Sep 17 00:00:00 2001 From: Nik Nazmir Nadim Date: Thu, 7 May 2026 09:57:18 +0800 Subject: [PATCH 17/27] add enum category --- backend/src/persistence/mysql.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/backend/src/persistence/mysql.js b/backend/src/persistence/mysql.js index 39dc0756..4a6c0fd6 100644 --- a/backend/src/persistence/mysql.js +++ b/backend/src/persistence/mysql.js @@ -1,6 +1,7 @@ const waitPort = require('wait-port'); const fs = require('fs'); const mysql = require('mysql2'); +const { DEFAULT_CATEGORY, normalizeCategory } = require('../categories'); const { MYSQL_HOST: HOST, @@ -39,11 +40,14 @@ async function init() { return new Promise((acc, rej) => { pool.query( - 'CREATE TABLE IF NOT EXISTS todo_items (id varchar(36), name varchar(255), completed boolean) DEFAULT CHARSET utf8mb4', + `CREATE TABLE IF NOT EXISTS todo_items ( + id varchar(36), + name varchar(255), + completed boolean, + category ENUM('work', 'personal', 'shopping') NOT NULL DEFAULT '${DEFAULT_CATEGORY}' + ) DEFAULT CHARSET utf8mb4`, (err) => { if (err) return rej(err); - - console.log(`Connected to mysql db at host ${HOST}`); acc(); }, ); @@ -92,8 +96,8 @@ async function getItem(id) { async function storeItem(item) { return new Promise((acc, rej) => { pool.query( - 'INSERT INTO todo_items (id, name, completed) VALUES (?, ?, ?)', - [item.id, item.name, item.completed ? 1 : 0], + 'INSERT INTO todo_items (id, name, completed, category) VALUES (?, ?, ?, ?)', + [item.id, item.name, item.completed ? 1 : 0, normalizeCategory(item.category)], (err) => { if (err) return rej(err); acc(); @@ -105,8 +109,8 @@ async function storeItem(item) { async function updateItem(id, item) { return new Promise((acc, rej) => { pool.query( - 'UPDATE todo_items SET name=?, completed=? WHERE id=?', - [item.name, item.completed ? 1 : 0, id], + 'UPDATE todo_items SET name=?, completed=?, category=? WHERE id=?', + [item.name, item.completed ? 1 : 0, normalizeCategory(item.category), id], (err) => { if (err) return rej(err); acc(); From b1b64838a272be39e5025227efd9e9c12a02b36e Mon Sep 17 00:00:00 2001 From: Nik Nazmir Nadim Date: Thu, 7 May 2026 10:05:13 +0800 Subject: [PATCH 18/27] update tests at backend --- backend/spec/persistence/sqlite.spec.js | 4 +++- backend/spec/routes/addItem.spec.js | 4 ++-- backend/spec/routes/updateItem.spec.js | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/backend/spec/persistence/sqlite.spec.js b/backend/spec/persistence/sqlite.spec.js index 996d4515..4b69969d 100644 --- a/backend/spec/persistence/sqlite.spec.js +++ b/backend/spec/persistence/sqlite.spec.js @@ -6,6 +6,7 @@ const ITEM = { id: '7aef3d7c-d301-4846-8358-2a91ec9d6be3', name: 'Test', completed: false, + category: 'personal', }; beforeEach(() => { @@ -38,10 +39,11 @@ test('it can update an existing item', async () => { await db.updateItem( ITEM.id, - Object.assign({}, ITEM, { completed: !ITEM.completed }), + Object.assign({}, ITEM, { completed: !ITEM.completed, category: 'shopping', }), ); const items = await db.getItems(); + expect(items[0].category).toBe('shopping'); expect(items.length).toBe(1); expect(items[0].completed).toBe(!ITEM.completed); }); diff --git a/backend/spec/routes/addItem.spec.js b/backend/spec/routes/addItem.spec.js index 2901f5a5..83d8d7db 100644 --- a/backend/spec/routes/addItem.spec.js +++ b/backend/spec/routes/addItem.spec.js @@ -14,14 +14,14 @@ jest.mock('../../src/persistence', () => ({ test('it stores item correctly', async () => { const id = 'something-not-a-uuid'; const name = 'A sample item'; - const req = { body: { name } }; + const req = { body: { name: 'A sample item', category: 'shopping' } }; const res = { send: jest.fn() }; uuid.mockReturnValue(id); await addItem(req, res); - const expectedItem = { id, name, completed: false }; + const expectedItem = { id, name, completed: false, category: 'shopping'}; expect(db.storeItem.mock.calls.length).toBe(1); expect(db.storeItem.mock.calls[0][0]).toEqual(expectedItem); diff --git a/backend/spec/routes/updateItem.spec.js b/backend/spec/routes/updateItem.spec.js index 640e56e7..896ef7a2 100644 --- a/backend/spec/routes/updateItem.spec.js +++ b/backend/spec/routes/updateItem.spec.js @@ -10,7 +10,7 @@ jest.mock('../../src/persistence', () => ({ test('it updates items correctly', async () => { const req = { params: { id: 1234 }, - body: { name: 'New title', completed: false }, + body: { name: 'New title', completed: false, category: 'work' }, }; const res = { send: jest.fn() }; @@ -23,6 +23,7 @@ test('it updates items correctly', async () => { expect(db.updateItem.mock.calls[0][1]).toEqual({ name: 'New title', completed: false, + category: 'work', }); expect(db.getItem.mock.calls.length).toBe(1); From 93818c08ff339cb55722b4c04742acba6a361d8a Mon Sep 17 00:00:00 2001 From: Nik Nazmir Nadim Date: Thu, 7 May 2026 10:07:00 +0800 Subject: [PATCH 19/27] define frontend category on a new file --- client/src/categories.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 client/src/categories.js diff --git a/client/src/categories.js b/client/src/categories.js new file mode 100644 index 00000000..e69de29b From 877fd33bdb2e03eb17bdf201c5f57a5e6b1d272e Mon Sep 17 00:00:00 2001 From: Nik Nazmir Nadim Date: Thu, 7 May 2026 10:16:50 +0800 Subject: [PATCH 20/27] add dropdown form --- client/src/components/AddNewItemForm.jsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/client/src/components/AddNewItemForm.jsx b/client/src/components/AddNewItemForm.jsx index b057ce14..c7e5a778 100644 --- a/client/src/components/AddNewItemForm.jsx +++ b/client/src/components/AddNewItemForm.jsx @@ -3,10 +3,11 @@ import PropTypes from 'prop-types'; import Button from 'react-bootstrap/Button'; import Form from 'react-bootstrap/Form'; import InputGroup from 'react-bootstrap/InputGroup'; - +import { CATEGORIES, DEFAULT_CATEGORY } from '../categories'; export function AddItemForm({ onNewItem }) { const [newItem, setNewItem] = useState(''); const [submitting, setSubmitting] = useState(false); + const [submitting, setSubmitting] = useState(false); const submitNewItem = (e) => { e.preventDefault(); @@ -14,7 +15,7 @@ export function AddItemForm({ onNewItem }) { const options = { method: 'POST', - body: JSON.stringify({ name: newItem }), + body: JSON.stringify({ name: newItem, category }), headers: { 'Content-Type': 'application/json' }, }; @@ -24,6 +25,7 @@ export function AddItemForm({ onNewItem }) { onNewItem(item); setSubmitting(false); setNewItem(''); + setCategory(DEFAULT_CATEGORY); }); }; @@ -37,6 +39,17 @@ export function AddItemForm({ onNewItem }) { placeholder="New Item" aria-label="New item" /> + setCategory(e.target.value)} + aria-label="Select category" + > + {CATEGORIES.map((categoryOption) => ( + + ))} + - {item.name} +
{item.name}
+ + {item.category} + @@ -99,6 +100,7 @@ ItemDisplay.propTypes = { name: PropTypes.string, completed: PropTypes.bool, category: PropTypes.string, + priority: PropTypes.string }), onItemUpdate: PropTypes.func, onItemRemoval: PropTypes.func, diff --git a/client/src/components/ItemDisplay.scss b/client/src/components/ItemDisplay.scss index 264d661b..c025c79d 100644 --- a/client/src/components/ItemDisplay.scss +++ b/client/src/components/ItemDisplay.scss @@ -47,4 +47,4 @@ button:focus { .category-badge { margin-top: 6px; letter-spacing: 0.04em; -} +} \ No newline at end of file diff --git a/client/src/components/TodoListCard.jsx b/client/src/components/TodoListCard.jsx index 5f1b4c7f..c4941f84 100644 --- a/client/src/components/TodoListCard.jsx +++ b/client/src/components/TodoListCard.jsx @@ -41,12 +41,12 @@ export function TodoListCard() { [items], ); + if (items === null) return 'Loading...'; + const filteredItems = items.filter( (item) => categoryFilter === 'all' || item.category === categoryFilter, ); - if (items === null) return 'Loading...'; - return ( <> From 1e77e6b9522fdde7cbddab6d20ebd8f884d65b3e Mon Sep 17 00:00:00 2001 From: Nik Nazmir Nadim Date: Thu, 7 May 2026 15:15:59 +0800 Subject: [PATCH 27/27] bug fix part 2 --- .gitignore | 1 + backend/src/persistence/mysql.js | 15 +- bug.md | 630 ------------------------------- 3 files changed, 15 insertions(+), 631 deletions(-) delete mode 100644 bug.md diff --git a/.gitignore b/.gitignore index bb31bdb7..54ddd35a 100644 --- a/.gitignore +++ b/.gitignore @@ -134,3 +134,4 @@ ANSWERS.md PRIORITY_FEATURE.md planIrfan.md learning.md +bug.md \ No newline at end of file diff --git a/backend/src/persistence/mysql.js b/backend/src/persistence/mysql.js index 7aa9b04b..cf76dbf1 100644 --- a/backend/src/persistence/mysql.js +++ b/backend/src/persistence/mysql.js @@ -49,7 +49,20 @@ async function init() { ) DEFAULT CHARSET utf8mb4`, (err) => { if (err) return rej(err); - acc(); + pool.query( + `ALTER TABLE todo_items ADD COLUMN priority VARCHAR(20) DEFAULT 'medium'`, + (err2) => { + // 1060 = duplicate column — column already exists, which is fine + if (err2 && err2.errno !== 1060) return rej(err2); + pool.query( + `ALTER TABLE todo_items ADD COLUMN category ENUM('work', 'personal', 'shopping') NOT NULL DEFAULT '${DEFAULT_CATEGORY}'`, + (err3) => { + if (err3 && err3.errno !== 1060) return rej(err3); + acc(); + }, + ); + }, + ); }, ); }); diff --git a/bug.md b/bug.md deleted file mode 100644 index 6f5a3000..00000000 --- a/bug.md +++ /dev/null @@ -1,630 +0,0 @@ -all bugs seen to be documented here. no deletions. detailed before after. no coding. - ---- - -## BUG 1 — ReferenceError: undefined variable `category` in updateItem route (UPDATED) - -**File:** `backend/src/routes/updateItem.js:10` - -**Description:** The variable `category` is passed as a third argument to `db.updateItem()` but is never defined anywhere in the file (previously named `catego`, now renamed to `category` but still undefined). This throws a `ReferenceError: category is not defined` at runtime, completely breaking the update functionality. Additionally, `db.updateItem()` (in both MySQL and SQLite) only accepts 2 parameters (`id` and `item`), so the extra argument is an API mismatch. The imported `normalizeCategory` function is never used. - -**BEFORE:** Server crashes with `ReferenceError` when any PUT request is made to `/api/items/:id`. The entire update route is non-functional. - -**AFTER:** The route should pass a properly constructed updates object (with normalized category) and call `db.updateItem(req.params.id, updates)` with only 2 arguments. - -**Severity:** CRITICAL - -**Status:** STILL EXISTS (variable renamed from `catego` to `category` but remains undefined) - ---- - -## BUG 2 — Empty `client/src/categories.js` file (FIXED) - -**File:** `client/src/categories.js` (entire file is empty) - -**Description:** This file is completely empty. Both `AddNewItemForm.jsx` (line 6) and `TodoListCard.jsx` (line 5) import `CATEGORIES` and/or `DEFAULT_CATEGORY` from this file. Since the file exports nothing, these imports are `undefined`. This causes `CATEGORIES.map(...)` to throw `TypeError: Cannot read properties of undefined (reading 'map')` when either component renders. - -**BEFORE:** The entire client application crashes on render. The category dropdown and filter dropdown never appear; the app is broken. - -**AFTER:** The file should export `CATEGORIES`, `DEFAULT_CATEGORY`, and `normalizeCategory` (similar to the backend's `categories.js`): -```js -export const CATEGORIES = ['work', 'personal', 'shopping']; -export const DEFAULT_CATEGORY = 'personal'; -``` - -**Severity:** CRITICAL - -**Status:** FIXED — File now exports `CATEGORIES`, `DEFAULT_CATEGORY`, and `formatCategoryLabel`. No longer empty. - ---- - -## BUG 3 — Null reference: `items.filter()` called before null check in TodoListCard (FIXED) - -**File:** `client/src/components/TodoListCard.jsx:44` - -**Description:** `items` is initialized as `null` via `useState(null)` on line 8. On line 44, `items.filter(...)` is called unconditionally. The null guard `if (items === null) return 'Loading...'` on line 48 comes AFTER the `.filter()` call. In React, the entire function body executes top-to-bottom, so `items.filter()` throws `TypeError: Cannot read properties of null (reading 'filter')` before the early return can protect it. - -**BEFORE:** Component crashes on initial render with `TypeError`. The loading state is never reached. - -**AFTER:** The null check on line 48 must be moved before line 44, or line 44 should use optional chaining: `const filteredItems = items?.filter(...) ?? [];` - -**Severity:** CRITICAL - -**Status:** FIXED — Null check on line 44 now correctly placed BEFORE the `items.filter()` call on line 46. Order was reversed from original bug description. - ---- - -## BUG 4 — SQLite `updateItem` SQL parameter order mismatch (FIXED) - -**File:** `backend/src/persistence/sqlite.js:118-121` - -**Description:** The SQL query now has 4 placeholders (`name=?, category=?, completed=? WHERE id=?`), but the parameter array has the WRONG ORDER: `[item.name, item.completed ? 1 : 0, normalizeCategory(item.category), id]`. The SQL expects `(name, category, completed, id)` but params are supplied as `(name, completed, category, id)`. The `completed` value gets written into the `category` column and vice versa. - -**BEFORE:** The SQLite `updateItem` silently swaps category and completed values. When you complete an item, its completion state is lost and its category is corrupted. - -**AFTER:** The parameter array should match SQL placeholder order: `[item.name, normalizeCategory(item.category), item.completed ? 1 : 0, id]` - -**Severity:** HIGH - -**Status:** FIXED — `sqlite.js:120-121` — SQL and params now correctly ordered: `[item.name, normalizeCategory(item.category), item.completed ? 1 : 0, id]`. Parameters match placeholder order. - ---- - -## BUG 5 — MySQL persistence `priority` column has syntax error and missing value (FIXED) - -**File:** `backend/src/persistence/mysql.js:43-48` (schema), `:96-107` (storeItem), `:66-78` (getItems) - -**Description:** The MySQL `CREATE TABLE` now includes a `priority` column (line 47), and the INSERT column list includes `priority`. BUT the `priority` column definition has no data type (`priority,` instead of `priority VARCHAR(20)`), causing a SQL syntax error on table creation. Additionally, the VALUES array has only 4 values for 5 columns — `[item.id, item.name, item.completed ? 1 : 0, normalizeCategory(item.category)]` — the priority value is missing. This causes a SQL parameter count mismatch error. - -**BEFORE:** The MySQL schema fails to create (syntax error on `priority,`). If the table somehow exists, `storeItem` throws a SQL parameter count mismatch error (5 columns, 4 values). Priority is never persisted. - -**AFTER:** The MySQL schema should define `priority` with a proper data type (e.g., `priority VARCHAR(20)`), and `storeItem` should include the priority value in the VALUES array with 5 placeholders. - -**Severity:** HIGH - -**Status:** FIXED — `mysql.js:47` — `priority` now has `VARCHAR(20)` type. `mysql.js:102-103` — 5 placeholders and 5 values including `item.priority`. - ---- - -## BUG 6 — SQLite persistence partially stores `priority` but doesn't retrieve it (FIXED) - -**File:** `backend/src/persistence/sqlite.js:26-32` (schema), `:103-114` (storeItem), `:71-84` (getItems) - -**Description:** The SQLite `CREATE TABLE` now includes `priority varchar(20)` (line 29). `storeItem` now inserts `priority` with 5 placeholders (line 107-108). BUT `getItems` and `getItem` do not return the `priority` field from the database — they only map `completed` and `category`. Also, there's a missing comma between `priority varchar(20)` (line 29) and `completed boolean,` (line 30) in the CREATE TABLE, which will cause a SQL syntax error on fresh database creation. - -**BEFORE:** Priority is stored but never returned. Items from SQLite have no `priority` field. On fresh DB creation, syntax error from missing comma. - -**AFTER:** `getItems` and `getItem` should include `priority` in the returned object. Fix the missing comma in CREATE TABLE between `priority varchar(20)` and `completed boolean`. - -**Severity:** HIGH - -**Status:** FIXED — `sqlite.js:29` now has trailing comma: `priority varchar(20),`. Also `getItems`/`getItem` return `priority` naturally via `SELECT *` with `Object.assign({}, item, ...)` which includes all database columns. - ---- - -## BUG 7 — MySQL `getItems` and `getItem` do not normalize category - -**File:** `backend/src/persistence/mysql.js:66-78` (getItems), `:81-94` (getItem) - -**Description:** The MySQL versions of `getItems()` and `getItem()` do not call `normalizeCategory()` on the returned items' category field. The SQLite versions correctly call `normalizeCategory(item.category)`. If any item has a NULL or invalid category value in MySQL, it will be returned as-is rather than being normalized to the default. - -**BEFORE:** Items with NULL or invalid categories in MySQL are returned with raw NULL/invalid values, causing UI inconsistencies between MySQL and SQLite backends. - -**AFTER:** Both `getItems` and `getItem` in mysql.js should include `category: normalizeCategory(item.category)` in the `Object.assign()` call. - -**Severity:** MEDIUM - -**Status:** FIXED — `mysql.js:75` and `mysql.js:91` — Both `getItems` and `getItem` now call `normalizeCategory(item.category)`. - ---- - -## BUG 8 — Duplicate/conflicting imports in AddNewItemForm.jsx - -**File:** `client/src/components/AddNewItemForm.jsx:3-5` - -**Description:** `Button` and `Form` are imported twice: -- Line 3: `import Button from 'react-bootstrap/Button';` -- Line 4: `import Form from 'react-bootstrap/Form';` -- Line 5: `import { InputGroup, Form, Button } from 'react-bootstrap';` - -The named imports on line 5 shadow the default imports on lines 3-4. While this may work in practice, it is confusing and non-idiomatic, and could cause subtle differences if the default and named exports diverge. - -**BEFORE:** Working but confusing code with shadowed imports. Potential for subtle bugs if default and named exports differ. - -**AFTER:** Use only one import style — either all default imports or all named imports from `react-bootstrap`. - -**Severity:** LOW - -**Status:** FIXED — `AddNewItemForm.jsx:3` — Duplicate imports consolidated to single named import `{ InputGroup, Form, Button } from 'react-bootstrap'`. - ---- - -## BUG 9 — Missing error handling in AddNewItemForm fetch (FIXED) - -**File:** `client/src/components/AddNewItemForm.jsx:23-30` - -**Description:** The `fetch('/api/items', options)` call has no `.catch()` handler. If the network request fails or the server returns an error response, `setSubmitting(false)` is never called, leaving the button permanently in the disabled "Adding..." state. The user cannot retry. - -**BEFORE:** On any fetch failure (network error, server error), the submit button stays disabled forever showing "Adding...". The user must refresh the page to recover. - -**AFTER:** Add a `.catch()` handler that calls `setSubmitting(false)` and optionally displays an error message. - -**Severity:** MEDIUM - -**Status:** FIXED — `AddNewItemForm.jsx:30` — `.catch()` now correctly implemented with arrow function: `.catch(() => setSubmitting(false))`. Also, `priority` now included in POST body (line 17). - ---- - -## BUG 10 — Missing error handling in ItemDisplay toggleCompletion fetch (FIXED) - -**File:** `client/src/components/ItemDisplay.jsx:28-38` - -**Description:** The `fetch` call in `toggleCompletion()` has no `.catch()` handler. If the PUT request fails, the UI state is not updated and the user gets no feedback. The item remains in its pre-toggle visual state with no indication of failure. - -**BEFORE:** On fetch failure, the checkbox toggle appears to do nothing. No error feedback to the user. - -**AFTER:** Add a `.catch()` handler that provides error feedback and optionally reverts the UI state. - -**Severity:** MEDIUM - -**Status:** FIXED — `ItemDisplay.jsx:39` — `.catch()` now has proper error handler: `.catch(err => console.error('Failed to toggle item:', err))`. - ---- - -## BUG 11 — Missing error handling in ItemDisplay removeItem fetch (optimistic deletion) (REGRESSED — new syntax error) - -**File:** `client/src/components/ItemDisplay.jsx:42-48` - -**Description:** The `fetch` call in `removeItem()` has no `.catch()` handler. If the DELETE request fails, `onItemRemoval(item)` is still called, removing the item from the UI even though it still exists on the server. The next page refresh will show the "deleted" item again. - -**BEFORE:** On DELETE failure, the item disappears from the UI but still exists on the server. It reappears on the next page load, confusing the user. - -**AFTER:** `.catch()` with a callback that confirms success before calling `onItemRemoval`. - -**Severity:** HIGH - -**Status:** MODIFIED — Error handling added at lines 43-48 with proper `.then()`/`.catch()` chain, BUT a new **SYNTAX ERROR** was introduced on line 43: `fetch(...)then(r => {` is missing the `.` before `then`. Should be `fetch(...).then(r => {`. This causes a runtime `TypeError`. - -**Suggested fix:** Add the missing dot: `fetch(`/api/items/${item.id}`, { method: 'DELETE' }).then(r => {` - ---- - -## BUG 12 — Missing error handling in Greeting fetch - -**File:** `client/src/components/Greeting.jsx:7-9` - -**Description:** The `fetch('/api/greeting')` call has no `.catch()` handler. If the request fails, `greeting` remains `null` and the component renders nothing (returns null). The user sees no greeting with no indication of why. - -**BEFORE:** On fetch failure, the greeting silently disappears. No error feedback. - -**AFTER:** Add a `.catch()` handler that sets an error state or fallback greeting. - -**Severity:** LOW - -**Status:** STILL EXISTS — Unchanged. No `.catch()` on Greeting fetch. - -**Suggested `.catch()` solution:** Add `.catch()` with a fallback greeting: -```js -fetch('/api/greeting') - .then((res) => res.json()) - .then((data) => setGreeting(data.greeting)) - .catch(() => setGreeting('Hello!')); -``` - ---- - -## BUG 13 — Missing error handling in TodoListCard initial fetch - -**File:** `client/src/components/TodoListCard.jsx:11-15` - -**Description:** The initial `fetch('/api/items')` in `useEffect` has no `.catch()` handler. If the request fails, `items` remains `null` forever and the component is stuck showing "Loading..." indefinitely. - -**BEFORE:** On fetch failure, the app is permanently stuck on "Loading..." with no error message or recovery option. - -**AFTER:** Add a `.catch()` handler that sets an error state and displays an error message. - -**Severity:** MEDIUM - -**Status:** STILL EXISTS — Unchanged. No `.catch()` on TodoListCard initial fetch. - -**Suggested `.catch()` solution:** Add `.catch()` with error state and fallback to empty array: -```js -fetch('/api/items') - .then(r => r.json()) - .then(data => setItems(data)) - .catch(err => { console.error('Failed to load items:', err); setItems([]); }); -``` - ---- - -## BUG 14 — Missing error handling in deleteItem route - -**File:** `backend/src/routes/deleteItem.js:3-5` - -**Description:** The route has no try/catch or error handling. If `db.removeItem(req.params.id)` throws (e.g., database error, or item doesn't exist), the error propagates as an unhandled promise rejection, potentially crashing the server. - -**BEFORE:** Database errors or invalid IDs cause unhandled promise rejections. The server may crash or return no response. - -**AFTER:** Wrap in try/catch and return an appropriate error status (e.g., 404 for not found, 500 for database errors). - -**Severity:** HIGH - -**Status:** STILL EXISTS — Unchanged. No try/catch in deleteItem route. - ---- - -## BUG 15 — Missing error handling in getItems route - -**File:** `backend/src/routes/getItems.js:3-5` - -**Description:** No try/catch around `db.getItems()`. If the database query fails, the error propagates as an unhandled promise rejection. - -**BEFORE:** Database errors cause unhandled promise rejections with no meaningful HTTP response to the client. - -**AFTER:** Wrap in try/catch and return a 500 status with error details. - -**Severity:** MEDIUM - -**Status:** STILL EXISTS — Unchanged. No try/catch in getItems route. - ---- - -## BUG 16 — No input validation in addItem route - -**File:** `backend/src/routes/addItem.js:6-17` - -**Description:** The route does not validate that `req.body.name` exists or is non-empty. If a client sends `{}` or `{ category: 'work' }` without a `name`, the item is stored with `name: undefined`. No validation of request body structure at all. - -**BEFORE:** Items can be created with `undefined` or empty names. The database stores NULL for the name field. - -**AFTER:** Validate that `req.body.name` exists and is a non-empty string. Return 400 if validation fails. - -**Severity:** MEDIUM - -**Status:** STILL EXISTS — Unchanged. No validation of `req.body.name` in addItem. - ---- - -## BUG 17 — fs.readFileSync returns Buffer instead of string in mysql.js (Docker secrets) - -**File:** `backend/src/persistence/mysql.js:20-23` - -**Description:** `fs.readFileSync(HOST_FILE)` returns a `Buffer` object, not a string. When this Buffer is passed to the MySQL connection pool as `host`, `user`, `password`, or `database`, it causes connection failures or unexpected behavior. The Buffer should be converted to a string using `.toString('utf8')` or by passing `{ encoding: 'utf8' }` to `readFileSync`. - -**BEFORE:** When using Docker secrets (FILE env vars), the database connection fails or behaves unpredictably because Buffer objects are passed instead of strings. - -**AFTER:** Use `fs.readFileSync(HOST_FILE, 'utf8').trim()` or `.toString('utf8').trim()` for all file reads. - -**Severity:** HIGH - -**Status:** STILL EXISTS — Unchanged. `fs.readFileSync()` still returns Buffer, no `.toString()`. - ---- - -## BUG 18 — Priority state defined but never used in AddNewItemForm (PARTIALLY FIXED) (PARTIALLY FIXED) - -**File:** `client/src/components/AddNewItemForm.jsx:11` - -**Description:** `const [priority, setPriority] = useState('medium')` is defined but `priority` is never included in the POST body (`body: JSON.stringify({ name: newItem, category })`) and there is no UI control to change the priority. The state variable is dead code. - -**BEFORE:** Items are always created with "medium" priority regardless of what the backend expects. Users have no way to set priority when adding items. - -**AFTER:** Either remove the unused state or add a priority selector to the form and include it in the POST body. - -**Severity:** MEDIUM - -**Status:** PARTIALLY FIXED — `priority` now included in POST body (line 17: `{ name: newItem, category, priority }`). `setPriority('')` called on success (line 28). BUT there is still no UI control (dropdown/slider) for users to select priority — always defaults to `'medium'`. - -**Suggested solution:** Add a `Form.Select` for priority between the category selector and the submit button: -```js - setPriority(e.target.value)}> - - - - -``` - ---- - -## BUG 19 — Duplicate checkbox icons rendered in ItemDisplay (FIXED) - -**File:** `client/src/components/ItemDisplay.jsx:62-68` - -**Description:** The toggle button renders both a `FontAwesomeIcon` component AND a raw `` element for the same checkbox. This causes two checkbox icons to be displayed side-by-side for every todo item. - -**BEFORE:** Each todo item shows two checkbox icons (one FontAwesomeIcon + one raw `` element) instead of one. - -**AFTER:** Remove one of the two icon implementations. Only one checkbox icon should be rendered per item. - -**Severity:** MEDIUM - -**Status:** FIXED — `ItemDisplay.jsx:66-69` — Raw `` element removed. Only `FontAwesomeIcon` remains. - ---- - -## BUG 20 — PropTypes for ItemDisplay missing `priority` field - -**File:** `client/src/components/ItemDisplay.jsx:96-104` - -**Description:** The PropTypes shape for `item` includes `id`, `name`, `completed`, and `category`, but omits `priority`. However, `item.priority` is used in the `PriorityBadge` component. This means PropTypes validation will not warn when `priority` is missing. - -**BEFORE:** No PropTypes warning when `item.priority` is undefined. The PriorityBadge silently falls back to "Medium" due to the `??` operator. - -**AFTER:** Add `priority: PropTypes.string` to the item PropTypes shape. - -**Severity:** LOW - -**Status:** FIXED — `ItemDisplay.jsx:102` — PropTypes now includes `priority: PropTypes.string`. - ---- - -## BUG 21 — Unused `React` import in ItemDisplay - -**File:** `client/src/components/ItemDisplay.jsx:11` - -**Description:** `import React from 'react'` is present but unnecessary. With the modern JSX transform (configured via Vite with `@vitejs/plugin-react`), React does not need to be in scope for JSX to work. - -**BEFORE:** Unused import adds a tiny amount of bundle size. No functional impact. - -**AFTER:** Remove the unused `import React from 'react'` line. - -**Severity:** LOW - -**Status:** STILL EXISTS — Unchanged. Unused `import React from 'react'` on line 11. - ---- - -## BUG 22 — Unconventional import ordering in ItemDisplay - -**File:** `client/src/components/ItemDisplay.jsx:24` - -**Description:** `import Badge from 'react-bootstrap/Badge'` appears between the `PriorityBadge` function definition and the `ItemDisplay` export. While JavaScript hoists imports and this works, it violates conventional coding style and makes the code harder to read. All imports should be at the top of the file. - -**BEFORE:** Code works but is confusingly organized. - -**AFTER:** Move the `Badge` import to the top of the file with the other imports. - -**Severity:** LOW - -**Status:** STILL EXISTS — Unchanged. `Badge` import still on line 24, between function and export. - ---- - -## BUG 23 — Greeting text contains placeholder/debug values - -**File:** `backend/src/routes/getGreeting.js:1-5` - -**Description:** The `GREETINGS` array contains placeholder/debug values: `"Welcome1"` (with a trailing digit typo), `"dsadad!"` (nonsense string), and `"yoyo!"`. These look like test strings that were never replaced with proper greeting messages. - -**BEFORE:** Users see random nonsense greetings like "dsadad!" and "Welcome1" (with a typo). - -**AFTER:** Replace with proper greeting messages like "Welcome!", "Hello!", "Good day!", etc. - -**Severity:** LOW - -**Status:** STILL EXISTS — Unchanged. Still `"Welcome1"`, `"dsadad!"`, `"yoyo!"`. - ---- - -## BUG 24 — updateItem.spec.js test has incorrect expectation for category (FIXED) - -**File:** `backend/spec/routes/updateItem.spec.js:23-28` - -**Description:** The test sends `category: 'work'` in the request body but expects `category: 'personal'` in the update call. Since `'work'` is a valid category, there is no reason for it to be changed to `'personal'`. This test expectation is incorrect. - -**BEFORE:** The test assertion for `category` will fail once the `catego` bug is fixed, because the code would correctly pass `category: 'work'` not `'personal'`. - -**AFTER:** The test expectation should be `category: 'work'` to match the input. - -**Severity:** MEDIUM - -**Status:** FIXED — Test now sends `category: 'personal'` (line 13) and expects `category: 'personal'` (line 27). Input and expectation now match. Also test now expects `priority: 'medium'` in the update call (line 26), which aligns with the route's default behavior. - ---- - -## BUG 25 — No Express error handler middleware or 404 handler - -**File:** `backend/src/index.js` - -**Description:** The Express app has no error handler middleware. If any route throws an uncaught error, Express returns a generic HTML 500 response rather than a structured JSON error. There is also no 404 handler for unmatched routes. - -**BEFORE:** Uncaught errors return generic HTML responses. Unmatched routes return Express default 404 HTML. API clients receive inconsistent error formats. - -**AFTER:** Add an error handler middleware and a 404 handler that return JSON responses. - -**Severity:** MEDIUM - -**Status:** STILL EXISTS — Unchanged. No error middleware or 404 handler in index.js. - ---- - -## BUG 26 — MySQL `storeItem` column/value count mismatch after partial fix (FIXED) - -**File:** `backend/src/persistence/mysql.js:96-107` - -**Description:** The `addItem` route constructs an item object with `id`, `name`, `completed`, `priority`, and `category`. `storeItem` in mysql.js now has 5 column names in the INSERT (`id, name, completed, priority, category`) but only 4 `?` placeholders in VALUES `VALUES (?, ?, ?, ?)`. The priority value is also missing from the parameter array `[item.id, item.name, item.completed ? 1 : 0, normalizeCategory(item.category)]`. This causes a SQL parameter count mismatch error (5 columns, 4 placeholders). - -**BEFORE:** SQL error due to column/placeholder count mismatch. Priority is never persisted to MySQL. - -**AFTER:** Add a 5th `?` placeholder and include `item.priority` in the VALUES array: `VALUES (?, ?, ?, ?, ?)` with `[item.id, item.name, item.completed ? 1 : 0, item.priority, normalizeCategory(item.category)]`. - -**Severity:** HIGH - -**Status:** FIXED — `mysql.js:102` — Now `VALUES (?, ?, ?, ?, ?)` with 5 values in param array including `item.priority`. - ---- - -## BUG 27 — SQLite `storeItem` does not store priority despite addItem route providing it - -**File:** `backend/src/persistence/sqlite.js:103-114` - -**Description:** Same as Bug 26 but for SQLite. The `storeItem` function only inserts `(id, name, completed, category)`, discarding the `priority` field. - -**BEFORE:** Priority is set in the route but never persisted to SQLite. Items always have no priority value in the database. - -**AFTER:** Add a `priority` column to the SQLite table schema and include it in the INSERT statement. - -**Severity:** HIGH - -**Status:** FIXED — `sqlite.js:107-108` — `storeItem` now inserts `priority` column and value with 5 placeholders. - ---- - -## BUG 28 — `onItemRemoval` produces corrupted state when item not found (FIXED) - -**File:** `client/src/components/TodoListCard.jsx:36-42` - -**Description:** If `findIndex` returns `-1` (item not found), `slice(-1 + 1)` = `slice(0)` which returns the full array, resulting in `setItems([...items.slice(0, -1), ...items.slice(0)])` which creates a corrupted array state with missing and duplicated items. - -**BEFORE:** If an item ID is not found in the list, the state update produces unexpected/duplicate items. - -**AFTER:** Check if `index === -1` before calling `setItems`, or use `items.filter(i => i.id !== item.id)`. - -**Severity:** LOW - -**Status:** FIXED — `TodoListCard.jsx:38-39` — Now uses correct slice pattern: `[...items.slice(0, index), ...items.slice(index + 1)]` which properly removes the item at the found index. - ---- - -## BUG 29 — `onItemUpdate` has same slice bug when item not found (FIXED) - -**File:** `client/src/components/TodoListCard.jsx:24-34` - -**Description:** If `items.findIndex((i) => i.id === item.id)` returns `-1`, then `items.slice(0, -1)` returns all items except the last one, and `items.slice(0)` returns all items. The result is a corrupted state with missing and duplicated items. - -**BEFORE:** If an update arrives for an item not in the current list, the state becomes corrupted. - -**AFTER:** Check if `index === -1` before calling `setItems`, or use `items.map(i => i.id === item.id ? item : i)`. - -**Severity:** LOW - -**Status:** FIXED — `TodoListCard.jsx:26-31` — Now uses correct slice pattern: `[...items.slice(0, index), item, ...items.slice(index + 1)]` which properly replaces the item at the found index. - ---- - -## BUG 30 — `addItem.spec.js` tests expect `priority` but persistence layers don't support it (FIXED) - -**File:** `backend/spec/routes/addItem.spec.js:21-51` - -**Description:** The tests for `addItem` expect items to be stored and returned with a `priority` field. However, neither the MySQL nor SQLite persistence layers have a `priority` column or store the priority field. These tests pass only because the persistence module is mocked — in reality, the priority would be lost. - -**BEFORE:** Tests pass in isolation (due to mocks) but the actual system does not store priority. This gives a false sense of correctness. - -**AFTER:** Either add priority support to both persistence layers (fixing Bugs 5 and 6), or update the tests to not expect priority. - -**Severity:** MEDIUM - -**Status:** FIXED — Both MySQL and SQLite persistence layers now store and return `priority`. Tests pass with mocks and actual behavior matches expectations. - ---- - -## BUG 31 — ESLint config specifies React version 18.2 but package.json uses React 19.1 - -**File:** `client/.eslintrc.cjs:12` - -**Description:** The ESLint settings specify `react: { version: '18.2' }` but `package.json` declares `"react": "^19.1.0"`. This version mismatch can cause ESLint rules to behave incorrectly, especially rules that depend on React version-specific behavior. - -**BEFORE:** ESLint may apply rules intended for React 18.2 to React 19.1 code, potentially missing issues or reporting false positives. - -**AFTER:** Update the ESLint config to match: `react: { version: 'detect' }` or explicitly set `'19.1'`. - -**Severity:** LOW - -**Status:** STILL EXISTS — Unchanged. ESLint still says `18.2`, package.json still `^19.1.0`. - ---- - -## NEW BUG A — MySQL `priority` column has no data type (SQL syntax error) - -**File:** `backend/src/persistence/mysql.js:47` - -**Description:** The CREATE TABLE statement includes `priority,` as a column name but without any data type definition. It should be `priority VARCHAR(20)` or similar. This causes a SQL syntax error when the table is created, preventing the entire MySQL schema from initializing. - -**BEFORE:** MySQL table creation fails with a syntax error. The application cannot start when using MySQL persistence. - -**AFTER:** Define the priority column with a proper data type: `priority VARCHAR(20) DEFAULT 'medium'` - -**Severity:** CRITICAL - -**Status:** FIXED — `mysql.js:47` — `priority VARCHAR(20)` now has proper data type. - ---- - -## NEW BUG B — MySQL `storeItem` INSERT has 5 columns but only 4 placeholders (FIXED) - -**File:** `backend/src/persistence/mysql.js:100` - -**Description:** The INSERT statement has 5 column names `(id, name, completed, priority, category)` but only 4 `?` placeholders in `VALUES (?, ?, ?, ?)`. This is a direct consequence of the partial fix for BUG 5/26 — the column name was added but the placeholder was not. This causes a SQL parameter count mismatch error. - -**BEFORE:** Any attempt to store an item via MySQL throws a SQL error. No items can be created when using MySQL persistence. - -**AFTER:** Add a 5th `?` placeholder: `VALUES (?, ?, ?, ?, ?)` and include the priority value in the parameter array. - -**Severity:** CRITICAL - -**Status:** FIXED — `mysql.js:102` — Now `VALUES (?, ?, ?, ?, ?)` with 5 values. - ---- - -## NEW BUG C — SQLite `updateItem` parameter order mismatch (swapped category/completed) (FIXED) - -**File:** `backend/src/persistence/sqlite.js:119-120` - -**Description:** The SQL is `SET name=?, category=?, completed=?` (3 placeholders) but params are `[item.name, item.completed ? 1 : 0, normalizeCategory(item.category), id]` (4 values). Even ignoring the extra `id`, the order is wrong: `completed` value goes into `category` placeholder and `category` value goes into `completed` placeholder. Toggling completion corrupts the category and vice versa. - -**BEFORE:** Completing a todo item changes its category instead. Changing category toggles its completion state. Data is silently corrupted. - -**AFTER:** Match parameter order to SQL placeholder order: `[item.name, normalizeCategory(item.category), item.completed ? 1 : 0, id]` - -**Severity:** HIGH - -**Status:** FIXED — `sqlite.js:120-121` — Parameter order now matches SQL placeholder order. - ---- - -## NEW BUG D — SQLite CREATE TABLE missing comma between `priority` and `completed` columns - -**File:** `backend/src/persistence/sqlite.js:29-30` - -**Description:** The CREATE TABLE SQL has `priority varchar(20)` on line 29 with no trailing comma, followed by `completed boolean,` on line 30. The missing comma causes a SQL syntax error when the table is created on a fresh database. This was introduced when the `priority` column was added to the schema. - -**BEFORE:** SQLite database creation fails with a SQL syntax error. The app cannot start with a fresh SQLite database. - -**AFTER:** Add a trailing comma after `priority varchar(20)` → `priority varchar(20),` - -**Severity:** CRITICAL - -**Status:** FIXED — `sqlite.js:29` now has trailing comma: `priority varchar(20),` - ---- - -## NEW BUG E — SyntaxError in ItemDisplay removeItem: missing `.` before `then` - -**File:** `client/src/components/ItemDisplay.jsx:43` - -**Description:** The `removeItem` function has a syntax error: `fetch(`/api/items/${item.id}`, { method: 'DELETE' })then(r => {` — the `.` before `then` is missing. This causes a `SyntaxError` at parse time, preventing the entire `ItemDisplay.jsx` module from loading. This broke the entire app's ability to render todo items. - -**BEFORE:** App crashes on load with `SyntaxError` when parsing ItemDisplay.jsx. No todo items can be displayed, toggled, or removed. - -**AFTER:** Add the missing `.` before `then`: `fetch(`/api/items/${item.id}`, { method: 'DELETE' }).then(r => {` - -**Severity:** CRITICAL - -**Status:** NEW BUG — Introduced during partial fix of BUG 11 - ---- - -## Summary by Severity (UPDATED) - -**Bugs FIXED:** 21 (BUG 2, 3, 4, 5, 6, 7, 8, 9, 10, 19, 20, 24, 26, 27, 28, 29, 30, NEW BUG A, B, C, D) -**Bugs PARTIALLY FIXED:** 1 (BUG 18 — priority in POST body but no UI selector) -**Bugs REGRESSED:** 1 (BUG 11 — error handling added but introduced syntax error) -**Bugs STILL EXISTING:** 12 (BUG 1, 12-17, 21-23, 25, 31) -**NEW BUGS FOUND:** 1 (E — syntax error from BUG 11 fix attempt) - -| Severity | Count | Bug Numbers | -|----------|-------|-------------| -| CRITICAL | 3 | 1, 11, E | -| HIGH | 2 | 14, 17 | -| MEDIUM | 7 | 13, 15, 16, 18, 25, 30, 12 | -| LOW | 4 | 21, 22, 23, 31 |