Skip to content

Commit 4b7cf8d

Browse files
Merge pull request #60 from CodeChefVIT/staging
Staging
2 parents 727517b + b8a73d0 commit 4b7cf8d

File tree

8 files changed

+245
-49
lines changed

8 files changed

+245
-49
lines changed

public/papers.png

698 KB
Loading

src/app/actions/get-papers-by-id.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
// "use server";
2-
import { ErrorResponse, PaperResponse } from "@/interface";
3-
import axios, { AxiosResponse } from "axios";
2+
import { ErrorResponse, type PaperResponse } from "@/interface";
3+
import axios, { type AxiosResponse } from "axios";
4+
5+
export const fetchPaperID = async (id: string): Promise<PaperResponse> => {
6+
const serverUrl = process.env.SERVER_URL ?? "https://papers.codechefvit.com";
47

5-
export const fetchPaperID = async (
6-
id: string,
7-
) => {
88
try {
9-
if(!process.env.SERVER_URL)
10-
{
11-
throw "error env not set (server url)"
12-
}
139
const response: AxiosResponse<PaperResponse> = await axios.get(
14-
`${process.env.SERVER_URL}/api/paper-by-id/${id}`,
10+
`${serverUrl}/api/paper-by-id/${id}`
1511
);
1612
return response.data;
1713
} catch (err: unknown) {
18-
throw err;
19-
14+
if (axios.isAxiosError(err)) {
15+
console.error("Axios error:", err.response?.data || err.message);
16+
const errorMessage = (err.response?.data as { message?: string })?.message ?? "Failed to fetch paper";
17+
throw new Error(errorMessage);
18+
} else {
19+
console.error("Unexpected error:", err);
20+
throw new Error("An unexpected error occurred");
21+
}
2022
}
2123
};

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: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import Footer from "@/components/Footer";
33
import Navbar from "@/components/Navbar";
44
import PdfViewer from "@/components/pdfViewer";
55
import Loader from "@/components/ui/loader";
6-
import { ErrorResponse, PaperResponse } from "@/interface";
7-
import axios, { AxiosResponse } from "axios";
8-
import { Metadata } from "next";
6+
import { type ErrorResponse, type PaperResponse } from "@/interface";
7+
import axios, { type AxiosResponse } from "axios";
8+
import { type Metadata } from "next";
99
import { redirect } from "next/navigation"; // Import redirect
1010

1111
export async function generateMetadata({
@@ -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;

0 commit comments

Comments
 (0)