Skip to content

Commit f189ee7

Browse files
merge with staging
2 parents 9399d2f + 2c58532 commit f189ee7

10 files changed

Lines changed: 142 additions & 85 deletions

File tree

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,4 @@ KV_REST_API_READ_ONLY_TOKEN="" # The read-only REST API token for your Verc
3030

3131
# Upstash_Redis
3232
UPSTASH_REDIS_REST_URL="" # REST URL of your Upstash Redis database
33-
UPSTASH_REDIS_REST_TOKEN="" # REST API token for Upstash Redis
33+
UPSTASH_REDIS_REST_TOKEN="" # REST API token for Upstash Redis

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ Before getting started, please ensure that the .env file is properly configured.
7070
Dev names and stats be seen in [github stats](https://github.com/CodeChefVIT/papers-codechef/graphs/contributors) page
7171

7272
## Maintainers
73-
Our top maintainers, making sure the Database of papers is maintainered and upto-date.
73+
Our top maintainers, making sure the database of papers is maintained and upto-date.
7474
<table>
7575
<tr>
7676
<td align="center">

src/app/api/papers/route.ts

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,25 @@
11
import { NextResponse, type NextRequest } from "next/server";
2-
import { connectToDatabase } from "@/lib/database/mongoose";
3-
import Paper from "@/db/papers";
4-
import { type IPaper } from "@/interface";
5-
import { escapeRegExp } from "@/lib/utils/regex";
6-
import { extractUniqueValues } from "@/lib/utils/paper-aggregation";
2+
import { getPapersBySubject } from "@/lib/services/paper";
73

84
export const dynamic = "force-dynamic";
95

106
export async function GET(req: NextRequest) {
117
try {
12-
await connectToDatabase();
138
const url = req.nextUrl.searchParams;
14-
const subject = url.get("subject");
15-
16-
if (!subject) {
9+
const sub = url.get("subject");
10+
if (!sub) {
1711
return NextResponse.json(
1812
{ message: "Subject query parameter is required" },
1913
{ status: 400 },
2014
);
2115
}
16+
const paper = await getPapersBySubject(sub);
2217

23-
const escapedSubject = escapeRegExp(subject);
24-
const papers: IPaper[] = await Paper.find({
25-
subject: { $regex: new RegExp(`${escapedSubject}`, "i") },
26-
});
27-
28-
const uniqueValues = extractUniqueValues(papers);
29-
30-
return NextResponse.json(
31-
{
32-
papers,
33-
...uniqueValues,
34-
},
35-
{ status: 200 },
36-
);
18+
return NextResponse.json(paper, { status: 200 });
3719
} catch (error) {
3820
return NextResponse.json(
3921
{ message: "Failed to fetch papers", error },
4022
{ status: 500 },
4123
);
4224
}
43-
}
25+
}

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

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,48 @@ import { connectToDatabase } from "@/lib/database/mongoose";
33
import TagReport from "@/db/tagReport";
44
import { Ratelimit } from "@upstash/ratelimit";
55
import { redis } from "@/lib/utils/redis";
6-
import { exams } from "@/components/select_options";
6+
7+
const exams: string[] = ["CAT-1", "CAT-2", "FAT", "Model CAT-1", "Model CAT-2", "Model FAT"]
78

89
interface ReportedFieldInput {
910
field: string;
1011
value?: string;
1112
}
13+
interface ReportTagBody {
14+
paperId?: string;
15+
reportedFields?: unknown;
16+
comment?: unknown;
17+
reporterEmail?: unknown;
18+
reporterId?: unknown;
19+
}
20+
1221
const ALLOWED_FIELDS = ["subject", "courseCode", "exam", "slot", "year"];
1322

14-
const ratelimit = new Ratelimit({
15-
redis,
16-
limiter: Ratelimit.slidingWindow(3, "1 h"),//per id - 3 request - per hour
17-
analytics: true,
23+
function getRateLimit(){
24+
return new Ratelimit({
25+
redis,
26+
limiter: Ratelimit.slidingWindow(3, "1 h"),//per id - 3 request - per hour
27+
analytics: true,
1828
});
19-
29+
}
2030
function getClientIp(req: Request & { ip?: string}): string {
21-
return req.ip || "127.0.0.1";
31+
const xff = req.headers.get("x-forwarded-for");
32+
if (typeof xff === "string" && xff.length > 0) {
33+
return xff.split(",")[0]?.trim()??"";
34+
}
35+
const xri = req.headers.get("x-real-ip");
36+
if (typeof xri === "string" && xri.length > 0) {
37+
return xri;
38+
}
39+
return "0.0.0.0";
2240
}
2341

2442
export async function POST(req: Request & { ip?: string }) {
2543
try {
2644
await connectToDatabase();
27-
28-
const body = await req.json();
29-
const { paperId } = body;
45+
const ratelimit = getRateLimit();
46+
const body = (await req.json()) as ReportTagBody;
47+
const paperId = typeof body.paperId === "string" ? body.paperId : undefined;
3048

3149
if (!paperId) {
3250
return NextResponse.json(
@@ -55,12 +73,14 @@ export async function POST(req: Request & { ip?: string }) {
5573
}
5674
const reportedFields: ReportedFieldInput[] = Array.isArray(body.reportedFields)
5775
? 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-
: [];
76+
.map((r): ReportedFieldInput | null => {
77+
if (!r || typeof r !== "object") return null;
78+
79+
const field = typeof (r as { field?: unknown }).field === "string" ? (r as { field: string }).field.trim() : "";
80+
const value = typeof (r as { value?: unknown }).value === "string" ? (r as { value: string }).value.trim() : undefined;
81+
return field ? { field, value } : null;
82+
})
83+
.filter((r): r is ReportedFieldInput => r !== null):[];
6484

6585
for (const rf of reportedFields) {
6686
if (!ALLOWED_FIELDS.includes(rf.field)) {
@@ -82,9 +102,10 @@ export async function POST(req: Request & { ip?: string }) {
82102
const newReport = await TagReport.create({
83103
paperId,
84104
reportedFields,
85-
comment: body.comment,
86-
reporterEmail: body.reporterEmail,
87-
reporterId: body.reporterId,
105+
comment: typeof body.comment === "string" ? body.comment : undefined,
106+
reporterEmail: typeof body.reporterEmail === "string" ? body.reporterEmail : undefined,
107+
reporterId: typeof body.reporterId === "string" ? body.reporterId : undefined,
108+
88109
});
89110

90111
return NextResponse.json(

src/components/Footer.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ import {
1515
} from "react-icons/fa6";
1616
import { Bold, Mail } from "lucide-react";
1717
import toast from "react-hot-toast";
18+
19+
type SubscribeResponse = {
20+
success?: boolean;
21+
error?: string;
22+
};
23+
1824
export default function Footer() {
1925
const { theme } = useTheme();
2026
const [isDarkMode, setIsDarkMode] = useState<boolean | null>(true);
@@ -37,8 +43,9 @@ export default function Footer() {
3743
body: JSON.stringify({ email }),
3844
})
3945
.then(async (res) => {
40-
if (!res.ok) throw new Error("Network response was not ok.");
41-
return res.json();
46+
const data = (await res.json()) as SubscribeResponse;
47+
if (!res.ok) throw new Error(data.error ?? "Something went wrong.");
48+
return data;
4249
}),
4350
{
4451
loading: "Subscribing...",
@@ -97,7 +104,7 @@ export default function Footer() {
97104
{/* Events */}
98105
<div className="flex w-full flex-col gap-2 text-black dark:text-white lg:w-[15%]">
99106
<h3 className="font-jost text-xl font-semibold">Events</h3>
100-
<Link href="https://devsoc25.codechefvit.com" target="_blank">DevSoc</Link>
107+
<Link href="https://devsoc25.codechefvit.com" target="_blank">DevSOC</Link>
101108
<Link href="https://gravitas.codechefvit.com" target="_blank">CookOff</Link>
102109
<Link href="https://gravitas.codechefvit.com" target="_blank">Clueminati</Link>
103110
</div>

src/components/ReportTagModal.tsx

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ import { Button } from "@/components/ui/button";
1414
import { MultiSelect } from "@/components/multi-select";
1515
import LabeledInput from "@/components/ui/LabeledInput";
1616
import LabeledSelect from "@/components/ui/LabeledSelect";
17-
import axios from "axios";
17+
import axios, { AxiosError } from "axios";
1818
import toast from "react-hot-toast";
1919

20+
type ReportResponse = { error?: string; message?: string };
21+
2022
interface ReportTagModalProps {
2123
paperId: string;
2224
subject?: string;
@@ -40,8 +42,8 @@ const ReportTagModal = ({
4042
const [internalOpen, setInternalOpen] = useState(false);
4143
const isControlled = open !== undefined && setOpen !== undefined;
4244

43-
const modalOpen = isControlled ? open! : internalOpen;
44-
const modalSetOpen = isControlled ? setOpen! : setInternalOpen;
45+
const modalOpen = isControlled ? open : internalOpen;
46+
const modalSetOpen = isControlled ? setOpen : setInternalOpen;
4547
const [comment, setComment] = useState("");
4648
const [email, setEmail] = useState("");
4749
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
@@ -64,14 +66,14 @@ const ReportTagModal = ({
6466
const isDirty = useMemo(() => {
6567
if (selectedCategories.length === 0) return false;
6668
for (const c of selectedCategories) {
67-
const curr = (categoryValues[c] || "").trim();
68-
const orig = (originalCategoryValues[c] || "").trim();
69+
const curr = (categoryValues[c] ?? "").trim();
70+
const orig = (originalCategoryValues[c] ?? "").trim();
6971
if (curr !== orig) return true;
7072
}
7173

7274
if (selectedCategories.includes("subject")) {
73-
const currCode = (categoryValues["courseCode"] || "").trim();
74-
const origCode = (originalCategoryValues["courseCode"] || "").trim();
75+
const currCode = (categoryValues.courseCode ?? "").trim();
76+
const origCode = (originalCategoryValues.courseCode ?? "").trim();
7577
if (currCode !== origCode) return true;
7678
}
7779

@@ -83,7 +85,8 @@ const ReportTagModal = ({
8385
for (const c of selectedCategories) {
8486
if (categoryValues[c]) continue;
8587
if (c === "subject" && subject) {
86-
const m = subject.match(/^(.*)\s*\[([^\]]+)\]\s*$/);
88+
const subjectRegex = /^(.*)\s*\[([^\]]+)\]\s*$/;
89+
const m = subjectRegex.exec(subject);
8790
if (m?.[1] && m?.[2]) {
8891
const name = m[1].trim();
8992
const code = m[2].trim();
@@ -104,17 +107,18 @@ const ReportTagModal = ({
104107
if (open) {
105108
const base: Record<string, string> = {};
106109
if (subject) {
107-
const m = subject.match(/^(.*)\s*\[([^\]]+)\]\s*$/);
110+
const subjectRegex = /^(.*)\s*\[([^\]]+)\]\s*$/;
111+
const m = subjectRegex.exec(subject);
108112
if (m?.[1] && m?.[2]) {
109-
base["subject"] = m[1].trim();
110-
base["courseCode"] = m[2].trim();
113+
base.subject = m[1].trim();
114+
base.courseCode = m[2].trim();
111115
} else {
112-
base["subject"] = subject;
116+
base.subject = subject;
113117
}
114118
}
115-
if (exam) base["exam"] = exam;
116-
if (slot) base["slot"] = slot;
117-
if (year) base["year"] = year;
119+
if (exam) base.exam = exam;
120+
if (slot) base.slot = slot;
121+
if (year) base.year = year;
118122
setOriginalCategoryValues(base);
119123
setOriginalEmail("");
120124
} else {
@@ -134,23 +138,23 @@ const ReportTagModal = ({
134138
}
135139

136140
if (selectedCategories.includes("subject")) {
137-
const sub = (categoryValues.subject || "").trim();
141+
const sub = (categoryValues.subject ?? "").trim();
138142
if (!sub) {
139143
toast.error("Subject name cannot be empty.");
140144
return;
141145
}
142146
}
143147

144148
if (selectedCategories.includes("slot")) {
145-
const v = (categoryValues.slot || "").trim();
149+
const v = (categoryValues.slot ?? "").trim();
146150
const slotRegex = /^[A-G][1-2]$/;
147151
if (!slotRegex.test(v)) {
148152
toast.error("Slot must be from A1 to G2 (e.g., D1, B2).");
149153
return;
150154
}
151155
}
152156
if (selectedCategories.includes("year")) {
153-
const y = (categoryValues.year || "").trim();
157+
const y = (categoryValues.year ?? "").trim();
154158
const yearRegex = /^\d{4}(-\d{4})?$/;
155159
if (!yearRegex.test(y)) {
156160
toast.error("Year must be a valid format (e.g., 2024 or 2024-2025).");
@@ -180,15 +184,15 @@ const ReportTagModal = ({
180184

181185
for (const c of selectedCategories) {
182186
if (c === "subject") {
183-
const newSub = (categoryValues.subject || "").trim();
184-
const oldSub = (originalCategoryValues.subject || "").trim();
187+
const newSub = (categoryValues.subject ?? "").trim();
188+
const oldSub = (originalCategoryValues.subject ?? "").trim();
185189

186190
if (newSub !== oldSub) {
187191
reportedFields.push({ field: "subject", value: newSub });
188192
}
189193

190-
const newCode = (categoryValues.courseCode || "").trim();
191-
const oldCode = (originalCategoryValues.courseCode || "").trim();
194+
const newCode = (categoryValues.courseCode ?? "").trim();
195+
const oldCode = (originalCategoryValues.courseCode ?? "").trim();
192196

193197
if (newCode !== oldCode) {
194198
reportedFields.push({ field: "courseCode", value: newCode });
@@ -197,8 +201,8 @@ const ReportTagModal = ({
197201
continue;
198202
}
199203

200-
const newVal = (categoryValues[c] || "").trim();
201-
const oldVal = (originalCategoryValues[c] || "").trim();
204+
const newVal = (categoryValues[c] ?? "").trim();
205+
const oldVal = (originalCategoryValues[c] ?? "").trim();
202206

203207
if (newVal !== oldVal) {
204208
reportedFields.push({ field: c, value: newVal });
@@ -213,20 +217,23 @@ if (reportedFields.length === 0 && comment.trim().length === 0) {
213217
paperId,
214218
reportedFields,
215219
comment,
216-
reporterEmail: email || undefined,
220+
reporterEmail: email ?? undefined,
217221
};
218222
setLoading(true);
219223
await toast.promise(
220-
axios.post("/api/report-tag", payload),
224+
axios.post<ReportResponse>("/api/report-tag", payload),
221225
{
222226
loading: "Submitting your report...",
223227
success: "Reported successfully. Thank you, We will work on that",
224-
error: (err)=>{
225-
return (
226-
err?.response?.data?.error ||
227-
err?.message ||
228-
"Failed to submit report."
229-
)
228+
error: (err: unknown)=>{
229+
if (axios.isAxiosError<ReportResponse>(err)) {
230+
return (
231+
err.response?.data?.error ??
232+
err.message ??
233+
"Failed to submit report."
234+
);
235+
}
236+
return err instanceof Error ? err.message : "Failed to submit report.";
230237
},
231238
}
232239
)

src/components/ui/LabeledSelect.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const LabeledSelect = ({ label, value, onChange, options, placeholder }: Labeled
1717
<Field label={label}>
1818
<Select value={value} onValueChange={onChange}>
1919
<SelectTrigger className="w-full">
20-
<SelectValue placeholder={placeholder || "Select"} />
20+
<SelectValue placeholder={placeholder ?? "Select"} />
2121
</SelectTrigger>
2222
<SelectContent>
2323
{options.map((opt) => (

src/components/ui/field.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export function Field({ label, children, className }: {
66
className?: string;
77
}) {
88
return (
9-
<div className={`space-y-1 w-full ${className || ""}`}>
9+
<div className={`space-y-1 w-full ${className ?? ""}`}>
1010
<Label>{label}</Label>
1111
{children}
1212
</div>

0 commit comments

Comments
 (0)