Rename lists -> projects and items -> cards throughout
Project scope shifts from a todo list to a project-management app. This is a straight terminology rename across code, comments, migrations, tests, and docs — no behaviour change. - DB: table todo_lists -> projects, todo_items -> cards, column todo_items.list_id -> cards.project_id, indexes renamed. Migrations 003/004 rewritten in place (destructive; recreate the volume with `down -v`). - API: /api/lists -> /api/projects, nested /items -> /cards, reorder body item_ids -> card_ids, JSON keys list/lists/item/items -> project/projects/ card/cards, item_count -> card_count, list_id -> project_id, and the matching error messages. - PHP: TodoList/TodoItem Repository + Controller -> Project/Card; shared SQL aliases l/i -> p/c. - Frontend: stores lists.ts/items.ts -> projects.ts/cards.ts (useProjectsStore / useCardsStore, MAX_PROJECTS), ListView -> ProjectView, TodoItemRow -> CardRow, route /lists/:id -> /projects/:id (name "project"), types TodoList/ TodoItem -> Project/Card, and all UI copy. CSS .lists*/.list-head* -> .projects*/.project-head*, .item* -> .card-row* (kept the generic .card panel class), .items -> .cards. - Product name in the header, PWA manifest, index.html title and package descriptions -> "Project Manager" / "Projects". Backend suite: 37 passing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
# PHP Todo List
|
||||
# PHP Project Manager
|
||||
|
||||
A small todo-list application: a REST API written in PHP (Slim 4) backed by an
|
||||
SQLite file, plus a Vue 3 + TypeScript PWA frontend in [web/](web/).
|
||||
A small project-management application: a REST API written in PHP (Slim 4)
|
||||
backed by an SQLite file, plus a Vue 3 + TypeScript PWA frontend in [web/](web/).
|
||||
Each user owns **projects**, and each project holds ordered **cards**.
|
||||
|
||||
## Status
|
||||
|
||||
@@ -9,10 +10,10 @@ SQLite file, plus a Vue 3 + TypeScript PWA frontend in [web/](web/).
|
||||
|-------|-------|-------|
|
||||
| 1 | Auth API — register, login, `GET /me` | ✅ done |
|
||||
| 2 | Frontend shell — Vite PWA, auth-gated routing, register/login pages | ✅ done |
|
||||
| 3 | Todo list + item CRUD API | ✅ done |
|
||||
| 4 | Frontend lists view — list index + create form | ✅ done |
|
||||
| 5 | Frontend list detail — items UI with drag-and-drop reorder | ✅ done |
|
||||
| 6 | List view — inline title/description editing, delete via a Manage menu | ✅ done |
|
||||
| 3 | Project + card CRUD API | ✅ done |
|
||||
| 4 | Frontend projects view — project index + create form | ✅ done |
|
||||
| 5 | Frontend project detail — cards UI with drag-and-drop reorder | ✅ done |
|
||||
| 6 | Project view — inline title/description editing, delete via a Manage menu | ✅ done |
|
||||
| 7 | Email verification (magic links) + profile page (resend, change email) | ✅ done |
|
||||
| 8 | Passwordless login — magic-link by default, password login behind a toggle | ✅ done |
|
||||
|
||||
@@ -108,7 +109,7 @@ environment). See [.env.example](.env.example).
|
||||
| `JWT_TTL` | `86400` | Token lifetime in seconds |
|
||||
| `APP_URL` | `http://localhost:5173` | Frontend base URL used to build magic links |
|
||||
| `MAIL_TRANSPORT` | `mail` | `mail` (PHP `mail()`), `smtp`, or `log` (append to a file) |
|
||||
| `MAIL_FROM` / `MAIL_FROM_NAME` | `no-reply@todo.test` / `Todo List` | Envelope sender |
|
||||
| `MAIL_FROM` / `MAIL_FROM_NAME` | `no-reply@todo.test` / `Projects` | Envelope sender |
|
||||
| `MAIL_LOG_PATH` | `storage/mail.log` | Where `log` transport writes |
|
||||
| `MAIL_SMTP_HOST` / `_PORT` / `_USERNAME` / `_PASSWORD` / `_ENCRYPTION` | — / `587` / — / — / `tls` | Used only when `MAIL_TRANSPORT=smtp` |
|
||||
|
||||
@@ -235,37 +236,37 @@ The current password is required (`422` if wrong). The address must be free
|
||||
until** the magic link sent to the new address is opened — until then `GET
|
||||
/api/me` shows the old address with `pending_email` set.
|
||||
|
||||
### Todo lists
|
||||
### Projects
|
||||
|
||||
All routes below require `Authorization: Bearer <jwt>`. A list belongs to one
|
||||
owner (the creator); another user's list — or a missing one — always responds
|
||||
All routes below require `Authorization: Bearer <jwt>`. A project belongs to one
|
||||
owner (the creator); another user's project — or a missing one — always responds
|
||||
`404`.
|
||||
|
||||
| Method | Path | Purpose |
|
||||
|--------|------|---------|
|
||||
| `GET` | `/api/lists` | the caller's lists, sorted A→Z by title |
|
||||
| `POST` | `/api/lists` | create a list |
|
||||
| `GET` | `/api/lists/{id}` | one list |
|
||||
| `PATCH` | `/api/lists/{id}` | update `title` and/or `description` |
|
||||
| `DELETE` | `/api/lists/{id}` | delete the list and its items (`204`) |
|
||||
| `GET` | `/api/projects` | the caller's projects, sorted A→Z by title |
|
||||
| `POST` | `/api/projects` | create a project |
|
||||
| `GET` | `/api/projects/{id}` | one project |
|
||||
| `PATCH` | `/api/projects/{id}` | update `title` and/or `description` |
|
||||
| `DELETE` | `/api/projects/{id}` | delete the project and its cards (`204`) |
|
||||
|
||||
`GET /api/lists` is always ordered alphabetically (case-insensitive) by title;
|
||||
there is no other sort option. A user may own at most **100 lists** — creating
|
||||
one beyond that responds `409`.
|
||||
`GET /api/projects` is always ordered alphabetically (case-insensitive) by
|
||||
title; there is no other sort option. A user may own at most **100 projects** —
|
||||
creating one beyond that responds `409`.
|
||||
|
||||
Create/update body: `title` (required on create, 1–255 chars), `description`
|
||||
(optional, ≤ 2000 chars, defaults to `""`). `PATCH` needs at least one field.
|
||||
|
||||
List representation:
|
||||
Project representation:
|
||||
|
||||
```json
|
||||
{
|
||||
"list": {
|
||||
"project": {
|
||||
"id": 1,
|
||||
"title": "Shopping",
|
||||
"description": "For the week",
|
||||
"title": "Website relaunch",
|
||||
"description": "Q3",
|
||||
"owner_id": 1,
|
||||
"item_count": 3,
|
||||
"card_count": 3,
|
||||
"completed_count": 1,
|
||||
"created_at": "2026-09-03T12:00:00Z",
|
||||
"updated_at": "2026-09-03T12:00:00Z"
|
||||
@@ -273,40 +274,41 @@ List representation:
|
||||
}
|
||||
```
|
||||
|
||||
`GET /api/lists` returns `{ "lists": [ … ] }`.
|
||||
`GET /api/projects` returns `{ "projects": [ … ] }`.
|
||||
|
||||
### Todo items
|
||||
### Cards
|
||||
|
||||
Scoped to a list; the parent list's ownership is checked first (`404` otherwise).
|
||||
Scoped to a project; the parent project's ownership is checked first
|
||||
(`404` otherwise).
|
||||
|
||||
| Method | Path | Purpose |
|
||||
|--------|------|---------|
|
||||
| `GET` | `/api/lists/{id}/items` | items, ordered by `position` then `id` |
|
||||
| `POST` | `/api/lists/{id}/items` | add an item |
|
||||
| `PUT` | `/api/lists/{id}/items/order` | reorder all items in one shot |
|
||||
| `GET` | `/api/lists/{id}/items/{itemId}` | one item |
|
||||
| `PATCH` | `/api/lists/{id}/items/{itemId}` | update `text`, `complete`, and/or `position` |
|
||||
| `DELETE` | `/api/lists/{id}/items/{itemId}` | delete the item (`204`) |
|
||||
| `GET` | `/api/projects/{id}/cards` | cards, ordered by `position` then `id` |
|
||||
| `POST` | `/api/projects/{id}/cards` | add a card |
|
||||
| `PUT` | `/api/projects/{id}/cards/order` | reorder all cards in one shot |
|
||||
| `GET` | `/api/projects/{id}/cards/{cardId}` | one card |
|
||||
| `PATCH` | `/api/projects/{id}/cards/{cardId}` | update `text`, `complete`, and/or `position` |
|
||||
| `DELETE` | `/api/projects/{id}/cards/{cardId}` | delete the card (`204`) |
|
||||
|
||||
Create body: `text` (required, 1–1000 chars), `complete` (optional bool,
|
||||
default `false`), `position` (optional integer ≥ 0; when omitted the item is
|
||||
default `false`), `position` (optional integer ≥ 0; when omitted the card is
|
||||
appended after the current highest position). `PATCH` needs at least one field.
|
||||
`position` is a plain sort key the client manages — updating one item never
|
||||
`position` is a plain sort key the client manages — updating one card never
|
||||
renumbers its siblings.
|
||||
|
||||
`PUT …/items/order` takes `{ "item_ids": [3, 1, 2] }` — every item in the list,
|
||||
each exactly once (`422` otherwise). It rewrites positions to `0..n-1` in one
|
||||
transaction and returns `{ "items": [ … ] }` in the new order. This is what the
|
||||
drag-and-drop reorder in the UI calls.
|
||||
`PUT …/cards/order` takes `{ "card_ids": [3, 1, 2] }` — every card in the
|
||||
project, each exactly once (`422` otherwise). It rewrites positions to `0..n-1`
|
||||
in one transaction and returns `{ "cards": [ … ] }` in the new order. This is
|
||||
what the drag-and-drop reorder in the UI calls.
|
||||
|
||||
Item representation:
|
||||
Card representation:
|
||||
|
||||
```json
|
||||
{
|
||||
"item": {
|
||||
"card": {
|
||||
"id": 10,
|
||||
"list_id": 1,
|
||||
"text": "Milk",
|
||||
"project_id": 1,
|
||||
"text": "Design homepage",
|
||||
"complete": false,
|
||||
"position": 0,
|
||||
"created_at": "2026-09-03T12:00:00Z",
|
||||
@@ -315,7 +317,7 @@ Item representation:
|
||||
}
|
||||
```
|
||||
|
||||
`GET …/items` returns `{ "items": [ … ] }`.
|
||||
`GET …/cards` returns `{ "cards": [ … ] }`.
|
||||
|
||||
### Error shape
|
||||
|
||||
@@ -342,16 +344,16 @@ TOKEN=$(curl -s -X POST $BASE/api/auth/login \
|
||||
|
||||
curl -s $BASE/api/me -H "Authorization: Bearer $TOKEN"
|
||||
|
||||
LIST=$(curl -s -X POST $BASE/api/lists \
|
||||
PROJECT=$(curl -s -X POST $BASE/api/projects \
|
||||
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||||
-d '{"title":"Shopping","description":"For the week"}' \
|
||||
-d '{"title":"Website relaunch","description":"Q3"}' \
|
||||
| grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
|
||||
|
||||
curl -s -X POST $BASE/api/lists/$LIST/items \
|
||||
curl -s -X POST $BASE/api/projects/$PROJECT/cards \
|
||||
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||||
-d '{"text":"Milk"}'
|
||||
-d '{"text":"Design homepage"}'
|
||||
|
||||
curl -s $BASE/api/lists/$LIST/items -H "Authorization: Bearer $TOKEN"
|
||||
curl -s $BASE/api/projects/$PROJECT/cards -H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## Tests
|
||||
@@ -373,8 +375,8 @@ src/Auth/AuthMiddleware.php Bearer-token authentication
|
||||
src/Auth/SessionPayload.php Shared user + session JSON shape
|
||||
src/Mail/ Mailer interface, SMTP/mail()/log transports, EmailVerifier
|
||||
src/Http/JsonErrorHandler.php Uniform JSON error envelope
|
||||
src/Http/Controllers/ Request handlers (Auth, EmailVerification, TodoList, TodoItem)
|
||||
src/Repository/ Database access (User, EmailVerification, TodoList, TodoItem)
|
||||
src/Http/Controllers/ Request handlers (Auth, EmailVerification, Project, Card)
|
||||
src/Repository/ Database access (User, EmailVerification, Project, Card)
|
||||
src/Support/Validator.php Request-body validation helper
|
||||
migrations/*.sql Schema, applied by bin/migrate.php
|
||||
Dockerfile PHP 8.3 + Apache image
|
||||
|
||||
Reference in New Issue
Block a user