Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Generated dependency vulnerability scan reports (local + CI artifacts)
reports/
# But allow the API route directory
!frontend/src/app/api/reports/
!frontend/src/app/api/reports/**

# Environment / secrets — never commit real credentials
.env
Expand Down
92 changes: 92 additions & 0 deletions frontend/src/app/api/bookmarks/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { NextRequest, NextResponse } from "next/server";
import {
addBookmark,
listBookmarks,
removeBookmarkByTask,
resetBookmarkStore,
} from "@/lib/bookmark-store";

/**
* GET /api/bookmarks?userId=<wallet>
* List all bookmarks for a user.
*/
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const userId = searchParams.get("userId") ?? "";

if (!userId.trim()) {
return NextResponse.json(
{ error: "userId query parameter is required." },
{ status: 400 },
);
}

const result = listBookmarks(userId);
return NextResponse.json(result);
}

/**
* POST /api/bookmarks
* Body: { userId, taskId, action?: "add" | "remove" | "toggle" }
* Default action is "add".
*/
export async function POST(request: NextRequest) {
let body: Record<string, unknown>;
try {
body = await request.json();
} catch {
return NextResponse.json(
{ error: "Invalid JSON body." },
{ status: 400 },
);
}

const userId = String(body.userId ?? "").trim();
const taskId = String(body.taskId ?? "").trim();
const action = String(body.action ?? "add") as "add" | "remove" | "toggle";

if (!userId || !taskId) {
return NextResponse.json(
{ error: "userId and taskId are required." },
{ status: 400 },
);
}

if (action === "remove") {
const result = removeBookmarkByTask(userId, taskId);
if (!result.ok) {
return NextResponse.json({ error: result.error }, { status: result.status });
}
return NextResponse.json({ bookmarked: false });
}

// default: add
const result = addBookmark(userId, taskId);
if (!result.ok) {
return NextResponse.json({ error: result.error }, { status: result.status });
}
return NextResponse.json({ bookmarked: true, bookmark: result.bookmark });
}

/**
* DELETE /api/bookmarks?userId=<wallet>&taskId=<taskId>
* Remove a bookmark by user+task pair.
*/
export async function DELETE(request: NextRequest) {
const { searchParams } = new URL(request.url);
const userId = searchParams.get("userId") ?? "";
const taskId = searchParams.get("taskId") ?? "";

if (!userId.trim() || !taskId.trim()) {
return NextResponse.json(
{ error: "userId and taskId are required." },
{ status: 400 },
);
}

const result = removeBookmarkByTask(userId, taskId);
if (!result.ok) {
return NextResponse.json({ error: result.error }, { status: result.status });
}
return NextResponse.json({ bookmarked: false });
}
95 changes: 95 additions & 0 deletions frontend/src/app/api/comparison/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { NextRequest, NextResponse } from "next/server";
import {
createComparison,
getComparison,
addToComparison,
removeFromComparison,
clearComparison,
} from "@/lib/grant-comparison";

/**
* GET /api/comparison?userId=<wallet>
* Get the user's current comparison set.
*/
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const userId = searchParams.get("userId") ?? "";

if (!userId.trim()) {
return NextResponse.json(
{ error: "userId query parameter is required." },
{ status: 400 },
);
}

const comparison = getComparison(userId);
return NextResponse.json({ comparison });
}

