hoangsonww / hoangsonww/ToDo-App-NextJS-Fullstack
Feature: Shared Lists & Real-Time Collaboration (invites, roles, presence, activity log)
- Dominant language
- TypeScript
- Stars
- 27
- Forks
- 18
- PR merge metrics
- No merged PRs in 30d
Description
## Summary
Introduce **shared lists** so multiple users can collaborate on the same to-do board with **role-based permissions**, **invite links**, **live presence** (who’s online/typing), and an **activity timeline**. Reuses the existing Next.js BE/FE + WebSockets foundation and fits cleanly into the current `/api/todos` surface.
## Why
* Today each user manages their own list; collaboration requires copy/paste or account sharing.
* Teams and families expect shared lists, live updates, and a simple invite flow.
* Makes excellent use of your existing WebSocket plumbing and SSR-friendly UI.
---
## User Stories
* As an owner, I can **create a shared list** and invite collaborators via **link** or **email**.
* As a collaborator, I can **accept an invite** and immediately see/add/update tasks with live updates.
* As an owner, I can set roles: **Owner**, **Editor**, **Viewer**.
* As any member, I can see **who’s online** on a list and when someone is **typing/editing** a task.
* As a member, I can view an **activity timeline** (added/edited/completed, by whom, when).
* As an owner, I can **revoke** access and **transfer ownership**.
---
## UX / IA
### New navigation
* **Lists** switcher (top-left): `My Personal`, `Work`, `Groceries`, `+ New List`
* **Share** button on the list header → opens a modal:
* Invite via email (optional)
* **Invite link** (copy to clipboard)
* Members table: name, role (dropdown), remove
### Task interactions (unchanged UX)
* Tasks remain per-list. Filters and categories still apply, scoped to the active list.
* Presence: avatars pill in the header; typing indicators inline on the task being edited.
* **Activity** side panel (right drawer): chronological feed with compact entries.
---
## Data Model (storage-agnostic)
> Keep names generic; adapt to your current SQLite/ORM layer.
```ts
// Core entities
type ListRole = 'OWNER' | 'EDITOR' | 'VIEWER';
interface List {
id: string; // uuid
name: string;
ownerId: string; // user id
createdAt: string;
updatedAt: string;
inviteCode?: string; // short slug for link invites (rotatable)
}
interface ListMember {
id: string; // uuid
listId: string;
userId: string;
role: ListRole;
createdAt: string;
}
interface Todo {
id: string;
listId: string; // <-- add this
title: string;
description?: string;
completed: boolean;
category?: string;
dueDate?: string;
createdAt: string;
updatedAt: string;
updatedBy?: string; // user id for activity attribution
}
interface Activity {
id: string;
listId: string;
actorId: string;
type:
| 'TASK_CREATED'
| 'TASK_UPDATED'
| 'TASK_COMPLETED'
| 'TASK_DELETED'
| 'MEMBER_ADDED'
| 'MEMBER_REMOVED'
| 'ROLE_CHANGED'
| 'LIST_RENAMED';
payload: Record; // e.g., {taskId, fieldsChanged}
createdAt: string;
}
```
**Indexes suggested**
* `ListMember(listId, userId)` unique
* `Todo(listId)`; `Activity(listId, createdAt desc)`
---
## API (Next.js /api)
> Extend current handlers; keep auth middleware as is.
```
# Lists
POST /api/lists { name } → List
GET /api/lists → [List] (user is member)
GET /api/lists/:id → List (+ members if requester is member)
PATCH /api/lists/:id { name? } (OWNER)
DELETE /api/lists/:id (OWNER)
# Memberships
POST /api/lists/:id/members { userEmail, role } (OWNER)
PATCH /api/lists/:id/members/:uid { role } (OWNER)
DELETE /api/lists/:id/members/:uid (OWNER)
# Invite links
POST /api/lists/:id/invite/rotate → { inviteUrl } (OWNER)
GET /api/invite/:code → accept join (auth’d), adds as EDITOR by default
# Activity
GET /api/lists/:id/activity?after= → Activity[]
# Todos (add listId to payloads & queries)
GET /api/todos?listId=:id
POST /api/todos { listId, title, ... }
PUT /api/todos { id, listId, ... }
PATCH /api/todos { id, listId, completed }
DELETE /api/todos { id, listId }
```
**OpenAPI**
* Update `openapi.yaml` with `List`, `ListMember`, `Activity` schemas and `listId` on Todo routes.
* Document 403 responses for permission failures.
---
## WebSockets
Use your existing WS channel; namespace by list:
**Channels**
* `list:{listId}`
**Events**
* Server → clients:
* `presence:update` `{ membersOnline: UserPresence[] }`
* `todo:created|updated|deleted` `{ todo }`
* `todo:reordered` `{ ids: string[] }` (if reordering is supported)
* `activity:append` `{ activity }`
* Client → server:
* `presence:ping` `{ listId }`
* `typing:start|stop` `{ listId, todoId }`
Presence can be a lightweight TTL map keyed by socket id + user id.
---
## Permissions
* **OWNER**: everything, including delete list, rotate invite, manage roles.
* **EDITOR**: CRUD todos, view members, cannot manage roles or delete the list.
* **VIEWER**: read-only; cannot modify todos or members.
* All endpoints: assert membership + role before action.
* WebSocket: authorize on subscribe; ignore unauthorized events.
---
## Activity Log
Append on:
* task create/update/complete/delete
* member add/remove/role change
* list rename
Keep entries compact; render relative timestamps in UI. Paginate `GET /activity`.
---
## Email (optional)
If SMTP is configured, support email invites in addition to link invites.
* Env: `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`, `MAIL_FROM`
* Fallback: invite link copy-to-clipboard only.
---
## Acceptance Criteria
* Create a list; invite a second account; both can view the same todos.
* Moving/adding/completing a task by user A updates **instantly** for user B over WebSockets.
* Role enforcement works (viewer cannot edit; editor cannot manage roles).
* Invite link join works; rotating the link invalidates the old one.
* Activity panel shows accurate entries with actor, action, and time.
* Presence shows online members; typing indicator appears while editing a task.
---
## Security & Edge Cases
* Invite codes are **unguessable** (e.g., 22-char base62). Rotation invalidates previous code.
* Prevent leaking list existence: 404 for non-members even if the id exists.
* On member removal, immediately disconnect their WS subscription for that list.
* Rate-limit invite acceptance to prevent abuse.
* Timezone awareness for timestamps; use ISO in API, format client-side.
---
## Migration Plan
1. Add `lists`, `list_members`, `activity` tables and `listId` column to `todos`.
2. Backfill existing todos into a default personal list per user.
3. Ship feature behind `FEATURE_SHARED_LISTS=true` (env or config).
4. Gradual rollout: enable for maintainers → 10% → 100%.
---
## Task Breakdown
### Backend
* [ ] DB migrations (tables + indexes; `listId` on todos).
* [ ] Auth guard helpers: `requireMember(listId, role?)`.
* [ ] Lists CRUD + membership endpoints.
* [ ] Invite link endpoints + code rotation.
* [ ] Extend todos handlers to require `listId`.
* [ ] Activity writer + `/activity` endpoint.
* [ ] WS: add `list:{listId}` namespace, presence + typing events.
* [ ] Tests: permissions, invite flow, activity append.
### Frontend
* [ ] List switcher UI + create/rename/delete.
* [ ] Share modal (invite link + members/roles table).
* [ ] Presence avatars + typing badges on task rows.
* [ ] Activity drawer with virtualized list.
* [ ] WS client: subscribe/unsubscribe by active list.
* [ ] Empty states, toasts, optimistic updates.
* [ ] Update Swagger page & types.
### DevOps / Config
* [ ] Env flags: `FEATURE_SHARED_LISTS`, optional SMTP vars.
* [ ] Update `openapi.yaml` + screenshots in README.
* [ ] CI: run DB migrations step; add basic e2e for invite flow.
---
## Stretch Goals (follow-ups)
* **Public read-only links** for a list (no login required, optional password).
* **Task assignments** (member chips + “My tasks” filter).
* **Calendar view** of due dates and ICS export.
* **Comment threads** per task with @mentions and notifications.
---
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.