Skip to content

Commit 06989da

Browse files
committed
fix build
1 parent 8fbfe98 commit 06989da

7 files changed

Lines changed: 82 additions & 54 deletions

File tree

.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,5 @@ KV_REST_API_TOKEN="" # The REST API token for your Vercel KV inst
2929
KV_REST_API_READ_ONLY_TOKEN="" # The read-only REST API token for your Vercel KV instance
3030

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

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

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ interface ReportedFieldInput {
99
field: string;
1010
value?: string;
1111
}
12+
interface ReportTagBody {
13+
paperId?: string;
14+
reportedFields?: unknown;
15+
comment?: unknown;
16+
reporterEmail?: unknown;
17+
reporterId?: unknown;
18+
}
19+
1220
const ALLOWED_FIELDS = ["subject", "courseCode", "exam", "slot", "year"];
1321

1422
const ratelimit = new Ratelimit({
@@ -18,15 +26,15 @@ const ratelimit = new Ratelimit({
1826
});
1927

2028
function getClientIp(req: Request & { ip?: string}): string {
21-
return req.ip || "127.0.0.1";
29+
return req.ip ?? "127.0.0.1";
2230
}
2331

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

28-
const body = await req.json();
29-
const { paperId } = body;
36+
const body = (await req.json()) as ReportTagBody;
37+
const paperId = typeof body.paperId === "string" ? body.paperId : undefined;
3038

3139
if (!paperId) {
3240
return NextResponse.json(
@@ -55,12 +63,14 @@ export async function POST(req: Request & { ip?: string }) {
5563
}
5664
const reportedFields: ReportedFieldInput[] = Array.isArray(body.reportedFields)
5765
? 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-
: [];
66+
.map((r): ReportedFieldInput | null => {
67+
if (!r || typeof r !== "object") return null;
68+
69+
const field = typeof (r as { field?: unknown }).field === "string" ? (r as { field: string }).field.trim() : "";
70+
const value = typeof (r as { value?: unknown }).value === "string" ? (r as { value: string }).value.trim() : undefined;
71+
return field ? { field, value } : null;
72+
})
73+
.filter((r): r is ReportedFieldInput => r !== null):[];
6474

6575
for (const rf of reportedFields) {
6676
if (!ALLOWED_FIELDS.includes(rf.field)) {
@@ -82,9 +92,10 @@ export async function POST(req: Request & { ip?: string }) {
8292
const newReport = await TagReport.create({
8393
paperId,
8494
reportedFields,
85-
comment: body.comment,
86-
reporterEmail: body.reporterEmail,
87-
reporterId: body.reporterId,
95+
comment: typeof body.comment === "string" ? body.comment : undefined,
96+
reporterEmail: typeof body.reporterEmail === "string" ? body.reporterEmail : undefined,
97+
reporterId: typeof body.reporterId === "string" ? body.reporterId : undefined,
98+
8899
});
89100

90101
return NextResponse.json(

src/components/Footer.tsx

Lines changed: 9 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,14 +43,14 @@ export default function Footer() {
3743
body: JSON.stringify({ email }),
3844
})
3945
.then(async (res) => {
40-
const data = await res.json();
41-
if (!res.ok) return Promise.reject(data.error || "Something went wrong.");
46+
const data = (await res.json()) as SubscribeResponse;
47+
if (!res.ok) throw new Error(data.error ?? "Something went wrong.");
4248
return data;
4349
}),
4450
{
4551
loading: "Subscribing...",
4652
success: "You've Successfully Subscribed!",
47-
error: (err: any) => err,
53+
error: (err: Error) => err.message,
4854
},
4955
);
5056

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>

src/context/filterContext.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import React, {
77
useCallback,
88
type ReactNode,
99
} from "react";
10-
import { useRouter, useSearchParams } from "next/navigation";
10+
import { ReadonlyURLSearchParams, useRouter, useSearchParams } from "next/navigation";
1111
import { type IPaper, type Filters } from "@/interface";
1212
import JSZip from "jszip";
1313
import { toast } from "react-hot-toast";
@@ -154,12 +154,16 @@ export const FilterProvider: React.FC<FilterProviderProps> = ({
154154
console.error(`Failed to fetch ${paper.file_url}`, err);
155155
}
156156
}
157-
157+
function getDownloadName(params:ReadonlyURLSearchParams, key: string, fallback = "download"): string {
158+
const value = params.get(key);
159+
if (!value) return fallback;
160+
return value.split(" [")[0]?.trim() ?? fallback;
161+
}
158162
const zipBlob = await zip.generateAsync({ type: "blob" });
159163
const url = URL.createObjectURL(zipBlob);
160164
const a = document.createElement("a");
161165
a.href = url;
162-
a.download = searchParams.get("subject")?.split(" [")[0];
166+
a.download = getDownloadName(searchParams,"subject");
163167
document.body.appendChild(a);
164168
a.click();
165169
a.remove();

0 commit comments

Comments
 (0)