Skip to content

Commit be3f0b9

Browse files
Merge pull request #419 from YOGESH-08/staging
feat: Report tag
2 parents 90b9492 + 1874f70 commit be3f0b9

14 files changed

Lines changed: 810 additions & 111 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"@types/node": "^24.6.1",
3131
"@ungap/with-resolvers": "^0.1.0",
3232
"@upstash/ratelimit": "^2.0.5",
33+
"@upstash/redis": "^1.33.0",
3334
"@vercel/kv": "^3.0.0",
3435
"axios": "^1.8.4",
3536
"canvas": "^3.2.0",

pnpm-lock.yaml

Lines changed: 91 additions & 59 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/api/report-tag/route.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { NextResponse } from "next/server";
2+
import { connectToDatabase } from "@/lib/database/mongoose";
3+
import TagReport from "@/db/tagReport";
4+
import { Ratelimit } from "@upstash/ratelimit";
5+
import { redis } from "@/lib/utils/redis";
6+
import { exams } from "@/components/select_options";
7+
8+
interface ReportedFieldInput {
9+
field: string;
10+
value?: string;
11+
}
12+
const ALLOWED_FIELDS = ["subject", "courseCode", "exam", "slot", "year"];
13+
14+
const ratelimit = new Ratelimit({
15+
redis,
16+
limiter: Ratelimit.slidingWindow(3, "1 h"),//per id - 3 request - per hour
17+
analytics: true,
18+
});
19+
20+
function getClientIp(req: Request & { ip?: string}): string {
21+
return req.ip || "127.0.0.1";
22+
}
23+
24+
export async function POST(req: Request & { ip?: string }) {
25+
try {
26+
await connectToDatabase();
27+
28+
const body = await req.json();
29+
const { paperId } = body;
30+
31+
if (!paperId) {
32+
return NextResponse.json(
33+
{ error: "paperId is required" },
34+
{ status: 400 }
35+
);
36+
}
37+
const ip = getClientIp(req);
38+
const key = `${ip}::${paperId}`;
39+
const { success } = await ratelimit.limit(key);
40+
41+
if (!success) {
42+
return NextResponse.json(
43+
{ error: "Rate limit exceeded for reporting." },
44+
{ status: 429 }
45+
);
46+
}
47+
const MAX_REPORTS_PER_PAPER = 5;
48+
const count = await TagReport.countDocuments({ paperId });
49+
50+
if (count >= MAX_REPORTS_PER_PAPER) {
51+
return NextResponse.json(
52+
{ error: "Received many reports; we are currently working on it." },
53+
{ status: 429 }
54+
);
55+
}
56+
const reportedFields: ReportedFieldInput[] = Array.isArray(body.reportedFields)
57+
? body.reportedFields
58+
.map((r:Partial<ReportedFieldInput>) => ({
59+
field: typeof r.field === "string" ? r.field.trim() : "",
60+
value: typeof r.value === "string" ? r.value.trim() : undefined,
61+
}))
62+
.filter((r:Partial<ReportedFieldInput>) => r.field)
63+
: [];
64+
65+
for (const rf of reportedFields) {
66+
if (!ALLOWED_FIELDS.includes(rf.field)) {
67+
return NextResponse.json(
68+
{ error: `Invalid field: ${rf.field}` },
69+
{ status: 400 }
70+
);
71+
}
72+
if (rf.field === "exam" && rf.value) {
73+
if (!exams.some(e => e.toLowerCase() === rf.value?.toLowerCase())) {
74+
return NextResponse.json(
75+
{ error: `Invalid exam value: ${rf.value}` },
76+
{ status: 400 }
77+
);
78+
}
79+
}
80+
}
81+
82+
const newReport = await TagReport.create({
83+
paperId,
84+
reportedFields,
85+
comment: body.comment,
86+
reporterEmail: body.reporterEmail,
87+
reporterId: body.reporterId,
88+
});
89+
90+
return NextResponse.json(
91+
{ message: "Report submitted.", report: newReport },
92+
{ status: 201 }
93+
);
94+
} catch (err) {
95+
console.error(err);
96+
return NextResponse.json(
97+
{ error: "Failed to submit tag report." },
98+
{ status: 500 }
99+
);
100+
}
101+
}

src/app/paper/[id]/page.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { extractBracketContent } from "@/lib/utils/string";
77
import axios, { type AxiosResponse } from "axios";
88
import { type Metadata } from "next";
99
import { redirect } from "next/navigation";
10+
import { PaperProvider } from "@/context/PaperContext";
1011

1112
export async function generateMetadata({
1213
params,
@@ -164,10 +165,20 @@ const PaperPage = async ({ params }: { params: { id: string } }) => {
164165
</div>
165166
</h1>
166167
<center>
168+
<PaperProvider
169+
value={{
170+
paperId: params.id,
171+
subject: paper.subject,
172+
exam: paper.exam,
173+
slot: paper.slot,
174+
year: paper.year,
175+
}}
176+
>
167177
<PdfViewer
168178
url={paper.file_url}
169179
name={`${extractBracketContent(paper.subject)}-${paper.exam}-${paper.slot}-${paper.year}`}
170180
></PdfViewer>
181+
</PaperProvider>
171182
</center>
172183
<RelatedPapers />
173184
</>

src/components/ReportButton.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { FaFlag } from "react-icons/fa6";
5+
import { Button } from "./ui/button";
6+
import ReportTagModal from "./ReportTagModal";
7+
import { usePaper } from "@/context/PaperContext";
8+
9+
export default function ReportButton(){
10+
const { paperId, subject, exam, slot, year } = usePaper();
11+
const [open, setOpen] = useState(false);
12+
return (
13+
<>
14+
<Button
15+
onClick={() => setOpen(true)}
16+
className="h-10 w-10 rounded p-0 text-white transition hover:bg-red-600 bg-red-500"
17+
>
18+
<FaFlag className="text-sm" />
19+
</Button>
20+
21+
<ReportTagModal
22+
paperId={paperId}
23+
subject={subject}
24+
exam={exam}
25+
slot={slot}
26+
year={year}
27+
open={open}
28+
setOpen={setOpen}
29+
/>
30+
</>
31+
);
32+
}

0 commit comments

Comments
 (0)