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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@types/node": "^24.6.1",
"@ungap/with-resolvers": "^0.1.0",
"@upstash/ratelimit": "^2.0.5",
"@upstash/redis": "^1.33.0",
"@vercel/kv": "^3.0.0",
"axios": "^1.8.4",
"canvas": "^3.2.0",
Expand Down
150 changes: 91 additions & 59 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

95 changes: 95 additions & 0 deletions src/app/api/report-tag/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { NextResponse } from "next/server";
import { connectToDatabase } from "@/lib/database/mongoose";
import TagReport from "@/db/tagReport";
import { Ratelimit } from "@upstash/ratelimit";
import { redis } from "@/lib/utils/redis";

const ALLOWED_EXAMS = ["CAT-1", "CAT-2", "FAT"];
Copy link
Member

Choose a reason for hiding this comment

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

we must already have allowed exams defined elsewhere. re-use that definition to reduce duplication and improve consistency

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure

const ALLOWED_FIELDS = ["subject", "courseCode", "exam", "slot", "year"];

const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(3, "1 h"),//per id - 3 request - per hour
analytics: true,
});

function getClientIp(req: any): string {
Copy link
Member

Choose a reason for hiding this comment

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

use appropriate type

return req.ip || "127.0.0.1";
}

export async function POST(req: Request & { ip?: string }) {
try {
await connectToDatabase();

const body = await req.json();
const { paperId } = body;

if (!paperId) {
return NextResponse.json(
{ error: "paperId is required" },
{ status: 400 }
);
}
const ip = getClientIp(req);
const key = `${ip}::${paperId}`;
const { success } = await ratelimit.limit(key);

if (!success) {
return NextResponse.json(
{ error: "Rate limit exceeded for reporting." },
{ status: 429 }
);
}
const MAX_REPORTS_PER_PAPER = 5;
const count = await TagReport.countDocuments({ paperId });

if (count >= MAX_REPORTS_PER_PAPER) {
return NextResponse.json(
{ error: "Received many reports; we are currently working on it." },
{ status: 429 }
);
}
const reportedFields = (body.reportedFields ?? [])
.map((r: any) => ({
field: String(r.field).trim(),
Copy link
Member

Choose a reason for hiding this comment

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

typecast it properly. avoid using any

value: r.value?.trim(),
}))
.filter((r: any) => r.field);

for (const rf of reportedFields) {
if (!ALLOWED_FIELDS.includes(rf.field)) {
return NextResponse.json(
{ error: `Invalid field: ${rf.field}` },
{ status: 400 }
);
}
if (rf.field === "exam" && rf.value) {
if (!ALLOWED_EXAMS.includes(rf.value)) {
return NextResponse.json(
{ error: `Invalid exam value: ${rf.value}` },
{ status: 400 }
);
}
}
}

const newReport = await TagReport.create({
paperId,
reportedFields,
comment: body.comment,
reporterEmail: body.reporterEmail,
reporterId: body.reporterId,
});

return NextResponse.json(
{ message: "Report submitted.", report: newReport },
{ status: 201 }
);
} catch (err) {
console.error(err);
return NextResponse.json(
{ error: "Failed to submit tag report." },
{ status: 500 }
);
}
}
5 changes: 5 additions & 0 deletions src/app/paper/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@ const PaperPage = async ({ params }: { params: { id: string } }) => {
<PdfViewer
url={paper.file_url}
name={`${extractBracketContent(paper.subject)}-${paper.exam}-${paper.slot}-${paper.year}`}
paperId={params.id}
subject={paper.subject}
exam={paper.exam}
slot={paper.slot}
year={paper.year}
></PdfViewer>
</center>
<RelatedPapers />
Expand Down
39 changes: 39 additions & 0 deletions src/components/ReportButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use client";

import { useState } from "react";
import { FaFlag } from "react-icons/fa6";
import { Button } from "./ui/button";
import ReportTagModal from "./ReportTagModal";

export default function ReportButton({
paperId, subject, exam, slot, year
}: {
paperId: string;
subject?: string;
exam?: string;
slot?: string;
year?: string;
}) {
const [open, setOpen] = useState(false);

return (
<>
<Button
onClick={() => setOpen(true)}
className="h-10 w-10 rounded p-0 text-white transition hover:bg-red-600 bg-red-500"
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

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

The Tailwind CSS class order is inconsistent. Following best practices, utility classes should be ordered: layout β†’ display β†’ positioning β†’ sizing β†’ spacing β†’ colors β†’ effects. Consider reordering to: h-10 w-10 rounded bg-red-500 p-0 text-white transition hover:bg-red-600.

Suggested change
className="h-10 w-10 rounded p-0 text-white transition hover:bg-red-600 bg-red-500"
className="h-10 w-10 p-0 rounded bg-red-500 text-white hover:bg-red-600 transition"

Copilot uses AI. Check for mistakes.
>
<FaFlag className="text-sm" />
</Button>

<ReportTagModal
paperId={paperId}
subject={subject}
exam={exam}
slot={slot}
year={year}
open={open}
setOpen={setOpen}
/>
</>
);
}
Loading