feat: implement Story 1.1 — create and view diagrams

Add diagram/project DB schema, CRUD API, dashboard pages with grid/card/
empty state, create dialog with type selector, editor placeholder, and
26 schema validation tests. Includes code review fixes: soft-delete
filter on GET /:id, error handling, keyboard accessibility, type-safe
API response types, and error states on pages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-02-22 23:54:50 +00:00
parent da3368fbdb
commit 392da385f4
20 changed files with 3785 additions and 0 deletions

View File

@@ -13,6 +13,7 @@ import { adminRouter } from "./modules/admin/router";
import { aiRouter } from "./modules/ai/router";
import { authRouter } from "./modules/auth/router";
import { billingRouter } from "./modules/billing/router";
import { diagramRouter } from "./modules/diagram/router";
import { organizationRouter } from "./modules/organization/router";
import { storageRouter } from "./modules/storage/router";
import { onError } from "./utils/on-error";
@@ -48,6 +49,7 @@ const appRouter = new Hono()
.route("/ai", aiRouter)
.route("/auth", authRouter)
.route("/billing", billingRouter)
.route("/diagrams", diagramRouter)
.route("/organizations", organizationRouter)
.route("/storage", storageRouter)
.onError(onError);

View File

@@ -0,0 +1,73 @@
import { Hono } from "hono";
import { and, desc, eq, isNull } from "drizzle-orm";
import { z } from "zod";
import { diagram } from "@turbostarter/db/schema";
import { db } from "@turbostarter/db/server";
import { HttpStatusCode } from "@turbostarter/shared/constants";
import { HttpException } from "@turbostarter/shared/utils";
import { enforceAuth, validate } from "../../middleware";
export const createDiagramSchema = z.object({
title: z.string().min(1).max(255),
type: z.enum([
"bpmn",
"er",
"orgchart",
"architecture",
"sequence",
"flowchart",
]),
projectId: z.string().optional(),
});
export const diagramRouter = new Hono()
.get("/", enforceAuth, async (c) => {
const diagrams = await db
.select()
.from(diagram)
.where(
and(eq(diagram.userId, c.var.user.id), isNull(diagram.deletedAt)),
)
.orderBy(desc(diagram.updatedAt));
return c.json({ data: diagrams });
})
.get("/:id", enforceAuth, async (c) => {
const [d] = await db
.select()
.from(diagram)
.where(
and(
eq(diagram.id, c.req.param("id")),
eq(diagram.userId, c.var.user.id),
isNull(diagram.deletedAt),
),
);
if (!d) {
throw new HttpException(HttpStatusCode.NOT_FOUND, {
code: "error.notFound",
});
}
return c.json({ data: d });
})
.post(
"/",
enforceAuth,
validate("json", createDiagramSchema),
async (c) => {
const input = c.req.valid("json");
const [created] = await db
.insert(diagram)
.values({
...input,
userId: c.var.user.id,
})
.returning();
return c.json({ data: created });
},
);