From c9e6e156accafa4fc2e976b40cea4dc47cda88a2 Mon Sep 17 00:00:00 2001 From: Christopher Mayor Date: Sun, 26 Apr 2026 15:57:58 -0700 Subject: [PATCH] feat: add comparison and user API endpoints New API routes under src/app/api/ for comparisons and user operations. --- src/app/api/comparisons/[slug]/route.ts | 24 +++++++++ src/app/api/comparisons/route.ts | 65 +++++++++++++++++++++++ src/app/api/user/comparisons/route.ts | 69 +++++++++++++++++++++++++ src/app/api/user/stats/route.ts | 23 +++++++++ 4 files changed, 181 insertions(+) create mode 100644 src/app/api/comparisons/[slug]/route.ts create mode 100644 src/app/api/comparisons/route.ts create mode 100644 src/app/api/user/comparisons/route.ts create mode 100644 src/app/api/user/stats/route.ts diff --git a/src/app/api/comparisons/[slug]/route.ts b/src/app/api/comparisons/[slug]/route.ts new file mode 100644 index 0000000..f614324 --- /dev/null +++ b/src/app/api/comparisons/[slug]/route.ts @@ -0,0 +1,24 @@ +import { db } from "@/lib/db"; +import { comparisons } from "@/lib/db/schema"; +import { eq, sql } from "drizzle-orm"; +import { getComparison } from "@/app/actions/comparison"; + +export async function GET( + _request: Request, + { params }: { params: Promise<{ slug: string }> } +) { + const { slug } = await params; + + await db + .update(comparisons) + .set({ viewCount: sql`${comparisons.viewCount} + 1` }) + .where(eq(comparisons.slug, slug)); + + const comparison = await getComparison(slug); + + if (!comparison) { + return Response.json({ error: "Not found" }, { status: 404 }); + } + + return Response.json(comparison); +} diff --git a/src/app/api/comparisons/route.ts b/src/app/api/comparisons/route.ts new file mode 100644 index 0000000..d6adbb8 --- /dev/null +++ b/src/app/api/comparisons/route.ts @@ -0,0 +1,65 @@ +import { db } from "@/lib/db"; +import { comparisons, comparisonItems } from "@/lib/db/schema"; +import { eq, and, desc, ilike, sql, inArray } from "drizzle-orm"; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const page = Math.max(1, Number(searchParams.get("page")) || 1); + const limit = Math.min(100, Math.max(1, Number(searchParams.get("limit")) || 20)); + const search = searchParams.get("search") || ""; + const offset = (page - 1) * limit; + + const conditions = [eq(comparisons.isPublic, true), eq(comparisons.status, "completed")]; + if (search) { + conditions.push(ilike(comparisons.title, `%${search}%`)); + } + + const where = and(...conditions); + + const [result, countResult] = await Promise.all([ + db + .select() + .from(comparisons) + .where(where) + .orderBy(desc(comparisons.createdAt)) + .limit(limit) + .offset(offset), + db + .select({ count: sql`count(*)` }) + .from(comparisons) + .where(where), + ]); + + const total = countResult[0].count; + + const comparisonIds = result.map((c) => c.id); + const itemsMap: Record = {}; + + if (comparisonIds.length > 0) { + const items = await db + .select({ + comparisonId: comparisonItems.comparisonId, + name: comparisonItems.name, + }) + .from(comparisonItems) + .where(inArray(comparisonItems.comparisonId, comparisonIds)); + + for (const item of items) { + if (!itemsMap[item.comparisonId]) itemsMap[item.comparisonId] = []; + itemsMap[item.comparisonId].push(item.name); + } + } + + const data = result.map((c) => ({ + id: c.id, + title: c.title, + summary: (c.summary || "").slice(0, 200), + slug: c.slug, + tags: c.tags || [], + items: itemsMap[c.id] || [], + viewCount: c.viewCount ?? 0, + createdAt: c.createdAt.toISOString(), + })); + + return Response.json({ comparisons: data, total, page, limit }); +} diff --git a/src/app/api/user/comparisons/route.ts b/src/app/api/user/comparisons/route.ts new file mode 100644 index 0000000..d46356f --- /dev/null +++ b/src/app/api/user/comparisons/route.ts @@ -0,0 +1,69 @@ +import { db } from "@/lib/db"; +import { comparisons, comparisonItems } from "@/lib/db/schema"; +import { eq, desc, sql, inArray } from "drizzle-orm"; +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; + +export async function GET(request: Request) { + const session = await auth.api.getSession({ headers: await headers() }); + + if (!session?.user) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const page = Math.max(1, Number(searchParams.get("page")) || 1); + const limit = Math.min(100, Math.max(1, Number(searchParams.get("limit")) || 20)); + const offset = (page - 1) * limit; + + const where = eq(comparisons.userId, session.user.id); + + const [result, countResult] = await Promise.all([ + db + .select() + .from(comparisons) + .where(where) + .orderBy(desc(comparisons.createdAt)) + .limit(limit) + .offset(offset), + db + .select({ count: sql`count(*)` }) + .from(comparisons) + .where(where), + ]); + + const total = countResult[0].count; + + const comparisonIds = result.map((c) => c.id); + const itemsMap: Record = {}; + + if (comparisonIds.length > 0) { + const items = await db + .select({ + comparisonId: comparisonItems.comparisonId, + name: comparisonItems.name, + }) + .from(comparisonItems) + .where(inArray(comparisonItems.comparisonId, comparisonIds)); + + for (const item of items) { + if (!itemsMap[item.comparisonId]) itemsMap[item.comparisonId] = []; + itemsMap[item.comparisonId].push(item.name); + } + } + + const data = result.map((c) => ({ + id: c.id, + title: c.title, + summary: (c.summary || "").slice(0, 200), + slug: c.slug, + tags: c.tags || [], + items: itemsMap[c.id] || [], + viewCount: c.viewCount ?? 0, + status: c.status, + isPublic: c.isPublic, + createdAt: c.createdAt.toISOString(), + })); + + return Response.json({ comparisons: data, total, page, limit }); +} diff --git a/src/app/api/user/stats/route.ts b/src/app/api/user/stats/route.ts new file mode 100644 index 0000000..b152dec --- /dev/null +++ b/src/app/api/user/stats/route.ts @@ -0,0 +1,23 @@ +import { db } from "@/lib/db"; +import { comparisons } from "@/lib/db/schema"; +import { eq, sql } from "drizzle-orm"; +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; + +export async function GET() { + const session = await auth.api.getSession({ headers: await headers() }); + + if (!session?.user) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + const result = await db + .select({ + totalComparisons: sql`count(*)`, + totalViews: sql`coalesce(sum(${comparisons.viewCount}), 0)`, + }) + .from(comparisons) + .where(eq(comparisons.userId, session.user.id)); + + return Response.json(result[0]); +}