From fe669d8adc9410fb0ce66b30be9477cf28fae1e3 Mon Sep 17 00:00:00 2001 From: DannMarTvMarket Date: Wed, 1 Apr 2026 14:12:17 -0500 Subject: [PATCH 1/2] Initial commit: Fullstack technical test project with board management --- ANALISIS_TECNICO_COMPLETO.md | 430 +++++++++++ app/.gitignore | 41 ++ app/README.md | 62 ++ app/bun.lock | 969 +++++++++++++++++++++++++ app/eslint.config.mjs | 18 + app/local.db | Bin 0 -> 32768 bytes app/next.config.ts | 7 + app/package.json | 39 + app/postcss.config.mjs | 7 + app/public/avatars/Profile.jpg | Bin 0 -> 1074502 bytes app/public/avatars/default.svg | 5 + app/public/file.svg | 1 + app/public/globe.svg | 1 + app/public/next.svg | 1 + app/public/vercel.svg | 1 + app/public/window.svg | 1 + app/src/app/api/boards/[id]/route.ts | 87 +++ app/src/app/api/boards/route.ts | 41 ++ app/src/app/api/columns/[id]/route.ts | 37 + app/src/app/api/columns/route.ts | 45 ++ app/src/app/api/tasks/[id]/route.ts | 78 ++ app/src/app/api/tasks/route.ts | 43 ++ app/src/app/board/[id]/BoardClient.tsx | 731 +++++++++++++++++++ app/src/app/board/[id]/page.tsx | 73 ++ app/src/app/favicon.ico | Bin 0 -> 25931 bytes app/src/app/globals.css | 26 + app/src/app/layout.tsx | 33 + app/src/app/page.tsx | 65 ++ app/src/db/client.ts | 102 +++ app/src/db/schema.ts | 60 ++ app/src/db/seed.ts | 124 ++++ app/tsconfig.json | 34 + 32 files changed, 3162 insertions(+) create mode 100644 ANALISIS_TECNICO_COMPLETO.md create mode 100644 app/.gitignore create mode 100644 app/README.md create mode 100644 app/bun.lock create mode 100644 app/eslint.config.mjs create mode 100644 app/local.db create mode 100644 app/next.config.ts create mode 100644 app/package.json create mode 100644 app/postcss.config.mjs create mode 100644 app/public/avatars/Profile.jpg create mode 100644 app/public/avatars/default.svg create mode 100644 app/public/file.svg create mode 100644 app/public/globe.svg create mode 100644 app/public/next.svg create mode 100644 app/public/vercel.svg create mode 100644 app/public/window.svg create mode 100644 app/src/app/api/boards/[id]/route.ts create mode 100644 app/src/app/api/boards/route.ts create mode 100644 app/src/app/api/columns/[id]/route.ts create mode 100644 app/src/app/api/columns/route.ts create mode 100644 app/src/app/api/tasks/[id]/route.ts create mode 100644 app/src/app/api/tasks/route.ts create mode 100644 app/src/app/board/[id]/BoardClient.tsx create mode 100644 app/src/app/board/[id]/page.tsx create mode 100644 app/src/app/favicon.ico create mode 100644 app/src/app/globals.css create mode 100644 app/src/app/layout.tsx create mode 100644 app/src/app/page.tsx create mode 100644 app/src/db/client.ts create mode 100644 app/src/db/schema.ts create mode 100644 app/src/db/seed.ts create mode 100644 app/tsconfig.json diff --git a/ANALISIS_TECNICO_COMPLETO.md b/ANALISIS_TECNICO_COMPLETO.md new file mode 100644 index 0000000..7309a22 --- /dev/null +++ b/ANALISIS_TECNICO_COMPLETO.md @@ -0,0 +1,430 @@ +# 🔍 ANÁLISIS TÉCNICO COMPLETO - KANBAN BOARD +## Senior Fullstack Developer Review + +**Fecha:** Abril 2025 +**Proyecto:** Mid-Fullstack Technical Test - Kanban Board +**Stack:** Next.js (App Router) + Bun + Drizzle ORM + SQLite + Tailwind CSS + +--- + +## 📊 RESUMEN EJECUTIVO + +### Score: 8.5/10 (Mid-Level +) + +| Aspecto | Calidad | Notas | +|---------|---------|-------| +| **Arquitectura** | ✅ Excelente | Server/Client separation clara, SSR + ISR | +| **Type Safety** | ✅ Muy Bueno | TypeScript bien utilizado, minor deprecation | +| **API Consistency** | ✅ Excelente | Todos los status codes correctos (201, 200, 400, 404) | +| **Estado Local** | ✅ Excelente | Optimistic updates con rollback, UX instant | +| **Validación** | ✅ Excelente | Zod en API routes, params async con coercion | +| **BD Schema** | ✅ Muy Bueno | Relaciones correctas, cascades, índices | +| **UI/UX** | ✅ Bueno | Responsive, overflow-x-auto en columnas, accesibility | +| **Error Handling** | ⚠️ Aceptable | Podría mejorar tipado de errores API | + +--- + +## 🔴 ERRORES ENCONTRADOS + +### 1️⃣ **DEPRECATED API - MINOR** + +**Archivo:** `BoardClient.tsx` (línea 46) +**Severidad:** ⚠️ Medium (deprecation warning en TypeScript 5.5+) + +```typescript +// ❌ ACTUAL (DEPRECATED) +async function api(input: RequestInfo, init?: RequestInit): Promise { + const res = await fetch(input, { + ...init, + headers: { + "content-type": "application/json", + ...(init?.headers ?? {}), + }, + }); + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(text || `Request failed (${res.status})`); + } + return (await res.json()) as T; +} + +// ✅ CORRECCIÓN +async function api(input: string | Request, init?: RequestInit): Promise { + const res = await fetch(input, { + ...init, + headers: { + "content-type": "application/json", + ...(init?.headers ?? {}), + }, + }); + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(text || `Request failed (${res.status})`); + } + return (await res.json()) as T; +} +``` + +**Por qué:** `RequestInfo` fue deprecado. La alternativa moderna es `string | Request`. + +--- + +### 2️⃣ **TYPE FRAGILITY en Priority Enum** + +**Archivo:** `api/boards/[id]/route.ts` (línea 50-51) +**Severidad:** 🟡 Low (pero frágil para el futuro) + +```typescript +// ❌ ACTUAL (FUNCIONA pero es frágil) +const tsks = await db + .select() + .from(tasks) + .where(eq(tasks.boardId, boardId)) + .orderBy(tasks.order); + +const tasksByColumnId = new Map(); +for (const t of tsks.map((x) => ({ ...x, priority: x.priority ?? "medium" }))) { + // Asume que priority puede ser NULL + const arr = tasksByColumnId.get(t.columnId) ?? []; + arr.push(t); + tasksByColumnId.set(t.columnId, arr); +} + +// ✅ MEJOR (TYPE-SAFE) +// En schema.ts, priority YA TIENE .notNull() ✓ +// En la consulta, Drizzle infiere que priority: "low" | "medium" | "high" +const tsks = await db + .select() + .from(tasks) + .where(eq(tasks.boardId, boardId)) + .orderBy(tasks.order); + +// Aquí priority NO puede ser null, pero si existían registros legacy: +const tasksByColumnId = new Map(); +for (const t of tsks) { + const arr = tasksByColumnId.get(t.columnId) ?? []; + arr.push(t); + tasksByColumnId.set(t.columnId, arr); +} +``` + +**Por qué:** Tu schema.ts tiene `.notNull().default("medium")`, así que si TODOS los registros tienen prioridad, no necesitas el `??` operator. Pero el código es defensivo (buena práctica para legacy DBs). + +--- + +### 3️⃣ **METADATA DEFAULT en Root Layout** + +**Archivo:** `layout.tsx` (línea 12-14) +**Severidad:** 🟢 Cosmético (pero visible en Google) + +```typescript +// ❌ ACTUAL (TEMPLATE DEFAULT) +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +// ✅ CORRECCIÓN +export const metadata: Metadata = { + title: "Kanban Board | Project Sync", + description: "Collaborative Kanban board for task management and team workflow optimization", + openGraph: { + title: "Kanban Board - Project Sync", + description: "Manage your projects with visual task cards and real-time updates", + type: "website", + }, +}; +``` + +--- + +### 4️⃣ **HOME PAGE UNUSED - REFACTOR** + +**Archivo:** `page.tsx` (root page) +**Severidad:** 🟢 UX (confusión para usuarios) + +```typescript +// ❌ ACTUAL (REDIRIGE A TEMPLATE DEFAULT) +export default function Home() { + return ( +
+ {/* Template boilerplate */} +
+ ); +} + +// ✅ MEJOR - OPCIÓN 1 (REDIRECT A PRIMER BOARD) +"use client"; +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; + +export default function Home() { + const router = useRouter(); + + useEffect(() => { + // Redirige al primer board o a una página de "create board" + router.push("/board/1"); + }, [router]); + + return
Cargando...
; +} + +// ✅ MEJOR - OPCIÓN 2 (LANDING PAGE LISTA DE BOARDS) +import Link from "next/link"; +import { db, ensureSchema } from "@/db/client"; +import { boards } from "@/db/schema"; + +export default async function Home() { + await ensureSchema(); + const allBoards = await db.select().from(boards); + + return ( +
+
+

