Skip to content
Merged
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
8 changes: 6 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@ Read `GUIDE.md` first — it contains the full project briefing and teaching rul

## Current Milestone

**M7 (Auth) is complete.** Next milestone is **M8: Chrome Extension**.
**ui-overhaul branch is in progress** (landing page + smart `/` routing — not yet merged).

After M8 comes M9 (Vercel deployment + preview URLs on PRs).
Before M8 can start, two things must be done on this branch:
1. Remove debug `console.log` from `client/src/app/page.tsx` line 9
2. Fix the Supabase SSR session refresh loop — create `client/src/proxy.ts` (Next.js 16.2 uses `proxy.ts`, NOT `middleware.ts`) using the Supabase SSR proxy pattern so token refresh happens before server components run

After that: **M8: Chrome Extension**, then M9 (Vercel deployment).

## Project Structure

Expand Down
10 changes: 10 additions & 0 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@upstash/redis": "^1.37.0",
"@vercel/analytics": "^2.0.1",
"@vercel/speed-insights": "^2.0.0",
"lucide-react": "^1.7.0",
"next": "^16.2.1",
"open-graph-scraper": "^6.11.0",
"pg": "^8.20.0",
Expand Down
16 changes: 16 additions & 0 deletions client/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions client/public/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions client/src/app/app/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import BookmarkCardSkeleton from "@/components/BookmarkCardSkeleton";

export default function Loading() {
return (
<div className="min-h-screen bg-[var(--bg)]">
{/* Header placeholder */}
<div className="h-12 border-b border-[var(--border)] bg-[var(--bg)]/80" />
<main className="max-w-2xl mx-auto pt-6 pb-16 px-4">
<div className="flex flex-col gap-3">
<BookmarkCardSkeleton />
<BookmarkCardSkeleton />
<BookmarkCardSkeleton />
</div>
</main>
</div>
);
}
78 changes: 78 additions & 0 deletions client/src/app/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Suspense } from "react";
import { prisma } from "@/lib/prisma";
import BookmarkCard from "@/components/BookmarkCard";
import BookmarkCardSkeleton from "@/components/BookmarkCardSkeleton";
import SaveUrlForm from "@/components/SaveUrlForm";
import Header from "@/components/Header";
import SearchBar from "@/components/SearchBar";
import { createClient } from "@/lib/supabase/server";
import { redirect } from "next/navigation";
import { Bookmark } from "lucide-react";

export const metadata = { title: "Your Bookmarks | Recall" };

async function BookmarkList({ userId }: { userId: string }) {
const bookmarks = await prisma.bookmark.findMany({
where: { userId },
orderBy: { createdAt: "desc" },
include: { tags: true },
});
Comment on lines +15 to +19

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Cap the initial bookmark query.

This loads every bookmark plus every tag for the user, and the route is refreshed again after mutations from SaveUrlForm and BookmarkCard. Once an account grows, /app becomes an unbounded DB query and HTML payload on every save/delete. Add pagination, cursoring, or at least a first-page limit here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/app/app/page.tsx` around lines 15 - 19, The initial query using
prisma.bookmark.findMany that populates bookmarks (variable bookmarks) currently
fetches all user rows including tags; limit the result set by adding
pagination/cursoring (e.g., take/skip or take + cursor) or at minimum a
first-page limit (take: N) and return a nextCursor/token so the UI components
(SaveUrlForm, BookmarkCard) can request subsequent pages; update the server
handler that renders the /app page to accept page/cursor params and adjust any
client-side refreshes to use paged refetches rather than reloading the entire
bookmarks list.


if (bookmarks.length === 0) {
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-2xl border border-[var(--border)] bg-[var(--surface)] text-center" style={{ padding: "48px 32px" }}>
<Bookmark size={40} className="text-[var(--accent)]" />
<div>
<h2 className="text-xl font-semibold text-[var(--text)] mb-1">Nothing saved yet</h2>
<p className="text-sm text-[var(--text-muted)]">Paste any URL above to save your first bookmark</p>
</div>
</div>
);
}

return (
<div className="flex flex-col gap-3">
{bookmarks.map((bookmark, i) => (
<div key={bookmark.id} className="animate-fade-up" style={{ animationDelay: `${i * 40}ms` }}>
<BookmarkCard
bookmark={{
...bookmark,
createdAt: bookmark.createdAt.toISOString(),
}}
/>
</div>
))}
</div>
);
}

function BookmarkListSkeleton() {
return (
<div className="flex flex-col gap-3">
<BookmarkCardSkeleton />
<BookmarkCardSkeleton />
<BookmarkCardSkeleton />
</div>
);
}

export default async function AppPage() {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) redirect("/auth");

return (
<div className="min-h-screen bg-[var(--bg)]">
<Header email={user.email ?? ""} />
<main className="max-w-2xl mx-auto pt-6 pb-16 px-4">
<SaveUrlForm />
<SearchBar />
<Suspense fallback={<BookmarkListSkeleton />}>
<BookmarkList userId={user.id} />
</Suspense>
</main>
</div>
);
}
23 changes: 23 additions & 0 deletions client/src/app/auth/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createClient } from "@/lib/supabase/server";
import { NextResponse } from "next/server";

export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get("code");
const rawNext = searchParams.get("next") ?? "/app";
// Reject anything that isn't a relative path to prevent open redirects
const next =
rawNext.startsWith("/") && !rawNext.startsWith("//") && !rawNext.includes("://")
? rawNext
: "/app";

if (code) {
const supabase = await createClient();
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (!error) {
return NextResponse.redirect(`${origin}${next}`);
}
}

return NextResponse.redirect(`${origin}/auth?error=oauth`);
}
Loading
Loading