Skip to content

Commit 2349e96

Browse files
Merge pull request #59 from abhitrueprogrammer/staging
papers count api, metadata update for paper by id page, updated subject selection on upload page to be similar to one on homepage, server verification on form data and dynamic years dropdown
2 parents 0face27 + 511f27d commit 2349e96

File tree

7 files changed

+228
-34
lines changed

7 files changed

+228
-34
lines changed

public/papers.png

698 KB
Loading

src/app/api/papers/count/route.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { NextResponse, NextRequest } from "next/server";
2+
import { connectToDatabase } from "@/lib/mongoose";
3+
import Paper from "@/db/papers";
4+
5+
export const dynamic = "force-dynamic";
6+
7+
export async function GET() {
8+
try {
9+
await connectToDatabase();
10+
11+
12+
const count: number = await Paper.countDocuments();
13+
14+
return NextResponse.json(
15+
{ count },
16+
{ status: 200 }
17+
);
18+
} catch (error) {
19+
return NextResponse.json(
20+
{ message: "Failed to fetch papers", error },
21+
{ status: 500 }
22+
);
23+
}
24+
}

src/app/api/upload/route.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { NextRequest, NextResponse } from "next/server";
22
import { PDFDocument } from "pdf-lib";
3-
3+
import {courses, slots, years} from "@/components/select_options"
44
import { connectToDatabase } from "@/lib/mongoose";
55
import cloudinary from "cloudinary";
66
import {
@@ -29,7 +29,14 @@ export async function POST(req: Request) {
2929
const year = formData.get("year") as string;
3030
const exam = formData.get("exam") as string;
3131
const isPdf = formData.get("isPdf") === "true"; // Convert string to boolean
32-
32+
if(!(courses.includes(subject) && slots.includes(slot) && years.includes(year)))
33+
{
34+
return NextResponse.json(
35+
{ message: "Bad Request" },
36+
37+
{ status: 400 },
38+
);
39+
}
3340
await connectToDatabase();
3441
let finalUrl: string | undefined = "";
3542
let public_id_cloudinary: string | undefined = "";

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

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,37 @@ export async function generateMetadata({
1616
const paper: PaperResponse | null = await fetchPaperID(params.id);
1717

1818
if (paper) {
19-
const subject = paper.subject;
2019
return {
21-
title: `Papers | ${subject}`,
20+
metadataBase: new URL("https://papers.codechefvit.com/"),
21+
title: `Papers| ${paper.subject}| ${paper.exam} |${paper.slot}`,
22+
description:
23+
`Discover ${paper.subject}'s question paper created by CodeChef-VIT at Vellore Institute of Technology. Made with ♡ to help students excel.`,
24+
icons: [{ rel: "icon", url: "/codechef_logo.svg" }],
2225
openGraph: {
23-
title: `Papers | ${subject}`,
26+
title: `Papers| ${paper.subject}| ${paper.exam} |${paper.slot}`,
27+
images: [{ url: "/papers.png" }],
28+
url: "https://papers.codechefvit.com/",
29+
type: "website",
30+
description:
31+
`Discover ${paper.subject}'s question paper created by CodeChef-VIT at Vellore Institute of Technology. Made with ♡ to help students excel.`,
32+
siteName: "Papers by CodeChef-VIT",
2433
},
2534
twitter: {
26-
title: `Papers | ${subject}`,
35+
card: "summary_large_image",
36+
title: `Papers| ${paper.subject}| ${paper.exam} |${paper.slot}`,
37+
description:
38+
`Discover ${paper.subject}'s question paper created by CodeChef-VIT at Vellore Institute of Technology. Made with ♡ to help students excel.`,
39+
images: [{ url: "/papers.png" }],
2740
},
41+
applicationName: "Papers by CodeChef-VIT",
42+
keywords: [
43+
paper.subject,
44+
paper.exam,
45+
paper.slot,
46+
paper.year
47+
],
48+
robots: "index, follow",
49+
2850
};
2951
}
3052

@@ -39,7 +61,6 @@ const PaperPage = async ({ params }: { params: { id: string } }) => {
3961
return paper;
4062
} catch (err) {
4163
if (axios.isAxiosError(err)) {
42-
4364
const errorResponse = err.response as AxiosResponse<ErrorResponse>;
4465
if (errorResponse?.status === 400 || errorResponse?.status === 404) {
4566
redirect("/");
@@ -69,7 +90,7 @@ const PaperPage = async ({ params }: { params: { id: string } }) => {
6990
{paper.subject} {paper.exam} {paper.slot} {paper.year}
7091
</h1>
7192
<center>
72-
<PdfViewer url={paper.finalUrl}></PdfViewer>
93+
<PdfViewer url={paper.finalUrl}></PdfViewer>
7394
</center>
7495
</>
7596
)}

src/app/upload/page.tsx

Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"use client";
22
import React, { useRef, useState } from "react";
33
import axios from "axios";
4-
import { slots, courses } from "./select_options";
54
import toast, { Toaster } from "react-hot-toast";
65
import { handleAPIError } from "../../util/error";
76
import { useRouter } from "next/navigation";
@@ -28,7 +27,9 @@ import {
2827
} from "@/components/ui/command";
2928
import Navbar from "@/components/Navbar";
3029
import Footer from "@/components/Footer";
31-
import {PostPDFToCloudinary} from "@/interface"
30+
import { PostPDFToCloudinary } from "@/interface";
31+
import { courses, slots, years } from "@/components/select_options";
32+
import SearchBar from "@/components/searchbarSubjectList";
3233
const Page = () => {
3334
const router = useRouter();
3435
const fileInputRef = useRef<HTMLInputElement>(null);
@@ -95,8 +96,7 @@ const Page = () => {
9596
let isPdf = false;
9697
if (files[0]?.type === "application/pdf") {
9798
isPdf = true;
98-
if(files.length > 1)
99-
{
99+
if (files.length > 1) {
100100
toast.error(`PDFs should be uploaded seperately`);
101101
return;
102102
}
@@ -136,8 +136,6 @@ const Page = () => {
136136
error: (err: ApiError) => err.message,
137137
},
138138
);
139-
140-
141139
};
142140

143141
const handleSubjectSelect = (value: string) => {
@@ -197,15 +195,17 @@ const Page = () => {
197195
{/* Subject Selection */}
198196
<div>
199197
<label>Subject:</label>
200-
<Command className="rounded-lg border shadow-md md:min-w-[450px]">
198+
{/* setSubject */}
199+
<SearchBar setSubject={setSubject}></SearchBar>
200+
{/* <Command className="rounded-lg border shadow-md md:min-w-[450px]">
201201
<CommandInput
202202
value={inputValue}
203203
onChangeCapture={(e) =>
204204
setInputValue((e.target as HTMLInputElement).value)
205205
}
206206
placeholder="Type a subject or search..."
207-
/>
208-
<CommandList className="h-[100px]">
207+
/> */}
208+
{/* <CommandList className="h-[100px]">
209209
<CommandEmpty>No results found.</CommandEmpty>
210210
211211
<CommandGroup heading="Subjects">
@@ -219,7 +219,7 @@ const Page = () => {
219219
))}
220220
</CommandGroup>
221221
</CommandList>
222-
</Command>
222+
</Command> */}
223223
</div>
224224

225225
{/* Year Selection */}
@@ -232,21 +232,14 @@ const Page = () => {
232232
<SelectContent>
233233
<SelectGroup>
234234
<SelectLabel>Years</SelectLabel>
235-
{(() => {
236-
const options = [];
237-
for (
238-
let i = 2011;
239-
i <= Number(new Date().getFullYear());
240-
i++
241-
) {
242-
options.push(
243-
<SelectItem key={i} value={String(i)}>
244-
{i}
245-
</SelectItem>,
246-
);
247-
}
248-
return options;
249-
})()}
235+
{years.map((year)=>
236+
{
237+
return (<SelectItem key={year} value={String(year)}>
238+
{year}
239+
</SelectItem>)
240+
241+
}
242+
)}
250243
</SelectGroup>
251244
</SelectContent>
252245
</Select>
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
"use client";
2+
3+
import { useState, useCallback, useRef, useEffect } from "react";
4+
import { Search } from "lucide-react";
5+
import debounce from "debounce";
6+
import { Input } from "@/components/ui/input";
7+
import { courses } from "./select_options";
8+
9+
function searchbarSubjectList({
10+
setSubject,
11+
}: {
12+
setSubject: React.Dispatch<React.SetStateAction<string>>;
13+
}) {
14+
const [searchText, setSearchText] = useState("");
15+
const [suggestions, setSuggestions] = useState<string[]>([]);
16+
const [error, setError] = useState<string | null>(null);
17+
const [loading, setLoading] = useState(false);
18+
const suggestionsRef = useRef<HTMLUListElement | null>(null);
19+
20+
const debouncedSearch = useCallback(
21+
debounce(async (text: string) => {
22+
if (text.length > 0) {
23+
setLoading(true);
24+
const escapedSearchText = text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
25+
26+
const regex = new RegExp(escapedSearchText, "i");
27+
const filteredSubjects = courses
28+
.filter((subject) => subject.search(regex) !== -1)
29+
.slice(0, 10);
30+
31+
if (filteredSubjects.length === 0) {
32+
setError("Subject not found");
33+
return;
34+
}
35+
setSuggestions(filteredSubjects);
36+
setError(null);
37+
38+
setLoading(false);
39+
} else {
40+
setSuggestions([]);
41+
}
42+
}, 500),
43+
[],
44+
);
45+
46+
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
47+
const text = e.target.value;
48+
setSearchText(text);
49+
if (text.length <= 0) {
50+
setSuggestions([]);
51+
}
52+
void debouncedSearch(text);
53+
};
54+
55+
const handleSelectSuggestion = async (suggestion: string) => {
56+
setSearchText(suggestion);
57+
setSuggestions([]);
58+
setSubject(suggestion);
59+
// router.push(`/catalogue?subject=${encodeURIComponent(suggestion)}`);
60+
};
61+
62+
const handleClickOutside = (event: MouseEvent) => {
63+
if (
64+
suggestionsRef.current &&
65+
!suggestionsRef.current.contains(event.target as Node)
66+
) {
67+
setSuggestions([]);
68+
}
69+
};
70+
71+
useEffect(() => {
72+
document.addEventListener("mousedown", handleClickOutside);
73+
return () => {
74+
document.removeEventListener("mousedown", handleClickOutside);
75+
};
76+
}, []);
77+
78+
return (
79+
<div className="mx-4 md:mx-0">
80+
<form className=" my-2 ml-2 w-full max-w-xl">
81+
<div className="relative">
82+
<Input
83+
type="text"
84+
value={searchText}
85+
onChange={handleSearchChange}
86+
placeholder="Search for subject..."
87+
// className={`text-md w-fuyll rounded-full border bg-[#434dba] px-4 py-6 pr-10 font-sans tracking-wider text-white shadow-sm placeholder:text-white focus:outline-none focus:ring-2 ${loading ? "opacity-70" : ""}`}
88+
/>
89+
<button
90+
type="submit"
91+
className="absolute inset-y-0 right-0 flex items-center pr-3"
92+
disabled
93+
>
94+
{" "}
95+
<Search className="h-5 w-5 text-white " />
96+
</button>
97+
{loading && (
98+
<div className="text-md absolute z-20 mt-2 w-full max-w-xl rounded-md border border-[#434dba] bg-white p-2 text-center font-sans font-semibold tracking-wider dark:bg-[#030712]">
99+
Loading suggestions...
100+
</div>
101+
)}
102+
{(suggestions.length > 0 || error) && !loading && (
103+
<ul
104+
ref={suggestionsRef}
105+
className="absolute z-20 mx-0.5 mt-2 w-full max-w-xl rounded-md border border-[#434dba] bg-white text-center dark:bg-[#030712] md:mx-0"
106+
>
107+
{error ? (
108+
<li className="text-red p-2">{error}</li>
109+
) : (
110+
suggestions.map((suggestion, index) => (
111+
<li
112+
key={index}
113+
onClick={() => handleSelectSuggestion(suggestion)}
114+
className="cursor-pointer truncate p-2 hover:opacity-50"
115+
style={{
116+
width: "100%",
117+
overflow: "hidden",
118+
whiteSpace: "nowrap",
119+
textOverflow: "ellipsis",
120+
}}
121+
>
122+
{suggestion}
123+
</li>
124+
))
125+
)}
126+
</ul>
127+
)}
128+
</div>
129+
</form>
130+
</div>
131+
);
132+
}
133+
134+
export default searchbarSubjectList;

src/app/upload/select_options.ts renamed to src/components/select_options.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,5 +245,20 @@ const slots: string[] = [
245245
"F2",
246246
"G2",
247247
];
248+
function getYears(startYear: number) {
249+
const currentYear = new Date().getFullYear(); // Get the current year
250+
const years = [];
248251

249-
export { slots, courses };
252+
// Loop from startYear to currentYear and add each year to the array
253+
for (let year = startYear; year <= currentYear; year++) {
254+
years.push(String(year));
255+
}
256+
257+
return years;
258+
}
259+
260+
// Example usage:
261+
const startYear = 2011;
262+
const years = getYears(startYear);
263+
264+
export { slots, courses, years };

0 commit comments

Comments
 (0)