My Kanban Boards

+ + {allBoards.length === 0 ? ( +
+

No boards created yet

+ + Create First Board + +
+ ) : ( +
+ {allBoards.map(b => ( + +

{b.name}

+

{b.description}

+ + ))} +
+ )} +
+
+ ); +} +``` + +--- + +## ✅ LO QUE ESTÁ BIEN + +### 1. **API Route Handlers - PERFECTO** + +Todos los endpoints con status codes correctos: + +```typescript +// POST /api/boards → 201 CREATED ✅ +// GET /api/boards → 200 OK ✅ +// GET /api/boards/[id] → 200 OK / 404 NOT FOUND ✅ +// POST /api/columns → 201 CREATED ✅ +// DELETE /api/columns/[id] → 200 OK / 404 NOT FOUND ✅ +// POST /api/tasks → 201 CREATED ✅ +// PATCH /api/tasks/[id] → 200 OK / 404 NOT FOUND ✅ +// DELETE /api/tasks/[id] → 200 OK / 404 NOT FOUND ✅ +``` + +### 2. **Schema Relations - EXCELENTE** + +```typescript +// Drizzle relations bidireccionales +export const boardsRelations = relations(boards, ({ many }) => ({ + columns: many(columns), // 1:N + tasks: many(tasks), // 1:N (directo) +})); + +export const columnsRelations = relations(columns, ({ one, many }) => ({ + board: one(boards, {...}), // N:1 + tasks: many(tasks), // 1:N +})); + +export const tasksRelations = relations(tasks, ({ one }) => ({ + board: one(boards, {...}), // N:1 + column: one(columns, {...}),// N:1 +})); +``` + +**Por qué es Senior:** +- Evita queries N+1 +- Type-safe joins +- Cascading deletes correctos + +### 3. **Optimistic Updates - SENIOR LEVEL** + +```typescript +async function onMoveTask(taskId: number, toColumnId: number) { + setBusyTaskIds((s) => new Set(s).add(taskId)); + const fromBoard = board; // 🔑 SAVEPOINT + try { + moveTaskLocally(taskId, toColumnId); // 🎯 OPTIMISTIC UI + await api(`/api/tasks/${taskId}`, { + method: "PATCH", + body: JSON.stringify({ columnId: toColumnId }), + }); + updateTaskLocally(taskId, (t) => ({ ...t, columnId: toColumnId })); + } catch (e) { + setBoard(fromBoard); // 🔄 ROLLBACK si falla + window.alert(...); + } +} +``` + +**Pattern:** Savepoint → Optimistic Update → Server Sync → Rollback on Error + +### 4. **Layout Responsive - EXCELENTE** + +```tsx + + +
+ {/* overflow-x-auto permite scroll horizontal */} + {columnsSorted.map(col => ( +
+ {/* min-w-[300px] previene squeeze */} +
+ ))} +
+``` + +**Por qué:** Kanban boards necesitan scroll horizontal en mobile. Tu layout lo maneja correctamente. + +### 5. **Database Schema with Migrations - MID-LEVEL+** + +```typescript +// client.ts: ensureSchema() con lightweight migrations +export async function ensureSchema() { + if (!schemaInitPromise) { + schemaInitPromise = (async () => { + // CREATE TABLE IF NOT EXISTS ✅ + // PRAGMA table_info() para migrations ✅ + // ALTER TABLE solo si falta columna ✅ + // CREATE INDEX para performance ✅ + })(); + } + await schemaInitPromise; +} +``` + +**Senior touches:** +- Promise memoization para evitar race conditions +- Schema versioning sin external migrations tool +- Índices estratégicos (board_id, column_id) + +--- + +## 📝 EXPLICACIONES TÉCNICAS (Para README/Entrevista) + +### **1. schema.ts** +> **Level: Mid-Level+** — Implementa relaciones Drizzle ORM bidireccionales (`relations`, `one()`, `many()`) con foreign key cascades para mantener integridad referencial. Las enums de prioridad en texto previenen queries inválidas mientras mantienen type-safety del TypeScript. Las columnas `created_at` usadas como timestamps Unix para portabilidad. + +### **2. page.tsx (board/[id])** +> **Level: Senior** — Combina Server Components para SSR + data fetching con Client Components para interactividad. Maneja `params: Promise` (Next.js 15 requirement) correctamente. Realiza dos queries: una via internal fetch a su propio API (cache: "no-store"), otra directa a BD (allBoards). Demuestra entendimiento de edge cases como la necesidad de `ensureSchema()` en contexto de servidor. + +### **3. BoardClient.tsx** +> **Level: Senior** — Optimistic UI updates con rollback: guarda snapshot del estado (`const fromBoard = board`), aplica cambios locales, sincroniza con servidor, revierte si hay error. Usa `useMemo()` para memoización de columnas ordenadas. Manejo granular de loading states (`busyTaskIds`, `deletingColumnIds`). Modal pattern con state isolation. Role-based access control (tech-lead vs jr). + +### **4. Route Handlers (/api/**)** +> **Level: Mid-Level** — Validación Zod en request body + params con coercion (`z.coerce.number()`). Async params handling (`await ctx.params`). Correct HTTP status codes: 201 (POST créé), 200 (success), 400 (validation fail), 404 (not found). Query composition con Drizzle: `select().where().orderBy()`. + +### **5. client.ts (Database Client)** +> **Level: Senior** — Lazy schema initialization con Promise memoization (`if (!schemaInitPromise)`). Lightweight migrations pattern usando `PRAGMA table_info()` para detectar nuevas columnas. Idempotent `CREATE TABLE IF NOT EXISTS` y `CREATE INDEX IF NOT EXISTS`. Manejo de legacy databases sin breaking changes. + +--- + +## 🚀 RECOMENDACIONES PARA MEJORAR A 9.5/10 + +### Priority 1: OBLIGATORIO (5 mins) +```typescript +// 1. BoardClient.tsx línea 46 +async function api(input: string | Request, init?: RequestInit): Promise + +// 2. layout.tsx línea 12-14 +export const metadata: Metadata = { + title: "Kanban Board | Project Sync", + description: "Collaborative Kanban board for task management", +}; +``` + +### Priority 2: RECOMENDADO (15 mins) +- [ ] Refactor home page para redirect a `/board/1` o landing page +- [ ] Agregar error boundaries en BoardClient +- [ ] Logging de errores API con estructura (timestamp, status, endpoint) + +### Priority 3: NICE-TO-HAVE (1 hour) +- [ ] Auth layer (next-auth o similar) +- [ ] Soft-delete para tasks/columns (audit trail) +- [ ] Real-time sync con WebSockets +- [ ] Drag-and-drop con `react-dnd` o `dnd-kit` + +--- + +## 🧪 TESTING CHECKLIST + +```bash +# Casos críticos para validar: +- [ ] POST /api/tasks con priority="invalid" → 400 +- [ ] GET /api/boards/999 → 404 +- [ ] DELETE /api/columns/1 (con tareas) → tasks también se borran (cascade) +- [ ] PATCH /api/tasks/1 → actualiza columnId +- [ ] Crear tarea, fallar servidor, rollback automático ✓ +- [ ] Crear columna, verificar next order+1 ✓ +``` + +--- + +## 📚 REFERENCIAS PARA ENTREVISTA + +1. **"¿Cómo manejas optimistic updates?"** + → `onMoveTask()`: snapshot estado → cambios locales → servidor sync → rollback si error + +2. **"¿Por qué Drizzle relations?"** + → Evita N+1 queries, type-safe joins, mejor que raw SQL + +3. **"¿Por qué async params en Next.js 15?"** + → Soporte para streaming + mejor performance en rutas dinámicas + +4. **"Schema migrations sin tool externo?"** + → `PRAGMA table_info()` detecta cambios, `ALTER TABLE IF NOT EXISTS` es idempotente + +5. **"¿Cómo garantizas type-safety entre API y client?"** + → Zod en servidor valida estructura, tipos TypeScript en client inferidos desde respuesta API + +--- + +## 💾 ARCHIVOS GENERADOS + +✅ Todos los Route Handlers ya existen: +- `api/boards/route.ts` ✅ +- `api/boards/[id]/route.ts` ✅ +- `api/columns/route.ts` ✅ +- `api/columns/[id]/route.ts` ✅ +- `api/tasks/route.ts` ✅ +- `api/tasks/[id]/route.ts` ✅ + +✅ Schema correcto: +- `db/schema.ts` ✅ +- `db/client.ts` ✅ + +⚠️ Pequeños ajustes: +- `BoardClient.tsx` línea 46 (deprecation) +- `layout.tsx` línea 12-14 (metadata) + +--- + +## 🎯 PRÓXIMOS PASOS + +1. Aplicar fixes Priority 1 (5 mins) +2. Runear tests locales con Bun +3. Agregar a tu README las explicaciones técnicas +4. Hacer mock entrevista con estas preguntas + +--- + +**Generated:** 2025-04-01 | **Reviewed by:** Senior Fullstack Engineer diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,41 @@ +# 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 (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..b62f7a4 --- /dev/null +++ b/app/README.md @@ -0,0 +1,62 @@ +## Mid Fullstack Technical Test — Kanban + +Kanban board built with: + +- **Next.js (App Router)** (`src/app`) +- **Bun** (package manager + scripts) +- **SQLite (local file)** via `@libsql/client` using `DATABASE_URL="file:..."` +- **Drizzle ORM** +- **TailwindCSS** + +## Quickstart (local) + +From the `app/` folder: + +1) Install deps + +```bash +bun install +``` + +2) (Optional) choose DB file location + +By default the app uses `file:./local.db`. You can override it: + +```bash +set DATABASE_URL=file:./local.db +``` + +3) Seed example data (creates tables + example board) + +```bash +bun run seed +``` + +4) Run dev server + +```bash +bun run dev +``` + +Open `http://localhost:3000/board/1` (or the latest created board id). + +## Architecture / design decisions + +- **DB bootstrap & lightweight migrations** + - `src/db/client.ts` exposes `ensureSchema()` which: + - creates tables with `CREATE TABLE IF NOT EXISTS` + - applies additive migrations (e.g. `ALTER TABLE ... ADD COLUMN`) when new columns are introduced + - creates indexes used by the API. + +- **API** + - Route Handlers live under `src/app/api/**/route.ts`. + - Inputs are validated with **Zod**. Invalid requests return `400` with `{ error, issues }`. + +- **UI** + - Main Kanban view is `src/app/board/[id]/BoardClient.tsx`. + - Uses a modal to create tasks and a ` setRole(e.target.value as UserRole)} + className="mt-1 w-full rounded-md border border-zinc-200 bg-white px-2 py-1 text-xs text-zinc-700" + > + + + + + + + + +
+
+
+

+ Board: {board.name} +

+ + +
+ +
+ {columnsSorted.map((col) => ( +
+
+
+

+ {col.title}{" "} + + ({col.tasks.length}) + +

+
+
+ + +
+
+ +
+ {col.tasks + .slice() + .sort((a, b) => a.order - b.order) + .map((t) => { + const busy = busyTaskIds.has(t.id); + const ui = priorityUi(t.priority); + return ( +
+
+
+

+ {t.title} +

+ {t.description ? ( +

+ {t.description} +

+ ) : null} +
+ +
+ +
+ + + {ui.label} + + +
+ + + User avatar setAvatarSrc("/avatars/default.svg")} + /> + +
+
+
+ ); + })} +
+ + +
+ ))} +
+
+
+ + + {modalOpen ? ( +
+ +
+ +
+
+ + setFormTitle(e.target.value)} + placeholder="e.g., Develop new user profile API" + className="mt-1 w-full rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 outline-none placeholder:text-zinc-400 focus:border-zinc-300" + /> +
+ +
+ +