diff --git a/src/app/api/course-list/route.ts b/src/app/api/course-list/route.ts new file mode 100644 index 0000000..4b3e7d6 --- /dev/null +++ b/src/app/api/course-list/route.ts @@ -0,0 +1,20 @@ +import { NextResponse } from "next/server"; +import { connectToDatabase } from "@/lib/mongoose"; +import { Course } from "@/db/papers"; + +export const dynamic = "force-dynamic"; + +export async function GET() { + try { + await connectToDatabase(); + const courses = await Course.find().lean(); + + return NextResponse.json(courses, { status: 200 }); + } catch (error) { + console.error(error); + return NextResponse.json( + { message: "Failed to fetch courses", error }, + { status: 500 } + ); + } +} diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts index e52990e..ddfca3d 100644 --- a/src/app/api/upload/route.ts +++ b/src/app/api/upload/route.ts @@ -1,13 +1,11 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextResponse } from "next/server"; import { PDFDocument } from "pdf-lib"; -import {courses, slots, years} from "@/components/select_options" +import { slots, years } from "@/components/select_options"; import { connectToDatabase } from "@/lib/mongoose"; import cloudinary from "cloudinary"; -import { - - CloudinaryUploadResult, -} from "@/interface"; -import {PaperAdmin} from "@/db/papers"; +import { type ICourses, type CloudinaryUploadResult } from "@/interface"; +import { PaperAdmin } from "@/db/papers"; +import axios from "axios"; // TODO: REMOVE THUMBNAIL FROM admin-buffer DB cloudinary.v2.config({ cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME, @@ -29,14 +27,19 @@ export async function POST(req: Request) { const year = formData.get("year") as string; const exam = formData.get("exam") as string; const isPdf = formData.get("isPdf") === "true"; // Convert string to boolean - if(!(courses.includes(subject) && slots.includes(slot) && years.includes(year))) - { - return NextResponse.json( - { message: "Bad Request" }, - - { status: 400 }, - ); + + const { data } = await axios.get(`${process.env.SERVER_URL}/api/course-list`); + const courses = data.map((course: { name: string }) => course.name); + if ( + !( + courses.includes(subject) && + slots.includes(slot) && + years.includes(year) + ) + ) { + return NextResponse.json({ message: "Bad Request" }, { status: 400 }); } + await connectToDatabase(); let finalUrl: string | undefined = ""; let public_id_cloudinary: string | undefined = ""; @@ -53,9 +56,12 @@ export async function POST(req: Request) { if (!process.env.NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET) { return; } - + const mergedPdfBytes = await CreatePDF(files); - [public_id_cloudinary, finalUrl] = await uploadPDFFile(mergedPdfBytes, uploadPreset); + [public_id_cloudinary, finalUrl] = await uploadPDFFile( + mergedPdfBytes, + uploadPreset, + ); } catch (error) { return NextResponse.json( { error: "Failed to process PDF" }, @@ -63,8 +69,12 @@ export async function POST(req: Request) { ); } } else { - [public_id_cloudinary, finalUrl] = await uploadPDFFile(files[0]!, uploadPreset); + [public_id_cloudinary, finalUrl] = await uploadPDFFile( + files[0]!, + uploadPreset, + ); } + const thumbnailResponse = cloudinary.v2.image(finalUrl!, { format: "jpg", }); @@ -73,7 +83,6 @@ export async function POST(req: Request) { .replace("upload", "upload/w_400,h_400,c_fill") .replace(//g, ""); const paper = new PaperAdmin({ - public_id_cloudinary, finalUrl, thumbnailUrl, @@ -91,35 +100,39 @@ export async function POST(req: Request) { console.error(error); return NextResponse.json( { message: "Failed to upload papers", error }, - { status: 500 }, ); } } - async function uploadPDFFile(file: File | ArrayBuffer, uploadPreset: string) { let bytes; - if(file instanceof File) //for pdf - { + if (file instanceof File) { bytes = await file.arrayBuffer(); - } - else // for images that are pdf - { + } else { bytes = file; } - return uploadFile(bytes, uploadPreset, "application/pdf") + return uploadFile(bytes, uploadPreset, "application/pdf"); } - async function uploadFile(bytes: ArrayBuffer, uploadPreset: string, fileType: string) { + +async function uploadFile( + bytes: ArrayBuffer, + uploadPreset: string, + fileType: string, +) { try { const buffer = Buffer.from(bytes); const dataUrl = `data:${fileType};base64,${buffer.toString("base64")}`; - const uploadResult = await cloudinary.v2.uploader.unsigned_upload(dataUrl, uploadPreset) as CloudinaryUploadResult; - return [uploadResult.public_id, uploadResult.secure_url ]; + const uploadResult = (await cloudinary.v2.uploader.unsigned_upload( + dataUrl, + uploadPreset, + )) as CloudinaryUploadResult; + return [uploadResult.public_id, uploadResult.secure_url]; } catch (e) { - throw (e); + throw e; } } + async function CreatePDF(files: File[]) { const pdfDoc = await PDFDocument.create(); diff --git a/src/components/searchbarSubjectList.tsx b/src/components/searchbarSubjectList.tsx index 270ea8f..c3ec8ee 100644 --- a/src/components/searchbarSubjectList.tsx +++ b/src/components/searchbarSubjectList.tsx @@ -2,8 +2,9 @@ import { useState, useCallback, useRef, useEffect } from "react"; import { Search } from "lucide-react"; import debounce from "debounce"; +import axios from "axios"; import { Input } from "@/components/ui/input"; -import { courses } from "./select_options"; +import { type ICourses } from "@/interface"; function SearchbarSubjectList({ setSubject, @@ -16,10 +17,28 @@ function SearchbarSubjectList({ const [suggestions, setSuggestions] = useState([]); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); + const [courses, setCourses] = useState([]); const suggestionsRef = useRef(null); + const fetchCourses = async () => { + try { + setLoading(true); + const response = await axios.get("/api/course-list"); + const fetchedCourses = response.data.map((course: { name: string }) => course.name); + setCourses(fetchedCourses); + setLoading(false); + } catch (err) { + setError("Failed to fetch courses"); + setLoading(false); + } + }; + + useEffect(() => { + void fetchCourses(); + }, []); + const debouncedSearch = useCallback( - debounce(async (text: string) => { + debounce((text: string) => { if (text.length > 0) { setLoading(true); const escapedSearchText = text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); @@ -32,6 +51,7 @@ function SearchbarSubjectList({ if (filteredSubjects.length === 0) { setError("Subject not found"); setSuggestions([]); + setLoading(false); return; } setSuggestions(filteredSubjects); @@ -41,7 +61,7 @@ function SearchbarSubjectList({ setSuggestions([]); } }, 500), - [], + [courses] ); const handleSearchChange = (e: React.ChangeEvent) => { @@ -50,7 +70,7 @@ function SearchbarSubjectList({ if (text.length <= 0) { setSuggestions([]); } - void debouncedSearch(text); + debouncedSearch(text); }; const handleSelectSuggestion = (suggestion: string) => { diff --git a/src/db/papers.ts b/src/db/papers.ts index e8bff84..d51b919 100644 --- a/src/db/papers.ts +++ b/src/db/papers.ts @@ -1,5 +1,5 @@ import mongoose, { Schema, type Model } from "mongoose"; -import { type IPaper } from "@/interface"; +import { type IPaper, type ICourses } from "@/interface"; const paperSchema = new Schema({ public_id_cloudinary: { type: String, required: true }, @@ -12,10 +12,16 @@ const paperSchema = new Schema({ isSelected: { type: Boolean, default: false }, }); +const courseSchema = new Schema({ + name: { type: String, required: true }, +}); + paperSchema.index({ subject: 1 }); export const PaperAdmin: Model = mongoose.models.Admin ?? mongoose.model("Admin", paperSchema); +export const Course: Model = + mongoose.models.Course ?? mongoose.model("Course", courseSchema); const Paper: Model = mongoose.models.Paper ?? mongoose.model("Paper", paperSchema); diff --git a/src/interface.ts b/src/interface.ts index 26d9cde..1920694 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -73,6 +73,10 @@ export interface IPaper{ exam: "CAT-1" | "CAT-2" | "FAT"; isSelected: boolean; } + +export interface ICourses{ + name: string; +} export interface IAdminUpload{ formData: FormData; files: File[];