diff --git a/README.md b/README.md index fc57b13..50b5419 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@

Papers


+ >

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!

## 🌐 Deploy @@ -17,7 +18,6 @@ - MongoDB & Mongoose : Database and object data modeling (ODM) for Node.js. - Cloudinary : Media storage and optimization service. - Shadcn : Collection of pre-built components using Radix UI and Tailwind CSS. - ## 💡 Features: - Access a vast collection of past CAT and FAT papers diff --git a/ongoing-papers.ts b/ongoing-papers.ts deleted file mode 100644 index 78fa73f..0000000 --- a/ongoing-papers.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { type IUpcomingPaper } from "@/interface"; - -const papers: IUpcomingPaper[] = [ - { - _id: "6708fd8002a75017a4f08759", - subject: "Discrete Mathematics and Graph Theory [BMAT205L]", - slots: ["A2", "B2"], - }, - { - _id: "670980523ec3fdad83b2d211", - subject: "Compiler Design [BCSE307L]", - slots: ["C1", "D1"], - }, - { - _id: "670a105e6272bcf9da4e2362", - - final_url: - "https://res.cloudinary.com/dtorpaj1c/image/upload/v1728712773/papers/ifkrnjgwdudtev9rxpki.pdf", - thumbnail_url: - "https://res.cloudinary.com/dtorpaj1c/image/upload/w_400,h_400,c_fill/v1728712773/papers/ifkrnjgwdudtev9rxpki.jpg", - subject: "Complex Variables and Linear Algebra [BMAT201L]", - slots: ["A1", "B1"], - year: "2023", - exam: "CAT-2", - semester: "Fall Semester", - campus: "Vellore", - }, - { - _id: "67097e7b3ec3fdad83b2d205", - final_url: - "https://res.cloudinary.com/dtorpaj1c/image/upload/v1728675439/papers/pyyqotz1mzuh2wmq1k9s.pdf", - thumbnail_url: - "https://res.cloudinary.com/dtorpaj1c/image/upload/w_400,h_400,c_fill/v1728675439/papers/pyyqotz1mzuh2wmq1k9s.jpg", - subject: "Computer Networks [BCSE308L]", - slots: ["C2", "D2"], - year: "2023", - exam: "CAT-2", - semester: "Fall Semester", - campus: "Vellore", - }, -]; - -export default papers; diff --git a/src/app/api/related-subject/route.ts b/src/app/api/related-subject/route.ts new file mode 100644 index 0000000..1640f9e --- /dev/null +++ b/src/app/api/related-subject/route.ts @@ -0,0 +1,41 @@ +import { NextResponse, type NextRequest } from "next/server"; +import { connectToDatabase } from "@/lib/mongoose"; +import { IRelatedSubject } from "@/interface"; +import RelatedSubject from "@/db/relatedSubjects"; + +export const dynamic = "force-dynamic"; + +export async function GET(req: NextRequest) { + try { + await connectToDatabase(); + const url = req.nextUrl.searchParams; + const subject = url.get("subject"); + const escapeRegExp = (text: string) => { + return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + }; + const escapedSubject = escapeRegExp(subject ?? ""); + + if (!subject) { + return NextResponse.json( + { message: "Subject query parameter is required" }, + { status: 400 }, + ); + } + const subjects: IRelatedSubject[] = await RelatedSubject.find({ + subject: { $regex: new RegExp(`${escapedSubject}`, "i") }, + }); + console.log("realted", subjects); + + return NextResponse.json( + { + related_subjects: subjects[0]?.related_subjects + }, + { status: 200 }, + ); + } catch (error) { + return NextResponse.json( + { message: "Failed to fetch related subject", error }, + { status: 500 }, + ); + } +} diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts index aee166c..571bbb9 100644 --- a/src/app/api/upload/route.ts +++ b/src/app/api/upload/route.ts @@ -181,5 +181,7 @@ async function CreatePDF(orderedFiles: File[]) { } const mergedPdfBytes = await pdfDoc.save(); - return mergedPdfBytes; + const ab = new ArrayBuffer(mergedPdfBytes.byteLength); + new Uint8Array(ab).set(mergedPdfBytes); + return ab; } diff --git a/src/app/api/user-papers/route.ts b/src/app/api/user-papers/route.ts index cc92e18..9f1ba6b 100644 --- a/src/app/api/user-papers/route.ts +++ b/src/app/api/user-papers/route.ts @@ -1,14 +1,10 @@ import { NextResponse } from "next/server"; import { connectToDatabase } from "@/lib/mongoose"; import Paper from "@/db/papers"; -import { StoredSubjects } from "@/interface"; +import { StoredSubjects, TransformedPaper } from "@/interface"; export const dynamic = "force-dynamic"; -interface TransformedPaper { - subject: string; - slots: string[]; -} export async function POST(req: Request) { try { diff --git a/src/app/request/page.tsx b/src/app/request/page.tsx index 0efe95d..f215103 100644 --- a/src/app/request/page.tsx +++ b/src/app/request/page.tsx @@ -1,5 +1,269 @@ -import PapersPage from "@/components/screens/PapersPage"; +"use client"; -export default function RequestPage() { - return ; +import { useEffect, useState, useRef, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { exams, slots, years } from "@/components/select_options"; +import { Input } from "@/components/ui/input"; +import axios from "axios"; +import Fuse from "fuse.js"; +import { type IUpcomingPaper } from "@/interface"; +import { Skeleton } from "../../components/ui/skeleton"; +import UpcomingPaper from "../../components/UpcomingPaper"; +import toast from "react-hot-toast"; +import { Search } from "lucide-react"; +import SkeletonPaperCard from "@/components/SkeletonPaperCard"; + +type Course = { + name?: string | null; + courseName?: string | null; + title?: string | null; +}; + +export default function PaperRequest() { + const [subjects, setSubjects] = useState([]); + const [searchText, setSearchText] = useState(""); + const [suggestions, setSuggestions] = useState([]); + const [selectedSubject, setSelectedSubject] = useState(null); + const [selectedExam, setSelectedExam] = useState(null); + const [selectedSlot, setSelectedSlot] = useState(null); + const [selectedYear, setSelectedYear] = useState(null); + const suggestionsRef = useRef(null); + const [displayPapers, setDisplayPapers] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + async function fetchSubjects() { + try { + const response = await axios.get(`/api/course-list`); + const courses: Course[] = response.data; + const names = courses + .map((course) => course.name ?? course.courseName ?? course.title) + .filter(Boolean) as string[]; + + setSubjects(names); + } catch (err) { + console.error("Error fetching subjects:", err); + } + } + void fetchSubjects(); + }, []); + + useEffect(() => { + async function fetchPapers() { + try { + setIsLoading(true); + const response = await axios.get( + "/api/upcoming-papers", + ); + + const randomPapers = [...response.data] + .sort(() => Math.random() - 0.5) + .slice(0, 4); + + setDisplayPapers(randomPapers); + } catch (error) { + console.error("Failed to fetch papers:", error); + } finally { + setIsLoading(false); + } + } + + void fetchPapers(); + }, []); + + const fuse = useMemo( + () => new Fuse(subjects, { includeScore: true, threshold: 0.3 }), + [subjects], + ); + + useEffect(() => { + if (!searchText.trim()) { + setSuggestions([]); + return; + } + + if (selectedSubject && searchText === selectedSubject) { + setSuggestions([]); + return; + } + + const results = fuse.search(searchText); + setSuggestions(results.map((r) => r.item).slice(0, 10)); + }, [searchText, fuse, selectedSubject]); + + const handleSelectSubject = (subject: string) => { + setSelectedSubject(subject); + setSearchText(subject); + setSuggestions([]); + setSelectedExam(null); + setSelectedSlot(null); + setSelectedYear(null); + }; + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if ( + suggestionsRef.current && + !suggestionsRef.current.contains(event.target as Node) + ) { + setSuggestions([]); + } + } + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const handleSubmit = async () => { + if (!selectedSubject || !selectedExam || !selectedSlot || !selectedYear) { + toast.error("Please fill all fields before submitting."); + return; + } + + try { + await toast.promise( + axios.post("/api/request", { + subject: selectedSubject, + exam: selectedExam, + slot: selectedSlot, + year: selectedYear, + }), + { + loading: "Submitting your request...", + success: "Your paper request was submitted successfully", + error: "Failed to submit your request. Please try again later.", + } + ); + + setSearchText(""); + setSelectedSubject(null); + setSelectedExam(null); + setSelectedSlot(null); + setSelectedYear(null); + } catch (error) { + console.error("Error submitting request:", error); + } + }; + + return ( +
+
+
+

