Skip to content

Commit 786a757

Browse files
Merge pull request #252 from Advik-Gupta/staging
Feat: fixed ui bugs
2 parents f6a40aa + 42e17f9 commit 786a757

23 files changed

+551
-189
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<h2 align="center"> Papers </h2>
55
<br/>
66

7+
78
> <p>Prepare to excel in your CATs and FATs with CodeChef-VIT's dedicated repository of past exam papers. Access key resources to review concepts, tackle challenging questions, and familiarize yourself with exam patterns. Boost your confidence, sharpen your strategy, and get ready to ace your exams!</p>
89
910
## 🌐 Deploy
@@ -17,7 +18,6 @@
1718
- MongoDB & Mongoose : Database and object data modeling (ODM) for Node.js.
1819
- Cloudinary : Media storage and optimization service.
1920
- Shadcn : Collection of pre-built components using Radix UI and Tailwind CSS.
20-
2121
## 💡 Features:
2222

2323
- Access a vast collection of past CAT and FAT papers

ongoing-papers.ts

Lines changed: 0 additions & 43 deletions
This file was deleted.

src/app/api/related-subject/route.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { NextResponse, type NextRequest } from "next/server";
2+
import { connectToDatabase } from "@/lib/mongoose";
3+
import { IRelatedSubject } from "@/interface";
4+
import RelatedSubject from "@/db/relatedSubjects";
5+
6+
export const dynamic = "force-dynamic";
7+
8+
export async function GET(req: NextRequest) {
9+
try {
10+
await connectToDatabase();
11+
const url = req.nextUrl.searchParams;
12+
const subject = url.get("subject");
13+
const escapeRegExp = (text: string) => {
14+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15+
};
16+
const escapedSubject = escapeRegExp(subject ?? "");
17+
18+
if (!subject) {
19+
return NextResponse.json(
20+
{ message: "Subject query parameter is required" },
21+
{ status: 400 },
22+
);
23+
}
24+
const subjects: IRelatedSubject[] = await RelatedSubject.find({
25+
subject: { $regex: new RegExp(`${escapedSubject}`, "i") },
26+
});
27+
console.log("realted", subjects);
28+
29+
return NextResponse.json(
30+
{
31+
related_subjects: subjects[0]?.related_subjects
32+
},
33+
{ status: 200 },
34+
);
35+
} catch (error) {
36+
return NextResponse.json(
37+
{ message: "Failed to fetch related subject", error },
38+
{ status: 500 },
39+
);
40+
}
41+
}

src/app/api/upload/route.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,5 +181,7 @@ async function CreatePDF(orderedFiles: File[]) {
181181
}
182182

183183
const mergedPdfBytes = await pdfDoc.save();
184-
return mergedPdfBytes;
184+
const ab = new ArrayBuffer(mergedPdfBytes.byteLength);
185+
new Uint8Array(ab).set(mergedPdfBytes);
186+
return ab;
185187
}

src/app/api/user-papers/route.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
import { NextResponse } from "next/server";
22
import { connectToDatabase } from "@/lib/mongoose";
33
import Paper from "@/db/papers";
4-
import { StoredSubjects } from "@/interface";
4+
import { StoredSubjects, TransformedPaper } from "@/interface";
55

66
export const dynamic = "force-dynamic";
77

8-
interface TransformedPaper {
9-
subject: string;
10-
slots: string[];
11-
}
128

