Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
94d8166
init: instructions
azaidrahman May 5, 2026
97b0d09
new file for planning
ponli550 May 6, 2026
941669d
uncached md files for organized structure
ponli550 May 6, 2026
64dbe72
add priority field to todo items schema and update persistence layers…
ponli550 May 6, 2026
462558c
added priority so output json include priority
ponli550 May 6, 2026
5ea06a7
add const updates to include priority levels based on different id
ponli550 May 6, 2026
10b1f71
adding form select item to be set priority. wrap inside input form fo…
ponli550 May 6, 2026
4346c8b
adding priority within item column and added three colors
ponli550 May 6, 2026
e026e91
reupdate , rerender <prirrityBadge> , debug
ponli550 May 6, 2026
57bf44d
added greetings and no items yet phrase
May 6, 2026
85ec187
added priority so that json output corectly with priority
ponli550 May 6, 2026
e4b3991
wrong word for export function
ponli550 May 6, 2026
37af2dc
add categories.js to validate input
May 6, 2026
022a9cd
ccept categories when adding items
May 6, 2026
f84dbf0
preserve / update categories after item changes
May 6, 2026
1d9ddc8
create table and edit functions
May 7, 2026
3efbb80
add enum category
May 7, 2026
7009e2b
Merge pull request #1 from azaidrahman/feat/PriorityLevel
ponli550 May 7, 2026
b1b6483
update tests at backend
May 7, 2026
93818c0
define frontend category on a new file
May 7, 2026
877fd33
add dropdown form
May 7, 2026
3fb1f75
edit category variable
May 7, 2026
61073e5
add show category for each to-do item
May 7, 2026
b651ffd
preserve category when completion
May 7, 2026
505c433
add category filter dropdown
May 7, 2026
d6af27d
add state when no category matches item
May 7, 2026
a70cd2f
resolve conflict
May 7, 2026
67f7e89
pull
May 7, 2026
00bdfbb
bug fix (a lot)
May 7, 2026
1e77e6b
bug fix part 2
May 7, 2026
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
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,11 @@ dist
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.pnp.*

# Reviewer notes
ANSWERS.md
PRIORITY_FEATURE.md
planIrfan.md
learning.md
bug.md
84 changes: 84 additions & 0 deletions INSTRUCTION.md
Original file line number Diff line number Diff line change
@@ -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/<name>`. 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/<name>` 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`.
22 changes: 20 additions & 2 deletions backend/spec/persistence/sqlite.spec.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
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',
name: 'Test',
completed: false,
priority: 'medium',
category: 'personal',
};

beforeEach(() => {
Expand Down Expand Up @@ -38,10 +45,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);
});
Expand All @@ -63,3 +71,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');
});
44 changes: 33 additions & 11 deletions backend/spec/routes/addItem.spec.js
Original file line number Diff line number Diff line change
@@ -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() }));
Expand All @@ -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' }),
);
});
4 changes: 3 additions & 1 deletion backend/spec/routes/updateItem.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: 'personal' },
};
const res = { send: jest.fn() };

Expand All @@ -23,6 +23,8 @@ test('it updates items correctly', async () => {
expect(db.updateItem.mock.calls[0][1]).toEqual({
name: 'New title',
completed: false,
priority: 'medium',
category: 'personal'
});

expect(db.getItem.mock.calls.length).toBe(1);
Expand Down
12 changes: 12 additions & 0 deletions backend/src/categories.js
Original file line number Diff line number Diff line change
@@ -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,
};
36 changes: 28 additions & 8 deletions backend/src/persistence/mysql.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -39,12 +40,29 @@ 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(20),
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();
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();
},
);
},
);
},
);
});
Expand All @@ -67,6 +85,7 @@ async function getItems() {
rows.map((item) =>
Object.assign({}, item, {
completed: item.completed === 1,
category: normalizeCategory(item.category)
}),
),
);
Expand All @@ -82,6 +101,7 @@ async function getItem(id) {
rows.map((item) =>
Object.assign({}, item, {
completed: item.completed === 1,
category: normalizeCategory(item.category)
}),
)[0],
);
Expand All @@ -92,8 +112,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, category) VALUES (?, ?, ?, ?, ?)',
[item.id, item.name, item.completed ? 1 : 0, item.priority, normalizeCategory(item.category)],
(err) => {
if (err) return rej(err);
acc();
Expand All @@ -105,8 +125,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();
Expand Down
Loading