+ Specific Paper Request +

+ +
+ setSearchText(e.target.value)} + placeholder="Search by subject..." + 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" : ""}`} + /> + + {suggestions.length > 0 && ( +
    + {suggestions.map((s, idx) => ( +
  • handleSelectSubject(s)} + className="cursor-pointer truncate p-2 hover:bg-gray-100 dark:hover:bg-gray-800" + > + {s} +
  • + ))} +
+ )} +
+ +
+ + + +
+ + +
+ +
+ {isLoading ? ( + + ) : ( + displayPapers.map((paper, subIndex) => ( +
+ +
+ )) + )} +
+
+
+ ); } diff --git a/src/app/upload/page.tsx b/src/app/upload/page.tsx index 5d88ddb..7ccfae6 100644 --- a/src/app/upload/page.tsx +++ b/src/app/upload/page.tsx @@ -140,9 +140,7 @@ const Page = () => { await toast.promise( async () => { try { - console.log("this is happening now"); await axios.post("/api/upload", formData); - console.log("this is happening after now"); return { message: "Papers uploaded successfully!" }; } catch (error) { if (error instanceof AxiosError && error.response?.data) { diff --git a/src/components/CatalogueContent.tsx b/src/components/CatalogueContent.tsx index b548b37..8891ca7 100644 --- a/src/components/CatalogueContent.tsx +++ b/src/components/CatalogueContent.tsx @@ -4,7 +4,7 @@ import { useSearchParams } from "next/navigation"; import { useCallback, useEffect, useState } from "react"; import axios, { type AxiosError } from "axios"; import { Button } from "@/components/ui/button"; -import { type IPaper, type Filters } from "@/interface"; +import { type IPaper, type Filters, IRelatedSubject, StoredSubjects } from "@/interface"; import Card from "./Card"; import { useRouter } from "next/navigation"; import Loader from "./ui/loader"; @@ -13,8 +13,8 @@ import Error from "./Error"; import { Filter } from "lucide-react"; import { Sheet, SheetContent, SheetTrigger } from "./ui/sheet"; import { Pin } from "lucide-react"; -import { StoredSubjects } from "@/interface"; import { getSecureUrl, generateFileName, downloadFile } from "@/util/download"; +import Link from "next/link"; const CatalogueContent = () => { const router = useRouter(); @@ -39,6 +39,29 @@ const CatalogueContent = () => { const [filtersPulled, setFiltersPulled] = useState(false); const [appliedFilters, setAppliedFilters] = useState(false); const [pinned, setPinned] = useState(false); + const [relatedSubjects, setRelatedSubjects] = useState([]); + // Fetch related subjects when subject changes + useEffect(() => { + if (!subject) return; + const fetchRelatedSubjects = async () => { + try { + const res = await axios.get<{related_subjects: string []}>("/api/related-subject", { + params: { subject }, + }); + console.log(res.data) + const data = res.data.related_subjects; + console.log("data" , data[0], data[1]); + if (data && data.length > 0) { + setRelatedSubjects(data); + } else { + setRelatedSubjects([]); + } + } catch (e) { + setRelatedSubjects([]); + } + }; + void fetchRelatedSubjects(); + }, [subject]); // Set initial state from searchParams on client-side mount useEffect(() => { @@ -317,22 +340,38 @@ const CatalogueContent = () => { -
-
-

- {subject?.split("[")[1]?.replace("]", "")} -

-

- {subject?.split(" [")[0]} -

-
-
- +
+
+
+

+ {subject?.split("[")[1]?.replace("]", "")} +

+

+ {subject?.split(" [")[0]} +

+
+
+ +
+ {relatedSubjects.length > 0 && ( +
+ Related subjects: + {relatedSubjects.map((sub) => ( + + {sub} + + ))} +
+ )}
{loading ? ( diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 5bc0c5a..587ad11 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -96,17 +96,17 @@ export default function Footer() { {/* Events */}

Events

- DevSoc - CookOff - Clueminati + DevSoc + CookOff + Clueminati
{/* Projects */}

Our Projects

- Papers - Contactify - FFCS-inator + Papers + Contactify + FFCS-inator
{/* Suggestions */} diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 22a54c7..6ce6598 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -40,14 +40,14 @@ function Navbar() {
- + codechef-logo - + -
+
Pinned Subjects
-
+
Paper Request
@@ -86,7 +86,7 @@ function Navbar() {
-
+
{pathname === "/upload" ? "Search Papers" : "Upload Papers"} diff --git a/src/components/PapersCarousel.tsx b/src/components/PapersCarousel.tsx index 38a73e5..1c2a4ad 100644 --- a/src/components/PapersCarousel.tsx +++ b/src/components/PapersCarousel.tsx @@ -14,6 +14,7 @@ import { import Autoplay from "embla-carousel-autoplay"; import { chunkArray } from "@/util/utils"; import { Skeleton } from "@/components/ui/skeleton"; +import SkeletonPaperCard from "@/components/SkeletonPaperCard"; function PapersCarousel() { const [displayPapers, setDisplayPapers] = useState([]); @@ -78,23 +79,7 @@ function PapersCarousel() { chunkSize === 4 ? "grid-cols-2 grid-rows-2" : "grid-cols-4" } gap-4 lg:auto-rows-fr`} > - {Array.from({ length: chunkSize }).map((_, idx) => ( -
-
- -
-
- -
- - -
-
-
- ))} + ) : ( chunkedPapers.map((paperGroup, index) => { diff --git a/src/components/PinnedPapersCarousel.tsx b/src/components/PinnedPapersCarousel.tsx index 449cd41..bd475bd 100644 --- a/src/components/PinnedPapersCarousel.tsx +++ b/src/components/PinnedPapersCarousel.tsx @@ -17,6 +17,7 @@ import AddPapers from "./AddPapers"; import Autoplay from "embla-carousel-autoplay"; import { chunkArray } from "@/util/utils"; import { StoredSubjects } from "@/interface"; +import SkeletonPaperCard from "./SkeletonPaperCard"; function PinnedPapersCarousel({ carouselType = "upcoming", @@ -54,15 +55,11 @@ function PinnedPapersCarousel({ localStorage.getItem("userSubjects") ?? "[]", ) as StoredSubjects; - console.log("Fetching papers for subjects:", storedSubjects); - const response = await axios.post<{ subject: string; slots: string[] }[]>( "/api/user-papers", storedSubjects, ); - console.log("Fetched papers:", response.data); - const fetchedPapers = response.data; const fetchedSubjectsSet = new Set( @@ -131,23 +128,7 @@ function PinnedPapersCarousel({ chunkSize === 4 ? "grid-cols-2 grid-rows-2" : "grid-cols-4" } gap-4 lg:auto-rows-fr`} > - {Array.from({ length: chunkSize }).map((_, idx) => ( -
-
- -
-
- -
- - -
-
-
- ))} + ) : ( chunkedPapers.map((paperGroup, index) => { diff --git a/src/components/Searchbar/searchbar-child.tsx b/src/components/Searchbar/searchbar-child.tsx index 8d0ed36..b3a83f2 100644 --- a/src/components/Searchbar/searchbar-child.tsx +++ b/src/components/Searchbar/searchbar-child.tsx @@ -145,7 +145,7 @@ function SearchBarChild({
{suggestion} diff --git a/src/components/SkeletonPaperCard.tsx b/src/components/SkeletonPaperCard.tsx new file mode 100644 index 0000000..bcc50fc --- /dev/null +++ b/src/components/SkeletonPaperCard.tsx @@ -0,0 +1,29 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +interface SkeletonPaperCardProps { + length?: number; +} + +export default function SkeletonPaperCard({ length = 4 }: SkeletonPaperCardProps) { + return ( + <> + {Array.from({ length }).map((_, idx) => ( +
+
+ +
+
+ +
+ + +
+
+
+ ))} + + ); +} diff --git a/src/components/screens/PapersPage.tsx b/src/components/screens/PapersPage.tsx deleted file mode 100644 index 60702e4..0000000 --- a/src/components/screens/PapersPage.tsx +++ /dev/null @@ -1,296 +0,0 @@ -"use client"; - -import { useEffect, useState, useRef, useMemo } from "react"; -import { Button } from "@/components/ui/button"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { exams, slots, years } from "@/components/select_options"; -import { Input } from "@/components/ui/input"; -import axios from "axios"; -import Fuse from "fuse.js"; -import { ArrowRight } from "lucide-react"; -import Image from "next/image"; -import { IUpcomingPaper } from "@/interface"; -import { Skeleton } from "../ui/skeleton"; -import { CarouselItem } from "@/components/ui/carousel"; -import UpcomingPaper from "../UpcomingPaper"; - -type Course = { - name?: string | null; - courseName?: string | null; - title?: string | null; -}; - -export default function PapersPage() { - const [subjects, setSubjects] = useState([]); - const [searchText, setSearchText] = useState(""); - const [suggestions, setSuggestions] = useState([]); - const [selectedSubject, setSelectedSubject] = useState(null); - const [selectedExam, setSelectedExam] = useState(null); - const [selectedSlot, setSelectedSlot] = useState(null); - const [selectedYear, setSelectedYear] = useState(null); - const suggestionsRef = useRef(null); - const [displayPapers, setDisplayPapers] = useState([]); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - async function fetchSubjects() { - try { - const response = await axios.get(`/api/course-list`); - const courses: Course[] = response.data; - const names = courses - .map((course) => course.name ?? course.courseName ?? course.title) - .filter(Boolean) as string[]; - - setSubjects(names); - } catch (err) { - console.error("Error fetching subjects:", err); - } - } - void fetchSubjects(); - }, []); - - useEffect(() => { - async function fetchPapers() { - try { - setIsLoading(true); - const response = await axios.get( - "/api/upcoming-papers", - ); - - const randomPapers = [...response.data] - .sort(() => Math.random() - 0.5) - .slice(0, 4); - - setDisplayPapers(randomPapers); - } catch (error) { - console.error("Failed to fetch papers:", error); - } finally { - setIsLoading(false); - } - } - - void fetchPapers(); - }, []); - - const fuse = useMemo( - () => new Fuse(subjects, { includeScore: true, threshold: 0.3 }), - [subjects], - ); - - useEffect(() => { - if (!searchText.trim()) { - setSuggestions([]); - return; - } - - if (selectedSubject && searchText === selectedSubject) { - setSuggestions([]); - return; - } - - const results = fuse.search(searchText); - setSuggestions(results.map((r) => r.item).slice(0, 10)); - }, [searchText, fuse, selectedSubject]); - - const handleSelectSubject = (subject: string) => { - setSelectedSubject(subject); - setSearchText(subject); - setSuggestions([]); - setSelectedExam(null); - setSelectedSlot(null); - setSelectedYear(null); - }; - - useEffect(() => { - function handleClickOutside(event: MouseEvent) { - if ( - suggestionsRef.current && - !suggestionsRef.current.contains(event.target as Node) - ) { - setSuggestions([]); - } - } - document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, []); - - const handleSubmit = async () => { - if (!selectedSubject || !selectedExam || !selectedSlot || !selectedYear) { - alert("⚠️ Please fill all fields before submitting."); - return; - } - - try { - await axios.post("/api/request", { - subject: selectedSubject, - exam: selectedExam, - slot: selectedSlot, - year: selectedYear, - }); - - alert("✅ Your paper request was submitted successfully 🎉"); - - setSearchText(""); - setSelectedSubject(null); - setSelectedExam(null); - setSelectedSlot(null); - setSelectedYear(null); - } catch (error) { - console.error("Error submitting request:", error); - alert("❌ Failed to submit your request. Please try again later."); - } - }; - - return ( -
-
-
-

- Specific Paper Request -

- -
- setSearchText(e.target.value)} - placeholder="Search by subject..." - 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" : ""}`} - /> - - {suggestions.length > 0 && ( -
    - {suggestions.map((s, idx) => ( -
  • handleSelectSubject(s)} - className="cursor-pointer truncate p-2 hover:bg-gray-100 dark:hover:bg-gray-800" - > - {s} -
  • - ))} -
- )} -
- -
- - - -
- - -
- -
- {isLoading - ? Array.from({ length: 4 }).map((_, idx) => ( -
- {/* Top section */} -
- - -
- - {/* Middle section */} -
- -
- - -
-
-
- )) - : displayPapers.map((paper, subIndex) => ( -
- -
- ))} -
-
-
- ); -} diff --git a/src/db/relatedSubjects.ts b/src/db/relatedSubjects.ts new file mode 100644 index 0000000..c9a4c27 --- /dev/null +++ b/src/db/relatedSubjects.ts @@ -0,0 +1,14 @@ +import mongoose, { Schema, type Model } from "mongoose"; +import { type IRelatedSubject } from "@/interface"; + + +const relatedSubjectSchema = new Schema({ + subject: { type: String, required: true }, + related_subjects: { type: [String], required: true }, +}); + +const RelatedSubject: Model = + mongoose.models.RelatedSubject ?? + mongoose.model("RelatedSubject", relatedSubjectSchema); + +export default RelatedSubject; diff --git a/src/interface.ts b/src/interface.ts index 74bcfca..c8362da 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -204,3 +204,14 @@ export interface Filters { export interface StoredSubjects { subjects: string[]; } + +export interface TransformedPaper { + subject: string; + slots: string[]; +} + + +export interface IRelatedSubject { + subject: string; + related_subjects: string[]; +} \ No newline at end of file diff --git a/src/lib/mongoose.ts b/src/lib/mongoose.ts index 7f129fe..7be32c3 100644 --- a/src/lib/mongoose.ts +++ b/src/lib/mongoose.ts @@ -1,27 +1,41 @@ import mongoose from "mongoose"; - -if (!process.env.MONGODB_URI) { - throw new Error("Please add your Mongo URI to .env.local"); +declare global { + // eslint-disable-next-line no-var + var mongoose: { conn: mongoose.Mongoose | null; promise: Promise | null } | undefined; // This must be a `var` and not a `let / const` } -const uri = process.env.MONGODB_URI; +let cached = global.mongoose; -let isConnected = false; +cached ??= global.mongoose = { conn: null, promise: null }; -export const connectToDatabase = async () => { - if (isConnected) { - return; - } +export async function connectToDatabase() { + const MONGODB_URI = process.env.MONGODB_URI!; - if (mongoose.connection.readyState === mongoose.ConnectionStates.connected) { - isConnected = true; - return; + if (!MONGODB_URI) { + throw new Error( + "Please define the MONGODB_URI environment variable inside .env.local", + ); } + if (cached?.conn) { + return cached.conn; + } + if (cached && !cached.promise) { + const opts = { + bufferCommands: false, + }; + cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => { + return mongoose; + }); + } try { - await mongoose.connect(uri); - isConnected = true; - } catch (error) { - throw new Error("Failed to connect to MongoDB"); + cached!.conn = await cached!.promise; + } catch (e) { + if (cached) { + cached.promise = null; + } + throw e; } -}; + + return cached?.conn; +} diff --git a/src/util/download_paper.tsx b/src/util/download.tsx similarity index 100% rename from src/util/download_paper.tsx rename to src/util/download.tsx