Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
23 changes: 11 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@

**Save anything. Find everything.**

[![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/MinitJain/recall?utm_source=oss&utm_medium=github&utm_campaign=MinitJain%2Frecall&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews)](https://coderabbit.ai)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Next.js](https://img.shields.io/badge/Next.js-16-black?logo=next.js)](https://nextjs.org)

</div>

---
Expand Down Expand Up @@ -45,15 +41,15 @@ Recall gives every bookmark an AI-generated tag cloud the moment you save it. Pa

## Tech Stack

| Layer | Technology |
|-------|-----------|
| Layer | Technology |
| -------------- | ----------------------- |
| Frontend + API | Next.js 16 (App Router) |
| Styling | Tailwind CSS |
| Database | PostgreSQL via Supabase |
| ORM | Prisma |
| AI Tagging | Google Gemini API |
| Auth | Supabase Auth |
| Deployment | Vercel |
| Styling | Tailwind CSS |
| Database | PostgreSQL via Supabase |
| ORM | Prisma |
| AI Tagging | Google Gemini API |
| Auth | Supabase Auth |
| Deployment | Vercel |

---

Expand Down Expand Up @@ -96,15 +92,18 @@ App runs at `http://localhost:3000`.
## Roadmap

**M8 — Chrome Extension**

- Floating save button on any page
- Popup with recent bookmarks
- Cross-browser sync via same account

**M9 — Deployment**

- Vercel production deploy
- Preview URLs on every PR

**Future**

- Bookmarklet (no extension required)
- Resurfacing — surface older bookmarks when you save something related
- Semantic / vector search
Expand Down
36 changes: 36 additions & 0 deletions client/src/app/api/bookmarks/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { getUserFromRequest } from "@/lib/supabase/get-user";

export async function DELETE(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;

const user = await getUserFromRequest(req);
if (!user)
return NextResponse.json({ error: "unauthorized" }, { status: 401 });

try {
const bookmark = await prisma.bookmark.findFirst({
where: { id, userId: user.id },
});

if (!bookmark) {
return NextResponse.json(
{ error: "bookmark not found" },
{ status: 404 },
);
}

await prisma.bookmark.delete({ where: { id: bookmark.id } });
} catch {
return NextResponse.json(
{ error: "failed to delete bookmark" },
{ status: 500 },
);
}

return new NextResponse(null, { status: 204 });
}
14 changes: 8 additions & 6 deletions client/src/app/api/bookmarks/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { isIP } from "net";
import { prisma } from "@/lib/prisma";
import { scrapeUrl } from "@/lib/scraper";
import { createClient } from "@/lib/supabase/server";
import { getUserFromRequest } from "@/lib/supabase/get-user";

function isPrivateIp(host: string): boolean {
if (isIP(host) === 4) {
Expand All @@ -28,10 +28,7 @@ function isPrivateIp(host: string): boolean {
}

export async function POST(req: NextRequest) {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
const user = await getUserFromRequest(req);
if (!user)
return NextResponse.json({ error: "unauthorized" }, { status: 401 });

Expand Down Expand Up @@ -95,8 +92,13 @@ export async function POST(req: NextRequest) {
return NextResponse.json(bookmark, { status: 201 });
}

export async function GET() {
export async function GET(req: NextRequest) {
const user = await getUserFromRequest(req);
if (!user)
return NextResponse.json({ error: "unauthorized" }, { status: 401 });

const bookmarks = await prisma.bookmark.findMany({
where: { userId: user.id },
orderBy: { createdAt: "desc" },
});
return NextResponse.json(bookmarks);
Expand Down
26 changes: 26 additions & 0 deletions client/src/lib/supabase/get-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NextRequest } from "next/server";
import { createClient } from "@/lib/supabase/server";
import { createClient as createSupabaseClient } from "@supabase/supabase-js";

export async function getUserFromRequest(req: NextRequest) {
const authHeader = req.headers.get("Authorization");

if (authHeader?.startsWith("Bearer ")) {
const token = authHeader.slice(7);
const supabase = createSupabaseClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
const {
data: { user },
} = await supabase.auth.getUser(token);
return user;
}

// Fall back to cookie-based auth (used by the web UI)
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
return user;
}
19 changes: 19 additions & 0 deletions extension/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"manifest_version": 3,
"name": "Recall",
"version": "1.0.0",
"description": "Save anything. Find everything.",

"action": {
"default_popup": "popup.html",
"default_title": "Recall"
},

"background": {
"service_worker": "serviceworker.js"
},

"permissions": ["activeTab", "storage"],

"host_permissions": ["http://localhost:3000/*", "http://localhost:3001/*"]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
205 changes: 205 additions & 0 deletions extension/popup.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}

body {
width: 320px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 14px;
background: #0a0a0a;
color: #ededed;
padding: 16px;
}

h1 {
font-size: 20px;
font-weight: 700;
margin-bottom: 4px;
}

.tagline {
color: #888;
margin-bottom: 16px;
font-size: 12px;
}

input {
width: 100%;
padding: 8px 10px;
margin-bottom: 8px;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 6px;
color: #ededed;
font-size: 14px;
outline: none;
}

input:focus {
border-color: #555;
}

button {
width: 100%;
padding: 8px 12px;
background: #ededed;
color: #0a0a0a;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
margin-bottom: 8px;
}

button:hover {
background: #d4d4d4;
}

button.secondary {
background: #1a1a1a;
color: #ededed;
border: 1px solid #333;
}

button.secondary:hover {
background: #222;
}

button.ghost {
background: transparent;
color: #888;
border: none;
width: auto;
padding: 0;
font-size: 12px;
font-weight: 400;
margin: 0;
}

button.ghost:hover {
color: #ededed;
}

.button-row {
display: flex;
gap: 8px;
}

.button-row button {
margin-bottom: 0;
}

.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}

.user-email {
font-size: 12px;
color: #888;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 200px;
}

.error {
background: #2a1a1a;
border: 1px solid #5a2a2a;
color: #f87171;
padding: 8px 10px;
border-radius: 6px;
margin-bottom: 8px;
font-size: 12px;
}

.status {
font-size: 12px;
color: #888;
margin-bottom: 8px;
text-align: center;
}

.status.success {
color: #4ade80;
}

.status.error-text {
color: #f87171;
}

.hidden {
display: none;
}

/* Bookmark list */
.bookmark-item {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
padding: 8px 0;
border-bottom: 1px solid #1a1a1a;
}

.bookmark-item:last-child {
border-bottom: none;
}

.bookmark-info {
flex: 1;
overflow: hidden;
}

.bookmark-title {
font-size: 13px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #ededed;
text-decoration: none;
display: block;
}

.bookmark-title:hover {
color: #d4d4d4;
}

.bookmark-url {
font-size: 11px;
color: #555;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.delete-btn {
background: transparent;
border: none;
color: #555;
cursor: pointer;
font-size: 16px;
padding: 0;
width: auto;
margin: 0;
line-height: 1;
flex-shrink: 0;
}

.delete-btn:hover {
color: #f87171;
background: transparent;
}

.empty-state {
text-align: center;
color: #555;
font-size: 13px;
padding: 24px 0;
}
Loading
Loading