diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2727c9b --- /dev/null +++ b/.env.example @@ -0,0 +1,31 @@ +# Auth.js / NextAuth — required for production. Generate: openssl rand -base64 32 +AUTH_SECRET= + +# Public URL of the app (required in production, e.g. Vercel). No trailing slash. +# Dev default is http://localhost:3000 if unset. +# AUTH_URL=https://your-project.vercel.app + +# --- Producción en Vercel (base compartida) --- +# Si defines ambas, la app usa Turso (libSQL) en lugar del archivo SQLite local. +# Crea una DB en https://turso.tech y copia URL + token. +# TURSO_DATABASE_URL=libsql://your-db.turso.io +# TURSO_AUTH_TOKEN= + +# Con Turso: si la tabla users está vacía, se insertan pm@ / dev@ (password123) como en Vercel /tmp. +# Para producción sin cuentas demo, define: +# TURSO_SKIP_DEMO_USERS=1 + +# Vercel sin Turso: SQLite en /tmp; demo users si users está vacío. Desactivar: +# VERCEL_SKIP_DEMO_USERS=1 + +# Board chat (Socket.io). Run: bun run socket — default http://localhost:3001 +# NEXT_PUBLIC_CHAT_SOCKET_URL=http://localhost:3001 +# SOCKET_CHAT_PORT=3001 + +# LiveKit (voice/video). Create a project at https://cloud.livekit.io — never commit real secrets. +# Browser WebSocket URL (same as LIVEKIT_URL if you use wss://) +# NEXT_PUBLIC_LIVEKIT_URL=wss://your-project.livekit.cloud +# Server token + Room API (optional duplicate of URL for server-only env) +# LIVEKIT_URL=wss://your-project.livekit.cloud +# LIVEKIT_API_KEY= +# LIVEKIT_API_SECRET= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2837422 --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +!.env.example + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# SQLite (local dev) +/data/ + +# Profile uploads (keep folder via .gitkeep) +/public/uploads/profiles/* +!/public/uploads/profiles/.gitkeep diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..1a49b56 --- /dev/null +++ b/README.en.md @@ -0,0 +1,142 @@ +# Mid-Level Fullstack Challenge — Solution + +Solution for [Red-Valley/mid-fullstack-challenge](https://github.com/Red-Valley/mid-fullstack-challenge): a **Kanban-style task board** with a **REST API**, **SQLite**, and a **Next.js (App Router)** UI styled with **Tailwind CSS**. Input is validated with **Zod**; responses use a single JSON shape for success and errors. + +**Runtime:** [Bun](https://bun.sh) (per the brief) or **Node.js ≥ 22.5** (required for [`node:sqlite`](https://nodejs.org/api/sqlite.html)). The same codebase runs with `bun` or `npm`. + +*[Versión en español →](./README.md)* + +--- + +## Quick start + +Copy `.env.example` to `.env.local` and set **`AUTH_SECRET`** (e.g. `openssl rand -base64 32`). Auth.js requires it for sign-in and production builds. + +```bash +# Install dependencies (use one package manager consistently) +bun install +# or: npm install + +# Sample board + demo users (see below) +bun run seed +# or: npm run seed + +# Dev server +bun run dev +# or: npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000) — you are sent to **/login** if not signed in. After seeding, demo users: + +| Email | Role | Password | +| ----- | ---- | -------- | +| `pm@example.com` | PM | `password123` | +| `dev@example.com` | DEVELOPER | `password123` | + +**PM** can create boards (`/create-board`), columns, tasks, assignees, and delete tasks. **DEVELOPER** can move tasks (drag-and-drop / status) and add comments; “New board” and task delete are hidden and API blocks those actions. + +### Node version (npm / no Bun) + +If you use **nvm** (e.g. nvm-windows), the repo includes `.nvmrc` (`22`): + +```bash +nvm install 22 +nvm use 22 +node -v # should be v22.x (≥ 22.5) +``` + +--- + +## What’s included + +| Area | Notes | +| ---- | ----- | +| **Boards & columns** | Create boards; add columns with display order (unique per board). | +| **Tasks** | Create, update (including move to another column on the **same** board), delete. | +| **API** | All required routes; validation; HTTP 400 / 404 / 409 where appropriate; consistent JSON envelope. | +| **UI** | Board list, Kanban columns, **modal** for new tasks, **dropdown** to move tasks, **drag-and-drop** between columns (extra), loading and empty states. | +| **Data** | SQLite file `data/app.db` (created on first use; **gitignored**). | +| **Seed** | `scripts/seed.ts` — one sample board with columns and tasks. | + +### Optional fields (beyond the minimum brief) + +Tasks also support **`taskType`** (`bug` \| `story` \| `task`), **`assigneeName`** (shown as initials on cards), and the UI includes priority badges and a light “enterprise” layout. The minimum schema required by the challenge (name/creation date, columns, task title/description/priority/creation date) is fully satisfied. + +--- + +## Scripts + +| Command | Description | +| ------- | ----------- | +| `bun run dev` / `npm run dev` | Next.js dev server (Turbopack) | +| `bun run build` / `npm run build` | Production build | +| `bun run start` / `npm start` | Run production build | +| `bun run seed` / `npm run seed` | Seed **Sample board** (idempotent: replaces an existing board with the same name) | +| `bun run lint` / `npm run lint` | ESLint | + +--- + +## API overview + +All JSON responses follow: + +- **Success:** `{ "ok": true, "data": … }` +- **Error:** `{ "ok": false, "error": { "code", "message", "details?" } }` + +| Method | Path | Description | +| ------ | ---- | ----------- | +| `GET` | `/api/boards` | List boards | +| `POST` | `/api/boards` | Body: `{ "name" }` | +| `GET` | `/api/boards/:id` | Board with columns and nested tasks | +| `POST` | `/api/columns` | Body: `{ "boardId", "name", "displayOrder" }` | +| `POST` | `/api/tasks` | Body: `{ "columnId", "title", "description?", "priority?", "taskType?", "assigneeName?" }` | +| `PATCH` | `/api/tasks/:id` | Partial update: `title`, `description`, `columnId` (same board only), `priority`, `taskType`, `assigneeName` | +| `DELETE` | `/api/tasks/:id` | Delete task | + +Implementation details: `src/app/api/**`, schemas in `src/lib/schemas.ts`, helpers in `src/lib/api-response.ts`. + +--- + +## Architecture & design decisions + +- **`src/lib/db.ts`** — SQLite (`data/app.db`), schema, indexes, foreign keys (`ON DELETE CASCADE`), WAL. Uses **`node:sqlite`** so there are no native addon build steps (helpful on Windows). A small **migration** adds columns on existing DBs if the schema evolved after first run. +- **`src/lib/schemas.ts` + `src/lib/api-response.ts`** — Zod on every mutating route and id params; shared error shape and Zod flattening for 400s. +- **`src/app/api/**`** — Thin handlers: parse → validate → query → return JSON (no ORM). +- **`src/app/page.tsx`** — Boards list and create board. +- **`src/app/boards/[id]/page.tsx`** — Kanban: columns, task cards (`src/components/kanban/`), modal for new tasks, move via select and optional drag-and-drop. + +**Trade-offs:** `node:sqlite` is still experimental in Node; requiring **≥ 22.5** keeps behavior predictable. Column `display_order` is unique per board (simple; production might use fractional indexes or a reorder endpoint). Task order within a column follows `created_at` (moving columns updates `column_id` only; no intra-column sort key). + +--- + +## Database + +- **Type:** SQLite (single file). +- **Location:** `data/app.db` under the project root (ignored by git). +- **Inspect:** Any SQLite client (e.g. [DB Browser for SQLite](https://sqlitebrowser.org/)) or the CLI, while the dev server is stopped or read-only if your tool allows it. + +--- + +## Deploying to Vercel + +1. Connect the repo to [Vercel](https://vercel.com), **Next.js** preset, Node **22.x**. +2. Minimum production env vars: **`AUTH_SECRET`** and **`AUTH_URL`** (your public deployment URL, no trailing slash). +3. **SQLite on Vercel:** the app stores the DB under **`/tmp`** on serverless and seeds **demo** users `pm@example.com` / `dev@example.com` (`password123`) when the users table is empty. Boards do not persist across instances like locally. For real production use a **remote database** or persistent disk (see `docs/vercel-deploy.md`). +4. **Socket.io** (`bun run socket`) must run as a **separate service**; on Vercel set **`NEXT_PUBLIC_CHAT_SOCKET_URL`** to that service’s public URL. + +Full checklist (env vars, LiveKit, caveats): **[`docs/vercel-deploy.md`](./docs/vercel-deploy.md)** (Spanish). Root **`vercel.json`** sets the Next.js framework preset. + +--- + +## AI assistance + +Much of this project was built with **Claude Code** and **Cursor** (agent, planning, and assisted editing), using prompt-style workflows aimed at speed and quality — along the lines of resources such as *“7 prompts to code faster”* (PDF used as a reference in the author’s workflow). + +In Cursor, **Skills** and specialized contexts included, among others: + +- **Software architecture** — layering, API design, data boundaries, and domain scope. +- **Backend development** — REST routes, validation, SQLite, and consistent JSON contracts. +- **Voice over IP (VoIP) and real-time communications** — as they apply to the board (chat, calls / rooms, signaling, and media considerations). +- **Streaming and live data** — patterns for near–real-time events, sockets, or equivalent channels used in the solution. + +Everything was **manually reviewed and tuned** to meet the challenge brief and to be explainable in a technical review. diff --git a/README.md b/README.md index 4b5193e..2506c19 100644 --- a/README.md +++ b/README.md @@ -1,107 +1,142 @@ -## Technical Challenge — Mid-Level +# Challenge Fullstack Mid-Level — Solución -### Background +Solución para [Red-Valley/mid-fullstack-challenge](https://github.com/Red-Valley/mid-fullstack-challenge): un **tablero tipo Kanban** con **API REST**, **SQLite** e interfaz **Next.js (App Router)** con **Tailwind CSS**. La entrada se valida con **Zod**; las respuestas usan un único formato JSON para éxito y error. -A small project management startup wants to build a simple internal tool for their team to organize work visually. They need a basic task board where team members can create boards, organize tasks into columns, and move tasks between stages. +**Runtime:** [Bun](https://bun.sh) (según el brief) o **Node.js ≥ 22.5** (necesario para [`node:sqlite`](https://nodejs.org/api/sqlite.html)). El mismo código funciona con `bun` o `npm`. -In this challenge, you'll build a simplified task board application with a REST API, a database, and a functional UI. +*[English version →](./README.en.md)* -We are **not evaluating specific tools or patterns**. We simply want to understand how you think, how you code, and how you approach real-world problems. Be yourself. +--- +## Inicio rápido -### What You Need to Build +Copia `.env.example` a `.env.local` y define **`AUTH_SECRET`** (p. ej. salida de `openssl rand -base64 32`). Sin esto, Auth.js fallará al iniciar sesión o al hacer build. -A functional **full stack application** with the ability to: +```bash +# Instalar dependencias (usa un solo gestor de paquetes de forma consistente) +bun install +# o: npm install -1. Create and view boards -2. Add columns to a board -3. Create, update, and delete tasks within columns -4. Move tasks between columns -5. View a board in a kanban-style layout +# Carga tablero de ejemplo y usuarios demo (ver abajo) +bun run seed +# o: npm run seed +# Servidor de desarrollo +bun run dev +# o: npm run dev +``` -### Database Schema +Abre [http://localhost:3000](http://localhost:3000) → te redirige a **/login** si no hay sesión. Tras el seed, usuarios de prueba: -Design the schema yourself. At minimum, you should support: +| Email | Rol | Contraseña | +| ----- | --- | ------------ | +| `pm@example.com` | PM | `password123` | +| `dev@example.com` | DEVELOPER | `password123` | -- **Boards** with a name and creation date -- **Columns** belonging to a board, with a name and display order -- **Tasks** belonging to a column, with: title, description, priority, and creation date +El **PM** puede crear tableros (`/create-board`), columnas, tareas, asignar responsables y eliminar. El **DEVELOPER** mueve tareas (arrastrar o menú según UI) y comenta; no crea tableros ni ve “New board” / eliminar tareas. -Include appropriate indexes and a seed script that creates one board with sample data. +### Versión de Node (npm / sin Bun) +Si usas **nvm** (por ejemplo nvm-windows), el repo incluye `.nvmrc` (`22`): -### Tech Stack +```bash +nvm install 22 +nvm use 22 +node -v # debería ser v22.x (≥ 22.5) +``` -#### Backend +--- -* Runtime: **Bun** -* Framework: **Next.js** (App Router) -* Database: **SQLite** (ORM, query builder, or raw SQL — your choice) +## Qué incluye -#### Frontend +| Área | Notas | +| ---- | ----- | +| **Tableros y columnas** | Crear tableros; añadir columnas con orden de visualización (único por tablero). | +| **Tareas** | Crear, actualizar (incluido mover a otra columna del **mismo** tablero), eliminar. | +| **API** | Todas las rutas requeridas; validación; HTTP 400 / 404 / 409 cuando corresponde; envoltorio JSON uniforme. | +| **UI** | Lista de tableros, columnas Kanban, **modal** para nuevas tareas, **desplegable** para mover tareas, **arrastrar y soltar** entre columnas (extra), estados de carga y vacíos. | +| **Datos** | Archivo SQLite `data/app.db` (se crea al primer uso; **ignorado por git**). | +| **Seed** | `scripts/seed.ts` — un tablero de ejemplo con columnas y tareas. | -* Framework: **Next.js** -* Styling: **TailwindCSS** -* Additional UI libraries are welcome but not required +### Campos opcionales (más allá del mínimo del brief) +Las tareas también admiten **`taskType`** (`bug` \| `story` \| `task`), **`assigneeName`** (mostrado como iniciales en las tarjetas), y la UI incluye badges de prioridad y un layout ligero tipo “enterprise”. El esquema mínimo del challenge (nombre/fecha de creación, columnas, título/descripción/prioridad/fecha de tarea) queda **totalmente cubierto**. -### Required API Endpoints +--- -| Method | Endpoint | Description | -|--------|----------|-------------| -| GET | `/api/boards` | List all boards | -| POST | `/api/boards` | Create a board | -| GET | `/api/boards/:id` | Get a board with its columns and tasks | -| POST | `/api/columns` | Create a column (linked to a board) | -| POST | `/api/tasks` | Create a task (linked to a column) | -| PATCH | `/api/tasks/:id` | Update a task (title, description, move to another column) | -| DELETE | `/api/tasks/:id` | Delete a task | +## Scripts -- Validate input on every endpoint. -- Return proper HTTP status codes (400, 404, etc.). -- Use a consistent JSON response structure. +| Comando | Descripción | +| ------- | ----------- | +| `bun run dev` / `npm run dev` | Servidor de desarrollo Next.js (Turbopack) | +| `bun run build` / `npm run build` | Build de producción | +| `bun run start` / `npm start` | Ejecutar el build de producción | +| `bun run seed` / `npm run seed` | Seed del tablero **Sample board** (idempotente: sustituye un tablero existente con el mismo nombre) | +| `bun run lint` / `npm run lint` | ESLint | +--- -### Required UI +## Resumen de la API -1. A page showing a board in **kanban-style layout** (columns side by side, tasks as cards) -2. Ability to **create a new task** via a modal or dialog -3. Ability to **move a task** between columns (a simple dropdown is fine, no drag-and-drop required) -4. Loading and empty states +Todas las respuestas JSON siguen: +- **Éxito:** `{ "ok": true, "data": … }` +- **Error:** `{ "ok": false, "error": { "code", "message", "details?" } }` -### Submission Instructions +| Método | Ruta | Descripción | +| ------ | ---- | ----------- | +| `GET` | `/api/boards` | Listar tableros | +| `POST` | `/api/boards` | Cuerpo: `{ "name" }` | +| `GET` | `/api/boards/:id` | Tablero con columnas y tareas anidadas | +| `POST` | `/api/columns` | Cuerpo: `{ "boardId", "name", "displayOrder" }` | +| `POST` | `/api/tasks` | Cuerpo: `{ "columnId", "title", "description?", "priority?", "taskType?", "assigneeName?" }` | +| `PATCH` | `/api/tasks/:id` | Actualización parcial: `title`, `description`, `columnId` (solo mismo tablero), `priority`, `taskType`, `assigneeName` | +| `DELETE` | `/api/tasks/:id` | Eliminar tarea | -* **Fork this repository**, complete your work, and **submit a pull request**. -* Include a `README.md` with: - * Clear instructions to run the project locally - * A short explanation of your architecture or design decisions - * A seed script to preload sample data +Detalle de implementación: `src/app/api/**`, esquemas en `src/lib/schemas.ts`, helpers en `src/lib/api-response.ts`. +--- -### Time Expectation +## Arquitectura y decisiones de diseño -You should spend no more than **2 hours** on this task. +- **`src/lib/sqlite-local.ts` + `src/lib/db.ts`** — En local: SQLite en `data/app.db` con **`node:sqlite`**. Si existen **`TURSO_DATABASE_URL`** y **`TURSO_AUTH_TOKEN`**, la app usa **Turso** (`@libsql/client`) para una base compartida en serverless. Las consultas pasan por **`sqlGet` / `sqlAll` / `sqlRun`** (async). +- **`src/lib/schemas.ts` + `src/lib/api-response.ts`** — Zod en todas las rutas que mutan y en los ids; formato de error compartido y flatten de Zod para 400. +- **`src/app/api/**`** — Handlers finos: parsear → validar → consultar → devolver JSON (sin ORM). +- **`src/app/page.tsx`** — Lista de tableros y crear tablero. +- **`src/app/boards/[id]/page.tsx`** — Kanban: columnas, tarjetas (`src/components/kanban/`), modal para nuevas tareas, mover con ` setColName(e.target.value)} + placeholder={tx("board.columnNamePlaceholder")} + /> + + + + + + ) : null} + + {b.columns.length === 0 ? ( +

+ {tx("board.noColumns")}{" "} + + bun run seed + {" "} + /{" "} + + npm run seed + + . +

+ ) : ( +
+ {b.columns.map((col) => ( + +
+

+ {col.name} +

+

+ {tx("board.tasksCount", { + count: col.tasks.length, + order: col.displayOrder, + })} +

+
+ + +
+ ))} +
+ )} + + + + + {activeTask ? ( +
+
+ +

+ {activeTask.title} +

+
+
+ ) : null} +
+ + {modalOpen ? ( +
+
+

+ {tx("board.modalNewTask")} +

+
+ + +