PRD Studio

Smoke Test App

24 tasks. The agent pulls these one at a time via the CLI.

  1. #1Scaffold a new Next.js 14+ project with TypeScript using `npx create-next-app@latest` (Pages Router). Enable Tailwind CS

    Scaffold a new Next.js 14+ project with TypeScript using `npx create-next-app@latest` (Pages Router). Enable Tailwind CSS during setup. Confirm the project runs with `npm run dev` and the default page loads at localhost:3000. Done when: `npm run dev` starts without errors and the browser renders the default Next.js page at localhost:3000.

    in_progress
  2. #2Install the `pg` package and its TypeScript types: `npm install pg` and `npm install --save-dev @types/pg`. Create `/lib

    Install the `pg` package and its TypeScript types: `npm install pg` and `npm install --save-dev @types/pg`. Create `/lib/db.ts` that exports a singleton `Pool` instance configured from `process.env.DATABASE_URL`. Add `.env.local` to `.gitignore` (if not already present) and create a `.env.local.example` file with `DATABASE_URL=postgres://user:password@host:5432/dbname` as a placeholder. Never hard-code any credentials. Done when: `pg` is in `package.json` dependencies, `/lib/db.ts` exports a `Pool` that reads `DATABASE_URL` from env, and no real credentials appear in any committed file.

    Depends on: #1

    pending
  3. #3Create `/lib/initDb.ts` that exports an `initDb()` async function. The function executes a `CREATE TABLE IF NOT EXISTS t

    Create `/lib/initDb.ts` that exports an `initDb()` async function. The function executes a `CREATE TABLE IF NOT EXISTS todos` DDL statement with columns: `id SERIAL PRIMARY KEY`, `text VARCHAR(500) NOT NULL CHECK (char_length(text) > 0)`, `completed BOOLEAN NOT NULL DEFAULT false`, `created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()`. Call `initDb()` from a Next.js custom server entry point or from `/pages/_app.tsx` server-side guard so it runs once on startup. Confirm the `todos` table is created automatically when the dev server starts against a real PostgreSQL instance. Done when: Starting `npm run dev` with a valid `DATABASE_URL` creates the `todos` table if it does not already exist (verifiable via `psql` or a DB GUI).

    Depends on: #2

    pending
  4. #4Define the shared TypeScript type in `/lib/types.ts`: `export interface Todo { id: number; text: string; completed: bool

    Define the shared TypeScript type in `/lib/types.ts`: `export interface Todo { id: number; text: string; completed: boolean; created_at: string; }`. This type will be imported by both API routes and frontend components. Done when: `/lib/types.ts` exists, exports the `Todo` interface with the four fields, and the project compiles without TypeScript errors (`npx tsc --noEmit`).

    Depends on: #2

    pending
  5. #5Create the API route `/pages/api/todos/index.ts` and implement the `GET /api/todos` handler. It must query `SELECT * FRO

    Create the API route `/pages/api/todos/index.ts` and implement the `GET /api/todos` handler. It must query `SELECT * FROM todos ORDER BY created_at ASC`, return `200` with a JSON array of `Todo` objects, and return `500` with `{ error: string }` on unexpected DB errors. Done when: A `GET` request to `/api/todos` (e.g., via `curl` or browser) returns `200` and a JSON array (empty `[]` when no rows exist).

    Depends on: #3, #4

    pending
  6. #6In the same `/pages/api/todos/index.ts` file, add the `POST /api/todos` handler. It must: (1) parse `{ text }` from the

    In the same `/pages/api/todos/index.ts` file, add the `POST /api/todos` handler. It must: (1) parse `{ text }` from the request body, (2) return `400 { error }` if `text` is missing, empty, or whitespace-only, (3) `INSERT` the new row into `todos`, (4) return `201` with the inserted `Todo` object, and (5) return `500` on unexpected errors. Done when: `POST /api/todos` with `{ "text": "Buy milk" }` returns `201` and the new todo; `POST` with `{ "text": "" }` or `{ "text": " " }` returns `400`.

    Depends on: #5

    pending
  7. #7In the same `/pages/api/todos/index.ts` file, add the bulk-delete `DELETE /api/todos?completed=true` handler. It must: (

    In the same `/pages/api/todos/index.ts` file, add the bulk-delete `DELETE /api/todos?completed=true` handler. It must: (1) check for the `completed=true` query parameter, (2) execute `DELETE FROM todos WHERE completed = true`, (3) return `204` on success, and (4) return `400` if the query parameter is absent or not `'true'`, and `500` on errors. Done when: `DELETE /api/todos?completed=true` returns `204` and removes all completed rows; calling it without the query param returns `400`.

    Depends on: #6

    pending
  8. #8Create the API route `/pages/api/todos/[id].ts` and implement the `PATCH /api/todos/:id` handler. It must: (1) parse `id

    Create the API route `/pages/api/todos/[id].ts` and implement the `PATCH /api/todos/:id` handler. It must: (1) parse `id` from the URL and `{ completed }` from the request body, (2) validate that `id` is a valid integer and `completed` is a boolean, returning `400` otherwise, (3) run `UPDATE todos SET completed = $1 WHERE id = $2 RETURNING *`, (4) return `404 { error }` if no row was updated, (5) return `200` with the updated `Todo`, and (6) return `500` on errors. Done when: `PATCH /api/todos/1` with `{ "completed": true }` returns `200` and the updated todo; `PATCH /api/todos/99999` returns `404`.

    Depends on: #5

    pending
  9. #9In `/pages/api/todos/[id].ts`, add the `DELETE /api/todos/:id` handler. It must: (1) parse `id` from the URL, (2) valida

    In `/pages/api/todos/[id].ts`, add the `DELETE /api/todos/:id` handler. It must: (1) parse `id` from the URL, (2) validate it is a valid integer (return `400` if not), (3) run `DELETE FROM todos WHERE id = $1`, (4) return `404` if no row was deleted, (5) return `204` on success, and (6) return `500` on errors. Done when: `DELETE /api/todos/1` returns `204` and removes the row; `DELETE /api/todos/99999` returns `404`.

    Depends on: #8

    pending
  10. #10Replace the default `/pages/index.tsx` with the main page component. Use `getServerSideProps` to call the `GET /api/todo

    Replace the default `/pages/index.tsx` with the main page component. Use `getServerSideProps` to call the `GET /api/todos` logic (or fetch from the API) and pass the initial `Todo[]` array as props. Render a top-level container `<main>` with an `<h1>` title (e.g., "Todos"), a placeholder `<TodoList>` area, an `<AddTodoForm>` placeholder, and a `<Footer>` placeholder. Hold todos in React state initialized from props. Done when: The page loads, receives the initial todo array from the server, and renders without errors (placeholders are acceptable at this stage).

    Depends on: #5

    pending
  11. #11Create `/components/AddTodoForm.tsx`. It must render a `<form>` with: a text `<input>` (placeholder "New todo", `aria-la

    Create `/components/AddTodoForm.tsx`. It must render a `<form>` with: a text `<input>` (placeholder "New todo", `aria-label="New todo"`) and an "Add" `<button>`. The button must be disabled (or input invalid) when the trimmed value is empty. On submit (Enter key or button click), call a `onAdd(text: string) => Promise<void>` prop callback, then clear and refocus the input on success. Show an inline validation message if submission is attempted with empty/whitespace input. Done when: The form renders, blocks empty submission with visible feedback, calls `onAdd` with trimmed text on valid submit, and clears+refocuses the input afterward.

    Depends on: #10

    pending
  12. #12Wire `AddTodoForm` into the main page. Implement the `handleAdd` async function in `index.tsx`: it calls `POST /api/todo

    Wire `AddTodoForm` into the main page. Implement the `handleAdd` async function in `index.tsx`: it calls `POST /api/todos` with `{ text }`, receives the new `Todo`, and appends it to the local todos state. Pass `handleAdd` as the `onAdd` prop to `<AddTodoForm>`. Done when: Typing a todo and pressing Enter or clicking "Add" creates a new row in the DB and the todo appears in the UI list without a page reload; the remaining count increases by 1.

    Depends on: #6, #11

    pending
  13. #13Create `/components/TodoItem.tsx`. It must render a single todo row with: (1) a `<input type="checkbox">` with `aria-lab

    Create `/components/TodoItem.tsx`. It must render a single todo row with: (1) a `<input type="checkbox">` with `aria-label` set to the todo text, checked when `completed=true`; (2) a `<span>` for the todo text; (3) a delete `<button aria-label="Delete todo">` showing "×". Accept props: `todo: Todo`, `onToggle: (id: number, completed: boolean) => void`, `onDelete: (id: number) => void`. Done when: `TodoItem` renders all three elements with correct props and triggers `onToggle`/`onDelete` callbacks on interaction.

    Depends on: #4, #10

    pending
  14. #14Create `/components/TodoList.tsx`. It maps over a `todos: Todo[]` prop and renders one `<TodoItem>` per todo, passing do

    Create `/components/TodoList.tsx`. It maps over a `todos: Todo[]` prop and renders one `<TodoItem>` per todo, passing down `onToggle` and `onDelete` callbacks. Renders an empty `<ul>` (or a friendly empty-state message) when the array is empty. Done when: `TodoList` renders all todos passed to it, and renders nothing (or an empty state) when the array is empty.

    Depends on: #13

    pending
  15. #15Wire `TodoList` into the main page. Implement `handleToggle` in `index.tsx`: it calls `PATCH /api/todos/:id` with the to

    Wire `TodoList` into the main page. Implement `handleToggle` in `index.tsx`: it calls `PATCH /api/todos/:id` with the toggled `completed` value, then updates the matching todo in local state. Implement `handleDelete` in `index.tsx`: it calls `DELETE /api/todos/:id`, then removes the todo from local state. Pass all required props to `<TodoList>`. Done when: Clicking a checkbox toggles strikethrough and updates the DB; clicking delete removes the todo from the UI and DB immediately.

    Depends on: #8, #9, #14

    pending
  16. #16Create `/components/Footer.tsx`. It accepts `todos: Todo[]` as a prop and: (1) computes `remaining = todos.filter(t => !

    Create `/components/Footer.tsx`. It accepts `todos: Todo[]` as a prop and: (1) computes `remaining = todos.filter(t => !t.completed).length`; (2) displays `"1 item left"` when remaining is 1, else `"X items left"` for any other count; (3) renders a "Clear completed" button only when `todos.some(t => t.completed)` is true; (4) accepts an `onClearCompleted: () => void` prop triggered by that button. Done when: Footer correctly displays singular/plural remaining count and conditionally renders the "Clear completed" button.

    Depends on: #4, #10

    pending
  17. #17Wire `Footer` into the main page. Implement `handleClearCompleted` in `index.tsx`: it calls `DELETE /api/todos?completed

    Wire `Footer` into the main page. Implement `handleClearCompleted` in `index.tsx`: it calls `DELETE /api/todos?completed=true`, then removes all completed todos from local state. Pass `todos` and `onClearCompleted` to `<Footer>`. Done when: Clicking "Clear completed" removes all completed todos from the DB and UI in one operation; the remaining count is unchanged; the button disappears if no completed todos remain.

    Depends on: #7, #16

    pending
  18. #18Apply Tailwind CSS styling to the main page layout (`/pages/index.tsx`): center the content horizontally with a max-widt

    Apply Tailwind CSS styling to the main page layout (`/pages/index.tsx`): center the content horizontally with a max-width (e.g., `max-w-lg mx-auto`), add vertical padding, and render a readable heading. Ensure the layout is usable at 320 px viewport width (no horizontal overflow, readable text). Done when: The page is visually centered, the heading is readable, and the layout does not break or overflow at 320 px viewport width.

    Depends on: #15, #17

    pending
  19. #19Apply Tailwind CSS styling to `AddTodoForm`: style the input (full-width, border, rounded, padding, focus ring) and the

    Apply Tailwind CSS styling to `AddTodoForm`: style the input (full-width, border, rounded, padding, focus ring) and the "Add" button (colored background, hover state). Ensure both elements are visually distinct and keyboard-focusable with a visible focus indicator. Done when: The form looks clean, the input and button are styled, and keyboard focus is clearly visible on both elements.

    Depends on: #18

    pending
  20. #20Apply Tailwind CSS styling to `TodoItem`: (1) strikethrough (`line-through`) and muted text color (`text-gray-400`) on c

    Apply Tailwind CSS styling to `TodoItem`: (1) strikethrough (`line-through`) and muted text color (`text-gray-400`) on completed todos; (2) the delete button is hidden by default and revealed on row hover using Tailwind's `group` / `group-hover` utilities; (3) the checkbox and text are vertically aligned; (4) add `aria-label` attributes to the checkbox and delete button for accessibility. Done when: Completed todos show strikethrough + muted color; the delete button is hidden at rest and visible on hover; all interactive elements have accessible labels.

    Depends on: #18

    pending
  21. #21Apply Tailwind CSS styling to `Footer`: flex layout with space between the remaining count text and the "Clear completed

    Apply Tailwind CSS styling to `Footer`: flex layout with space between the remaining count text and the "Clear completed" button, subtle top border to separate it from the list, and a hover style on the "Clear completed" button. Done when: Footer renders with correct layout, the count text and button are separated, and the button has a hover effect.

    Depends on: #18

    pending
  22. #22Perform a full accessibility audit of all interactive elements. Verify: (1) the new-todo `<input>` has `aria-label="New

    Perform a full accessibility audit of all interactive elements. Verify: (1) the new-todo `<input>` has `aria-label="New todo"`; (2) each checkbox has an `aria-label` equal to the todo text; (3) each delete button has `aria-label="Delete [todo text]"`; (4) the "Add" and "Clear completed" buttons have descriptive text or aria-labels; (5) Tab order flows logically through all controls. Fix any missing labels or focus traps. Done when: All interactive elements are reachable and operable via keyboard alone, and each has a valid accessible label (verifiable by inspecting the DOM or using an accessibility checker).

    Depends on: #19, #20, #21

    pending
  23. #23Add an `env.example` (or update `.env.local.example`) at the project root documenting all required environment variables

    Add an `env.example` (or update `.env.local.example`) at the project root documenting all required environment variables (`DATABASE_URL`). Audit all committed source files to confirm no literal database credentials or `DATABASE_URL` values are hard-coded anywhere. Add a note in `README.md` (create it if absent) explaining how to set up `.env.local` and run the app. Done when: No `DATABASE_URL` value appears in any committed source file; `README.md` documents setup steps; `.env.local` is listed in `.gitignore`.

    Depends on: #2

    pending
  24. #24Perform end-to-end manual smoke testing of all six user flows against the running app with a live PostgreSQL database: (

    Perform end-to-end manual smoke testing of all six user flows against the running app with a live PostgreSQL database: (1) Add a todo — appears in list, count increments, input clears. (2) Toggle complete — strikethrough applies, count decrements; toggle back — strikethrough removed, count increments. (3) Delete single — todo removed, count adjusts correctly. (4) Clear completed — all completed removed, incomplete untouched, button hides. (5) Remaining count — always correct singular/plural. (6) Persistence — refresh browser and restart server; all data reappears. Fix any bugs found. Done when: All six flows complete without errors, all acceptance criteria in §9 of the PRD pass, and the DB state is consistent with the UI after each action.

    Depends on: #22, #23

    pending