Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"resume": "Resume",
"personality": "Personality",
"leaderboard": "Leaderboard",
"kanban": "Kanban Board",
"home": "Home",
"features": "Features",
"openMenu": "Open navigation menu",
Expand Down
1 change: 1 addition & 0 deletions messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"resume": "CV",
"personality": "Personalidad",
"leaderboard": "Clasificación",
"kanban": "Tablero Kanban",
"home": "Inicio",
"features": "Funciones",
"openMenu": "Abrir menú de navegación",
Expand Down
83 changes: 83 additions & 0 deletions src/app/api/kanban/[projectId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { supabaseAdmin } from "@/lib/supabase";
import { resolveAppUser } from "@/lib/resolve-user";

export const dynamic = "force-dynamic";

interface RouteParams {
params: Promise<{
projectId: string;
}>;
}

export async function GET(req: Request, props: RouteParams) {
const { projectId } = await props.params;
const session = await getServerSession(authOptions);
if (!session?.githubId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

const user = await resolveAppUser(session.githubId, session.githubLogin);
if (!user) return Response.json({ error: "User not found" }, { status: 404 });

// Verify project belongs to user
const { data: project, error: projectError } = await supabaseAdmin
.from("projects")
.select("*")
.eq("id", projectId)
.eq("user_id", user.id)
.single();

if (projectError || !project) {
return Response.json({ error: "Project not found" }, { status: 404 });
}

// Get stages
const { data: stages, error: stagesError } = await supabaseAdmin
.from("workflow_stages")
.select("*")
.eq("project_id", projectId)
.order("position", { ascending: true });

if (stagesError) {
return Response.json({ error: stagesError.message }, { status: 500 });
}

// Get tasks
const { data: tasks, error: tasksError } = await supabaseAdmin
.from("tasks")
.select("*")
.eq("project_id", projectId)
.order("position", { ascending: true });

if (tasksError) {
return Response.json({ error: tasksError.message }, { status: 500 });
}

return Response.json({ project, stages, tasks });
}

export async function DELETE(req: Request, props: RouteParams) {
const { projectId } = await props.params;
const session = await getServerSession(authOptions);
if (!session?.githubId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

const user = await resolveAppUser(session.githubId, session.githubLogin);
if (!user) return Response.json({ error: "User not found" }, { status: 404 });

// Delete project (cascade will delete stages and tasks)
const { error } = await supabaseAdmin
.from("projects")
.delete()
.eq("id", projectId)
.eq("user_id", user.id);

if (error) {
return Response.json({ error: error.message }, { status: 500 });
}

return Response.json({ success: true });
}
133 changes: 133 additions & 0 deletions src/app/api/kanban/[projectId]/stages/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { supabaseAdmin } from "@/lib/supabase";
import { resolveAppUser } from "@/lib/resolve-user";

export const dynamic = "force-dynamic";

interface RouteParams {
params: Promise<{
projectId: string;
}>;
}

export async function POST(req: Request, props: RouteParams) {
const { projectId } = await props.params;
const session = await getServerSession(authOptions);
if (!session?.githubId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

const user = await resolveAppUser(session.githubId, session.githubLogin);
if (!user) return Response.json({ error: "User not found" }, { status: 404 });

// Verify project belongs to user
const { data: project, error: projectError } = await supabaseAdmin
.from("projects")
.select("*")
.eq("id", projectId)
.eq("user_id", user.id)
.single();

if (projectError || !project) {
return Response.json({ error: "Project not found" }, { status: 404 });
}

let body;
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}

const { name, color, position } = body;
if (!name) {
return Response.json({ error: "Stage name is required" }, { status: 400 });
}
if (typeof position !== "number") {
return Response.json({ error: "Position must be a number" }, { status: 400 });
}

const { data: stage, error: stageError } = await supabaseAdmin
.from("workflow_stages")
.insert({
project_id: projectId,
name,
color: color || "#6366f1",
position,
})
.select("*")
.single();

if (stageError) {
return Response.json({ error: stageError.message }, { status: 500 });
}

return Response.json({ stage }, { status: 201 });
}

export async function PUT(req: Request, props: RouteParams) {
const { projectId } = await props.params;
const session = await getServerSession(authOptions);
if (!session?.githubId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

const user = await resolveAppUser(session.githubId, session.githubLogin);
if (!user) return Response.json({ error: "User not found" }, { status: 404 });

// Verify project belongs to user
const { data: project, error: projectError } = await supabaseAdmin
.from("projects")
.select("*")
.eq("id", projectId)
.eq("user_id", user.id)
.single();

if (projectError || !project) {
return Response.json({ error: "Project not found" }, { status: 404 });
}

let body;
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}

const { stages, deleteStageId } = body;

if (deleteStageId) {
// Delete the stage
const { error: deleteError } = await supabaseAdmin
.from("workflow_stages")
.delete()
.eq("id", deleteStageId)
.eq("project_id", projectId);

if (deleteError) {
return Response.json({ error: deleteError.message }, { status: 500 });
}
}

if (stages && Array.isArray(stages)) {
// Bulk upsert/update positions and attributes of stages
const payload = stages.map((s) => ({
id: s.id,
project_id: projectId,
name: s.name,
color: s.color || "#6366f1",
position: s.position,
}));

const { error: upsertError } = await supabaseAdmin
.from("workflow_stages")
.upsert(payload, { onConflict: "id" });

if (upsertError) {
return Response.json({ error: upsertError.message }, { status: 500 });
}
}

return Response.json({ success: true });
}
136 changes: 136 additions & 0 deletions src/app/api/kanban/[projectId]/tasks/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { supabaseAdmin } from "@/lib/supabase";
import { resolveAppUser } from "@/lib/resolve-user";

export const dynamic = "force-dynamic";

interface RouteParams {
params: Promise<{
projectId: string;
}>;
}

export async function POST(req: Request, props: RouteParams) {
const { projectId } = await props.params;
const session = await getServerSession(authOptions);
if (!session?.githubId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

const user = await resolveAppUser(session.githubId, session.githubLogin);
if (!user) return Response.json({ error: "User not found" }, { status: 404 });

// Verify project belongs to user
const { data: project, error: projectError } = await supabaseAdmin
.from("projects")
.select("*")
.eq("id", projectId)
.eq("user_id", user.id)
.single();

if (projectError || !project) {
return Response.json({ error: "Project not found" }, { status: 404 });
}

let body;
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}

const { title, description, stageId, position } = body;
if (!title) {
return Response.json({ error: "Task title is required" }, { status: 400 });
}
if (!stageId) {
return Response.json({ error: "Stage ID is required" }, { status: 400 });
}

const { data: task, error: taskError } = await supabaseAdmin
.from("tasks")
.insert({
project_id: projectId,
stage_id: stageId,
title,
description: description || "",
position: typeof position === "number" ? position : 0,
})
.select("*")
.single();

if (taskError) {
return Response.json({ error: taskError.message }, { status: 500 });
}

return Response.json({ task }, { status: 201 });
}

export async function PUT(req: Request, props: RouteParams) {
const { projectId } = await props.params;
const session = await getServerSession(authOptions);
if (!session?.githubId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

const user = await resolveAppUser(session.githubId, session.githubLogin);
if (!user) return Response.json({ error: "User not found" }, { status: 404 });

// Verify project belongs to user
const { data: project, error: projectError } = await supabaseAdmin
.from("projects")
.select("*")
.eq("id", projectId)
.eq("user_id", user.id)
.single();

if (projectError || !project) {
return Response.json({ error: "Project not found" }, { status: 404 });
}

let body;
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}

const { tasks, deleteTaskId } = body;

if (deleteTaskId) {
// Delete the task
const { error: deleteError } = await supabaseAdmin
.from("tasks")
.delete()
.eq("id", deleteTaskId)
.eq("project_id", projectId);

if (deleteError) {
return Response.json({ error: deleteError.message }, { status: 500 });
}
}

if (tasks && Array.isArray(tasks)) {
// Bulk upsert/update positions and attributes of tasks
const payload = tasks.map((t) => ({
id: t.id,
project_id: projectId,
stage_id: t.stage_id,
title: t.title,
description: t.description || "",
position: t.position,
updated_at: new Date().toISOString(),
}));

const { error: upsertError } = await supabaseAdmin
.from("tasks")
.upsert(payload, { onConflict: "id" });

if (upsertError) {
return Response.json({ error: upsertError.message }, { status: 500 });
}
}

return Response.json({ success: true });
}
Loading
Loading