diff --git a/src/app/actions/get-papers-by-id.ts b/src/app/actions/get-papers-by-id.ts index 3f30051f..6909aa6d 100644 --- a/src/app/actions/get-papers-by-id.ts +++ b/src/app/actions/get-papers-by-id.ts @@ -1,20 +1,22 @@ -import { type PaperResponse } from "@/interface"; +import type { PaperResponse, ApiResponse } from "@/interface"; import axios, { type AxiosResponse } from "axios"; export const fetchPaperID = async (id: string): Promise => { const serverUrl = process.env.SERVER_URL ?? "https://papers.codechefvit.com"; try { - const response: AxiosResponse = await axios.get( + const response: AxiosResponse> = await axios.get( `${serverUrl}/api/paper-by-id/${id}`, ); - return response.data; + + if (!response.data.data) { + throw new Error("Paper not found"); + } + return response.data.data; } catch (err: unknown) { - if (axios.isAxiosError(err)) { - console.error("Axios error:", err.response?.data ?? err.message); - const errorMessage = - (err.response?.data as { message?: string })?.message ?? - "Failed to fetch paper"; + if (axios.isAxiosError>(err)) { + const errorMessage = err.response?.data?.message ?? err.message; + console.error("Axios error:", errorMessage); throw new Error(errorMessage); } else { console.error("Unexpected error:", err); diff --git a/src/app/api/course-list/route.ts b/src/app/api/course-list/route.ts index 47bf19d7..f84f51f6 100644 --- a/src/app/api/course-list/route.ts +++ b/src/app/api/course-list/route.ts @@ -1,17 +1,14 @@ -import { NextResponse } from "next/server"; import { getCourseList } from "@/lib/services/subject"; +import { success, failure } from "@/lib/utils/response"; export const dynamic = "force-dynamic"; export async function GET() { try { const courses = await getCourseList(); - return NextResponse.json(courses, { status: 200 }); + return success(courses); } catch (error) { console.error(error); - return NextResponse.json( - { message: "Failed to fetch courses", error }, - { status: 500 }, - ); + return failure("Failed to fetch courses", 500); } } diff --git a/src/app/api/paper-by-id/[id]/route.ts b/src/app/api/paper-by-id/[id]/route.ts index 5a69c287..28b88c4f 100644 --- a/src/app/api/paper-by-id/[id]/route.ts +++ b/src/app/api/paper-by-id/[id]/route.ts @@ -1,23 +1,21 @@ -import { NextResponse } from "next/server"; import { Types } from "mongoose"; import { getPaperById } from "@/lib/services/paper"; +import { success, failure } from "@/lib/utils/response" + +export const dynamic = "force-dynamic"; export async function GET(req: Request, { params }: { params: { id: string } }) { try { const { id } = params; if (!Types.ObjectId.isValid(id)) { - return NextResponse.json({ message: "Invalid paper ID" }, { status: 400 }); + return failure("Invalid paper ID"); } - const paper = await getPaperById(id); - - return NextResponse.json(paper, { status: 200 }); + + return success(paper); } catch (error) { console.error(error); - return NextResponse.json( - { message: "Failed to fetch paper", error }, - { status: 500 }, - ); + return failure("Failed to fetch paper", 500); } } diff --git a/src/app/api/papers/count/route.ts b/src/app/api/papers/count/route.ts index cc2fbf1e..0bb50a57 100644 --- a/src/app/api/papers/count/route.ts +++ b/src/app/api/papers/count/route.ts @@ -1,17 +1,15 @@ -import { NextResponse } from "next/server"; import { getCourseCounts } from "@/lib/services/paper"; +import { success, failure } from "@/lib/utils/response"; export const dynamic = "force-dynamic"; -export async function GET(req: Request) { +export async function GET() { try { const courseCount = await getCourseCounts(); - return NextResponse.json(courseCount, { status: 200 }); + return success(courseCount); } catch (error) { - return NextResponse.json( - { message: "Failed to fetch course counts", error }, - { status: 500 }, - ); + console.error(error); + return failure("Failed to fetch course counts", 500); } } diff --git a/src/app/api/papers/route.ts b/src/app/api/papers/route.ts index b7840ddb..a5eb23cb 100644 --- a/src/app/api/papers/route.ts +++ b/src/app/api/papers/route.ts @@ -1,5 +1,6 @@ -import { NextResponse, type NextRequest } from "next/server"; +import { type NextRequest } from "next/server"; import { getPapersBySubject } from "@/lib/services/paper"; +import { success, failure } from "@/lib/utils/response"; export const dynamic = "force-dynamic"; @@ -8,18 +9,13 @@ export async function GET(req: NextRequest) { const url = req.nextUrl.searchParams; const sub = url.get("subject"); if (!sub) { - return NextResponse.json( - { message: "Subject query parameter is required" }, - { status: 400 }, - ); + return failure("Subject query parameter is required"); } const paper = await getPapersBySubject(sub); - return NextResponse.json(paper, { status: 200 }); + return success(paper); } catch (error) { - return NextResponse.json( - { message: "Failed to fetch papers", error }, - { status: 500 }, - ); + console.error(error); + return failure("Failed to fetch papers", 500); } } diff --git a/src/app/api/related-subject/route.ts b/src/app/api/related-subject/route.ts index 6f374ff6..debd00dc 100644 --- a/src/app/api/related-subject/route.ts +++ b/src/app/api/related-subject/route.ts @@ -1,5 +1,6 @@ import { getRelatedSubjects } from "@/lib/services/subject"; -import { NextResponse, type NextRequest } from "next/server"; +import { type NextRequest } from "next/server"; +import { success, failure } from "@/lib/utils/response"; export const dynamic = "force-dynamic"; @@ -9,21 +10,13 @@ export async function GET(req: NextRequest) { const subject = url.get("subject"); if (!subject) { - return NextResponse.json( - { message: "Subject query parameter is required" }, - { status: 400 }, - ); + return failure("Subject query parameter is required", 400); } const relatedSubjects = await getRelatedSubjects(subject); - return NextResponse.json( - {related_subjects: relatedSubjects}, - { status: 200 }, - ); + return success({ related_subjects: relatedSubjects }); } catch (error) { - return NextResponse.json( - { message: "Failed to fetch related subject", error }, - { status: 500 }, - ); + console.error(error); + return failure("Failed to fetch related subject", 500); } } \ No newline at end of file diff --git a/src/app/api/report-tag/route.ts b/src/app/api/report-tag/route.ts index 032cbd15..23633e7a 100644 --- a/src/app/api/report-tag/route.ts +++ b/src/app/api/report-tag/route.ts @@ -1,6 +1,6 @@ -import { NextResponse } from "next/server"; -import { reportTag, ReportTagBody } from "@/lib/services/report"; +import { reportTag, type ReportTagBody } from "@/lib/services/report"; import { rateLimitCheck } from "@/lib/utils/rate-limiter"; +import { success, failure } from "@/lib/utils/response"; import { customErrorHandler } from "@/lib/utils/error"; export async function POST(req: Request & { ip?: string }) { @@ -9,18 +9,12 @@ export async function POST(req: Request & { ip?: string }) { const paperId = typeof body.paperId === "string" ? body.paperId : undefined; if (!paperId) { - return NextResponse.json( - { error: "paperId is required" }, - { status: 400 } - ); + return failure("paperId is required", 400); } await rateLimitCheck(req, paperId); const newReport = await reportTag(paperId, body); - return NextResponse.json( - { message: "Report submitted.", report: newReport }, - { status: 201 } - ); + return success({ message: "Report submitted.", report: newReport }, "Created", 201); } catch (err) { console.error(err); return customErrorHandler(err, "Failed to submit tag report."); diff --git a/src/app/api/request/route.ts b/src/app/api/request/route.ts index 48c93022..a0fbf2c6 100644 --- a/src/app/api/request/route.ts +++ b/src/app/api/request/route.ts @@ -1,36 +1,19 @@ -import { NextResponse } from "next/server"; -import { connectToDatabase } from "@/lib/database/mongoose"; -import PaperRequest from "@/db/paperRequest"; +import { success, failure } from "@/lib/utils/response"; +import { createPaperRequest } from "@/lib/services/paper" +import type { CreatePaperInputType } from "@/lib/services/paper" export async function POST(req: Request) { try { - await connectToDatabase(); - const body = (await req.json()) as { - subject: string; - exam: string; - slot: string; - year: string; - }; - - const { subject, exam, slot, year } = body; + const {subject, exam, slot, year} = (await req.json()) as CreatePaperInputType if (!subject || !exam || !slot || !year) { - return NextResponse.json( - { error: "All fields are required." }, - { status: 400 }, - ); + return failure("All fields are required.", 400); } - const newRequest = await PaperRequest.create({ subject, exam, slot, year }); - return NextResponse.json( - { message: "Paper request submitted successfully!", request: newRequest }, - { status: 201 }, - ); + const newRequest = await createPaperRequest({subject, exam, slot, year}); + return success({ message: "Paper request submitted successfully!", request: newRequest }, "Created", 201); } catch (error) { console.error("Error creating paper request:", error); - return NextResponse.json( - { error: "Failed to submit request." }, - { status: 500 }, - ); + return failure("Failed to submit request.", 500); } } diff --git a/src/app/api/selected-papers/route.ts b/src/app/api/selected-papers/route.ts index 5c822f6b..0a5a7a11 100644 --- a/src/app/api/selected-papers/route.ts +++ b/src/app/api/selected-papers/route.ts @@ -1,33 +1,18 @@ -import { NextResponse } from "next/server"; -import { connectToDatabase } from "@/lib/database/mongoose"; -import Paper from "@/db/papers"; +import { success, failure } from "@/lib/utils/response"; +import { getSelectedPapers } from "@/lib/services/paper"; export const dynamic = "force-dynamic"; export async function GET() { try { - await connectToDatabase(); - - const selectedPapers = await Paper.find({ isSelected: true }).limit(8); + const selectedPapers = await getSelectedPapers(); if (selectedPapers.length === 0) { - return NextResponse.json( - { - message: "No selected papers found.", - }, - { status: 404 }, - ); + return failure("No selected papers found.", 404); } - return NextResponse.json(selectedPapers, { - status: 200, - }); + return success(selectedPapers); } catch (error) { console.error("Error fetching papers:", error); - return NextResponse.json( - { - error: "Failed to fetch papers.", - }, - { status: 500 }, - ); + return failure("Failed to fetch papers.", 500); } -} +} \ No newline at end of file diff --git a/src/app/api/subscribe/route.ts b/src/app/api/subscribe/route.ts index ce228186..791e7d31 100644 --- a/src/app/api/subscribe/route.ts +++ b/src/app/api/subscribe/route.ts @@ -1,19 +1,19 @@ -import { NextResponse } from "next/server"; -import { customErrorHandler } from "@/lib/utils/error"; import { subscribeEmail } from "@/lib/services/subscribe"; +import { success, failure } from "@/lib/utils/response"; +import { customErrorHandler } from "@/lib/utils/error"; export async function POST(req: Request) { try { const { email } = (await req.json()) as { email: string }; if (!email) { - return NextResponse.json({ error: "Email is required" }, { status: 400 }); + return failure("Email is required", 400); } await subscribeEmail(email); - return NextResponse.json({ message: "Email added successfully" }); + return success({ message: "Email added successfully" }); } catch (error) { console.error("Error adding email:", error); return customErrorHandler(error, "Failed to add email"); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/app/api/upcoming-papers/route.ts b/src/app/api/upcoming-papers/route.ts index 9c764280..96e81406 100644 --- a/src/app/api/upcoming-papers/route.ts +++ b/src/app/api/upcoming-papers/route.ts @@ -1,36 +1,19 @@ -import { NextResponse } from "next/server"; -import { connectToDatabase } from "@/lib/database/mongoose"; -import UpcomingSubject from "@/db/upcoming-paper"; +import { success, failure } from "@/lib/utils/response"; +import { getUpcomingSubjects } from "@/lib/services/subject"; export const dynamic = "force-dynamic"; export async function GET() { try { - await connectToDatabase(); - const selectedSubjects = await UpcomingSubject.find() - .sort({ _id: 1 }) - .limit(16) - .lean(); - + const selectedSubjects = await getUpcomingSubjects(); + if (selectedSubjects.length === 0) { - return NextResponse.json( - { - message: "No selected papers found.", - }, - { status: 404 }, - ); + return failure("No selected papers found.", 404); } - return NextResponse.json(selectedSubjects, { - status: 200, - }); + return success(selectedSubjects); } catch (error) { console.error("Error fetching papers:", error); - return NextResponse.json( - { - error: "Failed to fetch papers.", - }, - { status: 500 }, - ); + return failure("Failed to fetch papers.", 500); } -} +} \ No newline at end of file diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts index 2fad98e7..471b5cb0 100644 --- a/src/app/api/upload/route.ts +++ b/src/app/api/upload/route.ts @@ -1,71 +1,33 @@ -import { NextResponse } from "next/server"; -import { connectToDatabase } from "@/lib/database/mongoose"; -import { PaperAdmin } from "@/db/papers"; -import { createPDFfromImages } from "@/lib/storage/pdf"; -import { uploadPDF, uploadThumbnail } from "@/lib/storage/gcp"; +import { success, failure } from "@/lib/utils/response"; +import { uploadPaper } from "@/lib/services/upload"; export const runtime = "nodejs"; export async function POST(req: Request) { try { - await connectToDatabase(); const formData = await req.formData(); const files = formData.getAll("files").filter(Boolean) as File[]; const isPdf = formData.get("isPdf") === "true"; - const thumb = formData.get("thumbnail") as File | null; + const thumbnail = formData.get("thumbnail") as File | null; + const campus = formData.get("campus") as string | null; if (files.length === 0) { - return NextResponse.json( - { error: "No files received." }, - { status: 400 }, - ); + return failure("No files received.", 400); } - let pdfBytes: Uint8Array; - if (isPdf) { - if (!files[0]) { - return NextResponse.json( - { error: "No PDF file provided." }, - { status: 400 }, - ); - } - pdfBytes = new Uint8Array(await files[0].arrayBuffer()); - } else { - pdfBytes = await createPDFfromImages(files); - } - - const buffer = Buffer.from(pdfBytes); - - const file_url = await uploadPDF("unapproved", buffer); + const result = await uploadPaper({ files, isPdf, thumbnail, campus }); - let thumbnail_url: string | null = null; - if (thumb) { - const thumbBuffer = Buffer.from(await thumb.arrayBuffer()); - thumbnail_url = await uploadThumbnail(thumbBuffer, file_url); + if (!result.success) { + return failure(result.message, result.status); } - const paper = new PaperAdmin({ - file_url, - thumbnail_url, - campus: formData.get("campus"), - subject: null, - slot: null, - year: null, - exam: null, - semester: null, - ambiguous_tags: [], - }); - await paper.save(); - - return NextResponse.json( - { status: "success", file_url, thumbnail_url }, - { status: 201 }, + return success( + { file_url: result.file_url, thumbnail_url: result.thumbnail_url }, + "Created", + 201, ); } catch (error) { console.error(error); - return NextResponse.json( - { message: "Failed to upload papers", error }, - { status: 500 }, - ); + return failure("Failed to upload papers", 500); } -} +} \ No newline at end of file diff --git a/src/app/api/user-papers/route.ts b/src/app/api/user-papers/route.ts index 8d2b9dfb..8473ff56 100644 --- a/src/app/api/user-papers/route.ts +++ b/src/app/api/user-papers/route.ts @@ -1,34 +1,18 @@ -import { NextResponse } from "next/server"; -import { connectToDatabase } from "@/lib/database/mongoose"; -import Paper from "@/db/papers"; -import { StoredSubjects } from "@/interface"; -import { transformPapersToSubjectSlots } from "@/lib/services/paper-transform"; +import { type StoredSubjects } from "@/interface"; +import { getPapersBySubjects } from "@/lib/services/paper" +import { success, failure } from "@/lib/utils/response"; export const dynamic = "force-dynamic"; export async function POST(req: Request) { try { - await connectToDatabase(); const subjects = (await req.json()) as StoredSubjects; - const usersPapers = await Paper.find({ - subject: { $in: subjects }, - }); + const transformedPapers = await getPapersBySubjects(subjects) - console.log("Fetched user papers:", usersPapers); - - const transformedPapers = transformPapersToSubjectSlots(usersPapers); - - return NextResponse.json(transformedPapers, { - status: 200, - }); + return success(transformedPapers); } catch (error) { console.error("Error fetching papers:", error); - return NextResponse.json( - { - error: "Failed to fetch papers.", - }, - { status: 500 }, - ); + return failure("Failed to fetch papers.", 500); } } diff --git a/src/app/pinned/page.tsx b/src/app/pinned/page.tsx index 59d1eafd..7b0a8f92 100644 --- a/src/app/pinned/page.tsx +++ b/src/app/pinned/page.tsx @@ -1,6 +1,5 @@ import React from "react"; import SearchBar from "@/components/Searchbar/searchbar"; -import PinnedPapersCarousel from "@/components/PinnedPapersCarousel"; const Pinned = () => { return ( diff --git a/src/app/request/page.tsx b/src/app/request/page.tsx index 97367f6c..56ee4ee3 100644 --- a/src/app/request/page.tsx +++ b/src/app/request/page.tsx @@ -14,18 +14,12 @@ 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"; import { useCourses } from "@/context/courseContext"; - -type Course = { - name?: string | null; - courseName?: string | null; - title?: string | null; -}; +import { type ApiResponse } from '@/interface' export default function PaperRequest() { const [subjects, setSubjects] = useState([]); @@ -38,7 +32,7 @@ export default function PaperRequest() { const suggestionsRef = useRef(null); const [displayPapers, setDisplayPapers] = useState([]); const [isLoading, setIsLoading] = useState(true); - const { courses, loading, error, refetch } = useCourses(); + const { courses } = useCourses(); useEffect(() => { setSubjects(courses.map((course) => course.name)); @@ -48,11 +42,9 @@ export default function PaperRequest() { async function fetchPapers() { try { setIsLoading(true); - const response = await axios.get( - "/api/upcoming-papers", - ); + const response = await axios.get>("/api/upcoming-papers"); - setDisplayPapers(response.data); + setDisplayPapers(response.data.data ?? []); } catch (error) { console.error("Failed to fetch papers:", error); } finally { diff --git a/src/app/upload/page.tsx b/src/app/upload/page.tsx index 18533bb4..4ff1347d 100644 --- a/src/app/upload/page.tsx +++ b/src/app/upload/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useState, useRef } from "react"; import toast from "react-hot-toast"; import { Button } from "@/components/ui/button"; import axios, { AxiosError } from "axios"; @@ -13,7 +13,7 @@ import { TouchSensor, useSensor, useSensors, - DragEndEvent, + type DragEndEvent, } from "@dnd-kit/core"; import { arrayMove, @@ -24,16 +24,16 @@ import { import { CSS } from "@dnd-kit/utilities"; import Dropzone from "react-dropzone"; import { Upload, XIcon } from "lucide-react"; -import { getDocument, GlobalWorkerOptions } from "pdfjs-dist"; +import { GlobalWorkerOptions } from "pdfjs-dist"; +import type { ApiResponse } from "@/interface"; GlobalWorkerOptions.workerSrc = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.8.69/pdf.worker.min.mjs"; -interface APIResponse { - status: string; - message?: string; +interface uploadResponse { + file_url: string; + thumbnail_url: string; } - export default function Page() { const campus = "Vellore"; const [files, setFiles] = useState([]); @@ -44,6 +44,12 @@ export default function Page() { const [isGlobalDragging, setIsGlobalDragging] = useState(false); const [zoomIndex, setZoomIndex] = useState(null); + const previewsRef = useRef(previews); + + useEffect(() => { + previewsRef.current = previews; + }, [previews]); + useEffect(() => { const onDragEnter = () => setIsGlobalDragging(true); const onDragLeave = () => setIsGlobalDragging(false); @@ -63,7 +69,7 @@ export default function Page() { // Cleanup previews on unmount useEffect(() => { return () => { - previews.forEach((item) => { + previewsRef.current.forEach((item) => { try { URL.revokeObjectURL(item.preview); } catch {} @@ -73,7 +79,6 @@ export default function Page() { const fileCheckAndSelect = useCallback( (acceptedFiles: File[]) => { - const maxFileSize = 5 * 1024 * 1024; const allowedFileTypes = [ "application/pdf", "image/jpeg", @@ -87,6 +92,16 @@ export default function Page() { return; } + const MAX_FILE_SIZE = 5 * 1024 * 1024; + const oversizedFiles = acceptedFiles.filter((file) => file.size > MAX_FILE_SIZE); + if (oversizedFiles.length > 0) { + toast.error( + `File "${oversizedFiles[0]?.name}" exceeds the 5 MB limit. Please select a file under 5 MB.`, + { id: toastId }, + ); + return; + } + const isNewPdf = acceptedFiles.some( (file) => file.type === "application/pdf", ); @@ -131,28 +146,18 @@ export default function Page() { } const allFiles = [...files, ...acceptedFiles]; - if (allFiles.length > 5) { - toast.error("You can upload up to 5 files only", { id: toastId }); - return; - } - - const totalSize = allFiles.reduce( - (sum, f) => sum + f.size, - 0, - ); - if (totalSize > maxFileSize){ - toast.error("The total upload size exceeds 5MB.", { id: toastId }); + if (allFiles.length > 10) { + toast.error("You can upload up to 10 files only", { id: toastId }); return; } const invalidFiles = acceptedFiles.filter( - (file) => - file.size > maxFileSize || !allowedFileTypes.includes(file.type), + (file) => !allowedFileTypes.includes(file.type), ); if (invalidFiles.length > 0) { toast.error( - "Some files are invalid. Make sure the total size is below 5MB and files are of allowed types (PDF, JPEG, PNG, GIF).", + "Some files are invalid. Make sure they are PDFs, JPEGs, PNGs, or GIFs.", { id: toastId }, ); return; @@ -273,13 +278,16 @@ export default function Page() { await toast.promise( async () => { try { - await axios.post("/api/upload", formData); + await axios.post>( + "/api/upload", + formData, + ); return { message: "Papers uploaded successfully!" }; } catch (error) { - if (error instanceof AxiosError && error.response?.data) { - const errorData = error.response.data as APIResponse; + if (error instanceof AxiosError) { + const errorData = error?.response?.data as ApiResponse; const errorMessage = - errorData.message ?? "Failed to upload papers"; + errorData?.message ?? "Failed to upload papers"; throw new Error(errorMessage); } throw new Error("Failed to upload papers"); @@ -427,84 +435,102 @@ export default function Page() { )} {previews.length > 0 && ( -
-
-
- + {({ getRootProps, getInputProps }) => { + return ( +
- item.id)} - strategy={horizontalListSortingStrategy} - > -
- {previews.map((item, index) => ( - -
-
- {/* Index badge */} -
- - {index + 1} - -
- - {/* Delete button */} - - - {/* Preview */} -
- {item.file.type.startsWith("image/") ? ( - {`Page - ) : ( -