139
export async function POST(req: Request) {
1410
try {

src/app/request/page.tsx

Lines changed: 267 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,269 @@
1-
import PapersPage from "@/components/screens/PapersPage";
1+
"use client";
22

3-
export default function RequestPage() {
4-
return <PapersPage />;
3+
import { useEffect, useState, useRef, useMemo } from "react";
4+
import { Button } from "@/components/ui/button";
5+
import {
6+
Select,
7+
SelectContent,
8+
SelectItem,
9+
SelectTrigger,
10+
SelectValue,
11+
} from "@/components/ui/select";
12+
import { exams, slots, years } from "@/components/select_options";
13+
import { Input } from "@/components/ui/input";
14+
import axios from "axios";
15+
import Fuse from "fuse.js";
16+
import { type IUpcomingPaper } from "@/interface";
17+
import { Skeleton } from "../../components/ui/skeleton";
18+
import UpcomingPaper from "../../components/UpcomingPaper";
19+
import toast from "react-hot-toast";
20+
import { Search } from "lucide-react";
21+
import SkeletonPaperCard from "@/components/SkeletonPaperCard";
22+
23+
type Course = {
24+
name?: string | null;
25+
courseName?: string | null;
26+
title?: string | null;
27+
};
28+
29+
export default function PaperRequest() {
30+
const [subjects, setSubjects] = useState<string[]>([]);
31+
const [searchText, setSearchText] = useState("");
32+
const [suggestions, setSuggestions] = useState<string[]>([]);
33+
const [selectedSubject, setSelectedSubject] = useState<string | null>(null);
34+
const [selectedExam, setSelectedExam] = useState<string | null>(null);
35+
const [selectedSlot, setSelectedSlot] = useState<string | null>(null);
36+
const [selectedYear, setSelectedYear] = useState<string | null>(null);
37+
const suggestionsRef = useRef<HTMLUListElement | null>(null);
38+
const [displayPapers, setDisplayPapers] = useState<IUpcomingPaper[]>([]);
39+
const [isLoading, setIsLoading] = useState(true);
40+
41+
useEffect(() => {
42+
async function fetchSubjects() {
43+
try {
44+
const response = await axios.get<Course[]>(`/api/course-list`);
45+
const courses: Course[] = response.data;
46+
const names = courses
47+
.map((course) => course.name ?? course.courseName ?? course.title)
48+
.filter(Boolean) as string[];
49+
50+
setSubjects(names);
51+
} catch (err) {
52+
console.error("Error fetching subjects:", err);
53+
}
54+
}
55+
void fetchSubjects();
56+
}, []);
57+
58+
useEffect(() => {
59+
async function fetchPapers() {
60+
try {
61+
setIsLoading(true);
62+
const response = await axios.get<IUpcomingPaper[]>(
63+
"/api/upcoming-papers",
64+
);
65+
66+
const randomPapers = [...response.data]
67+
.sort(() => Math.random() - 0.5)
68+
.slice(0, 4);
69+
70+
setDisplayPapers(randomPapers);
71+
} catch (error) {
72+
console.error("Failed to fetch papers:", error);
73+
} finally {
74+
setIsLoading(false);
75+
}
76+
}
77+
78+
void fetchPapers();
79+
}, []);
80+
81+
const fuse = useMemo(
82+
() => new Fuse(subjects, { includeScore: true, threshold: 0.3 }),
83+
[subjects],
84+
);
85+
86+
useEffect(() => {
87+
if (!searchText.trim()) {
88+
setSuggestions([]);
89+
return;
90+
}
91+
92+
if (selectedSubject && searchText === selectedSubject) {
93+
setSuggestions([]);
94+
return;
95+
}
96+
97+
const results = fuse.search(searchText);
98+
setSuggestions(results.map((r) => r.item).slice(0, 10));
99+
}, [searchText, fuse, selectedSubject]);
100+
101+
const handleSelectSubject = (subject: string) => {
102+
setSelectedSubject(subject);
103+
setSearchText(subject);
104+
setSuggestions([]);
105+
setSelectedExam(null);
106+
setSelectedSlot(null);
107+
setSelectedYear(null);
108+
};
109+
110+
useEffect(() => {
111+
function handleClickOutside(event: MouseEvent) {
112+
if (
113+
suggestionsRef.current &&
114+
!suggestionsRef.current.contains(event.target as Node)
115+
) {
116+
setSuggestions([]);
117+
}
118+
}
119+
document.addEventListener("mousedown", handleClickOutside);
120+
return () => document.removeEventListener("mousedown", handleClickOutside);
121+
}, []);
122+
123+
const handleSubmit = async () => {
124+
if (!selectedSubject || !selectedExam || !selectedSlot || !selectedYear) {
125+
toast.error("Please fill all fields before submitting.");
126+
return;
127+
}
128+
129+
try {
130+
await toast.promise(
131+
axios.post("/api/request", {
132+
subject: selectedSubject,
133+
exam: selectedExam,
134+
slot: selectedSlot,
135+
year: selectedYear,
136+
}),
137+
{
138+
loading: "Submitting your request...",
139+
success: "Your paper request was submitted successfully",
140+
error: "Failed to submit your request. Please try again later.",
141+
}
142+
);
143+
144+
setSearchText("");
145+
setSelectedSubject(null);
146+
setSelectedExam(null);
147+
setSelectedSlot(null);
148+
setSelectedYear(null);
149+
} catch (error) {
150+
console.error("Error submitting request:", error);
151+
}
152+
};
153+
154+
return (
155+
<div className="min-h-screen bg-[#F3F5FF] px-6 py-12 text-black dark:bg-[#070114] dark:text-white">
156+
<main>
157+
<div className="mx-auto mb-16 max-w-4xl text-center">
158+
<h2 className="mb-12 font-vipnabd text-3xl font-extrabold md:text-4xl">
159+
Specific Paper Request
160+
</h2>
161+
162+
<div className="relative mx-auto mb-8 max-w-xl font-play">
163+
<Input
164+
type="text"
165+
value={searchText}
166+
onChange={(e) => setSearchText(e.target.value)}
167+
placeholder="Search by subject..."
168+
className={`text-md rounded-lg bg-[#B2B8FF] px-4 py-6 pr-10 font-play tracking-wider text-black shadow-sm ring-0 placeholder:text-black focus:outline-none focus:ring-0 dark:bg-[#7480FF66] dark:text-white placeholder:dark:text-white ${suggestions.length > 0 ? "rounded-b-none" : ""}`}
169+
/>
170+
<button
171+
type="button"
172+
className="absolute inset-y-0 right-0 flex items-center pr-3"
173+
>
174+
<Search className="h-5 w-5 text-black dark:text-white" />{" "}
175+
</button>
176+
{suggestions.length > 0 && (
177+
<ul
178+
ref={suggestionsRef}
179+
className="absolute z-20 max-h-[250px] w-full max-w-xl overflow-y-auto rounded-md rounded-t-none border border-t-0 bg-white text-center shadow-lg dark:bg-[#303771]"
180+
>
181+
{suggestions.map((s, idx) => (
182+
<li
183+
key={idx}
184+
onClick={() => handleSelectSubject(s)}
185+
className="cursor-pointer truncate p-2 hover:bg-gray-100 dark:hover:bg-gray-800"
186+
>
187+
{s}
188+
</li>
189+
))}
190+
</ul>
191+
)}
192+
</div>
193+
194+
<div className="mb-8 flex justify-center gap-4">
195+
<Select
196+
onValueChange={setSelectedExam}
197+
disabled={!selectedSubject}
198+
value={selectedExam ?? undefined}
199+
>
200+
<SelectTrigger className="w-32">
201+
<SelectValue placeholder="Exam" />
202+
</SelectTrigger>
203+
<SelectContent>
204+
{exams.map((exam) => (
205+
<SelectItem key={exam} value={exam}>
206+
{exam}
207+
</SelectItem>
208+
))}
209+
</SelectContent>
210+
</Select>
211+
<Select
212+
onValueChange={setSelectedSlot}
213+
disabled={!selectedSubject}
214+
value={selectedSlot ?? undefined}
215+
>
216+
<SelectTrigger className="w-32">
217+
<SelectValue placeholder="Slot" />
218+
</SelectTrigger>
219+
<SelectContent>
220+
{slots.map((slot) => (
221+
<SelectItem key={slot} value={slot}>
222+
{slot}
223+
</SelectItem>
224+
))}
225+
</SelectContent>
226+
</Select>
227+
<Select
228+
onValueChange={setSelectedYear}
229+
disabled={!selectedSubject}
230+
value={selectedYear ?? undefined}
231+
>
232+
<SelectTrigger className="w-32">
233+
<SelectValue placeholder="Year" />
234+
</SelectTrigger>
235+
<SelectContent>
236+
{[...years]
237+
.sort((a, b) => Number(b) - Number(a))
238+
.map((year) => (
239+
<SelectItem key={year} value={year}>
240+
{year}
241+
</SelectItem>
242+
))}
243+
</SelectContent>
244+
</Select>
245+
</div>
246+
247+
<Button
248+
className="rounded-lg bg-[#4A55FF] px-8 py-3 text-base text-white hover:bg-[#3A44CC] dark:bg-[#9EA8FF] dark:text-black dark:hover:bg-[#7D86E5]"
249+
onClick={handleSubmit}
250+
>
251+
Submit
252+
</Button>
253+
</div>
254+
255+
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
256+
{isLoading ? (
257+
<SkeletonPaperCard length={4} />
258+
) : (
259+
displayPapers.map((paper, subIndex) => (
260+
<div key={subIndex} className="h-full">
261+
<UpcomingPaper subject={paper.subject} slots={paper.slots} />
262+
</div>
263+
))
264+
)}
265+
</div>
266+
</main>
267+
</div>
268+
);
5269
}

0 commit comments

Comments
 (0)