/**
* POST /api/comparison
* Body: { userId, taskIds?, taskId?, action? }
*
* Actions:
* - "create" (default): create/replace comparison with taskIds array
* - "add": add taskId to comparison
* - "remove": remove taskId from comparison
* - "clear": clear comparison set
*/
export async function POST(request: NextRequest) {
let body: Record<string, unknown>;
try {
body = await request.json();
} catch {
return NextResponse.json(
{ error: "Invalid JSON body." },
{ status: 400 },
);
}

const userId = String(body.userId ?? "").trim();
const action = String(body.action ?? "create");

if (!userId) {
return NextResponse.json(
{ error: "userId is required." },
{ status: 400 },
);
}

if (action === "clear") {
clearComparison(userId);
return NextResponse.json({ comparison: null });
}

if (action === "add" || action === "remove") {
const taskId = String(body.taskId ?? "").trim();
if (!taskId) {
return NextResponse.json(
{ error: "taskId is required for add/remove actions." },
{ status: 400 },
);
}

const result =
action === "add"
? addToComparison(userId, taskId)
: removeFromComparison(userId, taskId);

if (!result.ok) {
return NextResponse.json({ error: result.error }, { status: result.status });
}
return NextResponse.json({ comparison: result.comparison });
}

// default: create
const taskIds = Array.isArray(body.taskIds)
? body.taskIds.map(String)
: [];

const result = createComparison(userId, taskIds);
if (!result.ok) {
return NextResponse.json({ error: result.error }, { status: result.status });
}
return NextResponse.json({ comparison: result.comparison });
}
111 changes: 111 additions & 0 deletions frontend/src/app/api/drafts/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { NextRequest, NextResponse } from "next/server";
import {
saveDraft,
getDraft,
listDrafts,
deleteDraftByTask,
getLastSavedAt,
MAX_FORM_DATA_SIZE,
} from "@/lib/draft-autosave";

/**
* GET /api/drafts?userId=<wallet>&taskId=<taskId>
*
* - If taskId is provided: returns the single draft for that user+task pair.
* - If taskId is omitted: returns all drafts for the user.
*/
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const userId = searchParams.get("userId") ?? "";
const taskId = searchParams.get("taskId");

if (!userId.trim()) {
return NextResponse.json(
{ error: "userId query parameter is required." },
{ status: 400 },
);
}

if (taskId) {
const draft = getDraft(userId, taskId);
if (!draft) {
return NextResponse.json(
{ error: "Draft not found." },
{ status: 404 },
);
}
return NextResponse.json({ draft });
}

const result = listDrafts(userId);
return NextResponse.json(result);
}

/**
* POST /api/drafts
* Body: { userId, taskId, formData, autoSaved? }
*
* Creates or updates a draft (upsert).
*/
export async function POST(request: NextRequest) {
let body: Record<string, unknown>;
try {
body = await request.json();
} catch {
return NextResponse.json(
{ error: "Invalid JSON body." },
{ status: 400 },
);
}

const userId = String(body.userId ?? "").trim();
const taskId = String(body.taskId ?? "").trim();
const formData = String(body.formData ?? "");
const autoSaved = body.autoSaved !== false;

if (!userId || !taskId) {
return NextResponse.json(
{ error: "userId and taskId are required." },
{ status: 400 },
);
}

if (formData.length > MAX_FORM_DATA_SIZE) {
return NextResponse.json(
{ error: `Form data exceeds maximum size of ${MAX_FORM_DATA_SIZE} bytes.` },
{ status: 400 },
);
}

const result = saveDraft({ userId, taskId, formData, autoSaved });
if (!result.ok) {
return NextResponse.json({ error: result.error }, { status: result.status });
}

return NextResponse.json({ draft: result.draft });
}

/**
* DELETE /api/drafts?userId=<wallet>&taskId=<taskId>
*
* Removes a draft for the specified user+task pair.
*/
export async function DELETE(request: NextRequest) {
const { searchParams } = new URL(request.url);
const userId = searchParams.get("userId") ?? "";
const taskId = searchParams.get("taskId") ?? "";

if (!userId.trim() || !taskId.trim()) {
return NextResponse.json(
{ error: "userId and taskId are required." },
{ status: 400 },
);
}

const result = deleteDraftByTask(userId, taskId);
if (!result.ok) {
return NextResponse.json({ error: result.error }, { status: result.status });
}

return NextResponse.json({ deleted: true });
}
Loading