-
Notifications
You must be signed in to change notification settings - Fork 0
feat: P2.3 collections, optimistic UI, security hardening, landing polish #26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
463675e
ca47158
cbb1892
44d5235
bb7d37c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import { prisma } from "@/lib/prisma"; | ||
| import { getUserFromRequest } from "@/lib/supabase/get-user"; | ||
| import { NextRequest } from "next/server"; | ||
|
|
||
| // DELETE /api/collections/[id]/bookmarks/[bookmarkId] — remove a bookmark from a collection | ||
| export async function DELETE( | ||
| req: NextRequest, | ||
| { params }: { params: Promise<{ id: string; bookmarkId: string }> } | ||
| ) { | ||
| const user = await getUserFromRequest(req); | ||
| if (!user) return Response.json({ error: "unauthorized" }, { status: 401 }); | ||
|
|
||
| const { id: collectionId, bookmarkId } = await params; | ||
|
|
||
| // Verify collection belongs to user | ||
| const collection = await prisma.collection.findUnique({ | ||
| where: { id: collectionId }, | ||
| }); | ||
| if (!collection || collection.userId !== user.id) { | ||
| return Response.json({ error: "not found" }, { status: 404 }); | ||
| } | ||
|
|
||
| try { | ||
| await prisma.collectionBookmark.delete({ | ||
| where: { collectionId_bookmarkId: { collectionId, bookmarkId } }, | ||
| }); | ||
| } catch (e: unknown) { | ||
| if ((e as { code?: string })?.code === "P2025") { | ||
| return new Response(null, { status: 204 }); // already removed — idempotent | ||
| } | ||
| return Response.json({ error: "failed to remove bookmark from collection" }, { status: 500 }); | ||
| } | ||
|
|
||
| return new Response(null, { status: 204 }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import { prisma } from "@/lib/prisma"; | ||
| import { getUserFromRequest } from "@/lib/supabase/get-user"; | ||
| import { NextRequest } from "next/server"; | ||
|
|
||
| // POST /api/collections/[id]/bookmarks — add a bookmark to a collection | ||
| export async function POST( | ||
| req: NextRequest, | ||
| { params }: { params: Promise<{ id: string }> } | ||
| ) { | ||
| const user = await getUserFromRequest(req); | ||
| if (!user) return Response.json({ error: "unauthorized" }, { status: 401 }); | ||
|
|
||
| const { id: collectionId } = await params; | ||
|
|
||
| // Verify collection belongs to user | ||
| const collection = await prisma.collection.findUnique({ | ||
| where: { id: collectionId }, | ||
| }); | ||
| if (!collection || collection.userId !== user.id) { | ||
| return Response.json({ error: "not found" }, { status: 404 }); | ||
| } | ||
|
|
||
| let body: { bookmarkId?: unknown }; | ||
| try { | ||
| body = await req.json(); | ||
| } catch { | ||
| return Response.json({ error: "invalid json" }, { status: 400 }); | ||
| } | ||
|
|
||
| const bookmarkId = | ||
| typeof body.bookmarkId === "string" ? body.bookmarkId.trim() : ""; | ||
| if (!bookmarkId) { | ||
| return Response.json({ error: "bookmarkId is required" }, { status: 400 }); | ||
| } | ||
|
|
||
| // Verify bookmark belongs to user | ||
| const bookmark = await prisma.bookmark.findUnique({ | ||
| where: { id: bookmarkId }, | ||
| }); | ||
| if (!bookmark || bookmark.userId !== user.id) { | ||
| return Response.json({ error: "bookmark not found" }, { status: 404 }); | ||
| } | ||
|
|
||
| // Upsert to avoid duplicate errors | ||
| await prisma.collectionBookmark.upsert({ | ||
| where: { collectionId_bookmarkId: { collectionId, bookmarkId } }, | ||
| create: { collectionId, bookmarkId }, | ||
| update: {}, | ||
| }); | ||
|
|
||
| return new Response(null, { status: 204 }); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { prisma } from "@/lib/prisma"; | ||
| import { getUserFromRequest } from "@/lib/supabase/get-user"; | ||
| import { NextRequest } from "next/server"; | ||
|
|
||
| // DELETE /api/collections/[id] | ||
| export async function DELETE( | ||
| req: NextRequest, | ||
| { params }: { params: Promise<{ id: string }> } | ||
| ) { | ||
| const user = await getUserFromRequest(req); | ||
| if (!user) return Response.json({ error: "unauthorized" }, { status: 401 }); | ||
|
|
||
| const { id } = await params; | ||
|
|
||
| const collection = await prisma.collection.findUnique({ where: { id } }); | ||
| if (!collection || collection.userId !== user.id) { | ||
| return Response.json({ error: "not found" }, { status: 404 }); | ||
| } | ||
|
|
||
| try { | ||
| await prisma.collection.delete({ where: { id } }); | ||
| } catch (e: unknown) { | ||
| if ((e as { code?: string })?.code === "P2025") { | ||
| return new Response(null, { status: 204 }); // already deleted — idempotent | ||
| } | ||
| return Response.json({ error: "failed to delete collection" }, { status: 500 }); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| return new Response(null, { status: 204 }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import { prisma } from "@/lib/prisma"; | ||
| import { getUserFromRequest } from "@/lib/supabase/get-user"; | ||
| import { NextRequest } from "next/server"; | ||
|
|
||
| // GET /api/collections — list all collections for the user | ||
| export async function GET(req: NextRequest) { | ||
| const user = await getUserFromRequest(req); | ||
| if (!user) return Response.json({ error: "unauthorized" }, { status: 401 }); | ||
|
|
||
| const collections = await prisma.collection.findMany({ | ||
| where: { userId: user.id }, | ||
| orderBy: { createdAt: "asc" }, | ||
| }); | ||
|
|
||
| return Response.json(collections); | ||
| } | ||
|
|
||
| // POST /api/collections — create a new collection | ||
| export async function POST(req: NextRequest) { | ||
| const user = await getUserFromRequest(req); | ||
| if (!user) return Response.json({ error: "unauthorized" }, { status: 401 }); | ||
|
|
||
| let body: { name?: unknown }; | ||
| try { | ||
| body = await req.json(); | ||
| } catch { | ||
| return Response.json({ error: "invalid json" }, { status: 400 }); | ||
| } | ||
|
|
||
| const name = typeof body.name === "string" ? body.name.trim() : ""; | ||
| if (!name) return Response.json({ error: "name is required" }, { status: 400 }); | ||
| if (name.length > 100) return Response.json({ error: "name too long" }, { status: 400 }); | ||
|
|
||
| let collection; | ||
| try { | ||
| collection = await prisma.collection.create({ | ||
| data: { name, userId: user.id }, | ||
| }); | ||
|
Comment on lines
+30
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reject duplicate collection names per user. This still allows the same account to create two collections with the same visible name. 🤖 Prompt for AI Agents |
||
| } catch { | ||
| return Response.json({ error: "failed to create collection" }, { status: 500 }); | ||
| } | ||
|
|
||
| return Response.json(collection, { status: 201 }); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.