From f63e61a3b20b2cbf47e0907b5b2e712b1336cc00 Mon Sep 17 00:00:00 2001 From: abhitrueprogrammer Date: Thu, 24 Apr 2025 15:47:46 +0530 Subject: [PATCH 1/2] fix: remove redundent course list fetch --- src/components/searchbarSubjectList.tsx | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/components/searchbarSubjectList.tsx b/src/components/searchbarSubjectList.tsx index 19ada99..7a0d8c1 100644 --- a/src/components/searchbarSubjectList.tsx +++ b/src/components/searchbarSubjectList.tsx @@ -21,25 +21,7 @@ function SearchbarSubjectList({ 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) { - console.error("Error fetching courses:", err); - setError("Failed to fetch courses"); - setLoading(false); - } - }; - useEffect(() => { - void fetchCourses(); - }, []); const debouncedSearch = useCallback( debounce((text: string) => { From bb0e9d158ba347998552d794b3a544f7a000b70c Mon Sep 17 00:00:00 2001 From: abhitrueprogrammer Date: Thu, 24 Apr 2025 16:03:06 +0530 Subject: [PATCH 2/2] feat: add fuzzy search in course list also delete rendent files --- src/app/upload/page_not_AI.tsx | 270 ------------------- src/components/Searchbar/searchbar-child.tsx | 13 +- src/components/searchbarSubjectList.tsx | 146 ---------- 3 files changed, 10 insertions(+), 419 deletions(-) delete mode 100644 src/app/upload/page_not_AI.tsx delete mode 100644 src/components/searchbarSubjectList.tsx diff --git a/src/app/upload/page_not_AI.tsx b/src/app/upload/page_not_AI.tsx deleted file mode 100644 index e2e1211..0000000 --- a/src/app/upload/page_not_AI.tsx +++ /dev/null @@ -1,270 +0,0 @@ -"use client"; - -import React, { useState } from "react"; -import axios from "axios"; -import toast from "react-hot-toast"; -import { handleAPIError } from "../../util/error"; -import { Button } from "@/components/ui/button"; - -import { type APIResponse } from "@/interface"; -import { slots, years, semesters, exams } from "@/components/select_options"; -import SearchBar from "@/components/searchbarSubjectList"; -import Dropzone from "react-dropzone"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; - -const Page = () => { - const [slot, setSlot] = useState(""); - const [subject, setSubject] = useState(""); - const [exam, setExam] = useState(""); - const [year, setYear] = useState(""); - const [campus, setCampus] = useState("Vellore"); - const [semester, setSemester] = useState(""); - - const [files, setFiles] = useState([]); - const [isUploading, setIsUploading] = useState(false); - const [resetSearch, setResetSearch] = useState(false); - - const handlePrint = async () => { - const maxFileSize = 5 * 1024 * 1024; - const allowedFileTypes = [ - "application/pdf", - "image/jpeg", - "image/png", - "image/gif", - ]; - - if (!slot) { - toast.error("Slot is required"); - return; - } - if (!subject) { - toast.error("Subject is required"); - return; - } - if (!exam) { - toast.error("Exam is required"); - return; - } - if (!year) { - toast.error("Year is required"); - return; - } - if (!campus) { - setCampus("Vellore"); - } - - if (!semester) { - toast.error("Semester is required"); - return; - } - if (!files || files.length === 0) { - toast.error("No files selected"); - return; - } - - if (files.length > 5) { - toast.error("More than 5 files selected"); - return; - } - - const invalidFiles = files.filter( - (file) => - file.size > maxFileSize || !allowedFileTypes.includes(file.type), - ); - - if (invalidFiles.length > 0) { - toast.error( - `Some files are invalid. Ensure each file is below 5MB and of an allowed type (PDF, JPEG, PNG, GIF).`, - ); - return; - } - - const isPdf = files.length === 1 && files[0]?.type === "application/pdf"; - if (isPdf && files.length > 1) { - toast.error("PDFs must be uploaded separately"); - return; - } - - const formData = new FormData(); - files.forEach((file) => { - formData.append("files", file); - }); - formData.append("subject", subject); - formData.append("slot", slot); - formData.append("year", year); - formData.append("exam", exam); - formData.append("semester", semester); - formData.append("campus", campus); - - formData.append("isPdf", String(isPdf)); - - setIsUploading(true); - - try { - await toast.promise(axios.post("/api/upload", formData), { - loading: "Uploading papers...", - success: "Papers uploaded successfully!", - error: "Failed to upload papers. Please try again.", - }); - - setSlot(""); - setSubject(""); - setExam(""); - setYear(""); - setFiles([]); - setResetSearch(true); - setTimeout(() => setResetSearch(false), 100); - } catch (error) { - handleAPIError(error); - } finally { - setIsUploading(false); - } - }; - - return ( -
-
-
- Select paper parameters - -
- {/* Slot Selection */} -
- - -
- - {/* Exam Selection */} -
- - -
- - {/* Subject Selection */} -
- - -
- - {/* Year Selection */} -
- - -
- - {/* Year Selection */} -
- - -
- - {/* File Dropzone */} -
- setFiles(acceptedFiles)} - accept={{ "image/*": [], "application/pdf": [] }} - > - {({ getRootProps, getInputProps }) => ( -
-
- -

- Drag 'n' drop some files here, or{" "} - click to select - files -

-
-
- {files?.length || 0} files selected -
-
- )} -
- -
-
-
- -
-
- ); -}; - -export default Page; diff --git a/src/components/Searchbar/searchbar-child.tsx b/src/components/Searchbar/searchbar-child.tsx index 7bf1749..eb4c159 100644 --- a/src/components/Searchbar/searchbar-child.tsx +++ b/src/components/Searchbar/searchbar-child.tsx @@ -4,6 +4,7 @@ import { useState, useRef, useEffect } from "react"; import { Search } from "lucide-react"; import { useRouter } from "next/navigation"; import { Input } from "@/components/ui/input"; +import Fuse from "fuse.js"; function SearchBarChild({ initialSubjects, @@ -16,15 +17,21 @@ function SearchBarChild({ const [searchText, setSearchText] = useState(""); const [suggestions, setSuggestions] = useState([]); const suggestionsRef = useRef(null); + const fuzzy = new Fuse(initialSubjects); const handleSearchChange = (e: React.ChangeEvent) => { const text = e.target.value; setSearchText(text); if (text.length > 1 && initialSubjects.length > 0) { - const filteredSuggestions = initialSubjects.filter((subject) => - subject.toLowerCase().includes(text.toLowerCase()), - ); + const filteredSuggestions = fuzzy + .search(text) + .sort((a, b) => { + return (a.score ?? Infinity) - (b.score ?? Infinity); // Use Infinity for undefined scores + }) + .map((item) => item.item) + .slice(0, 10); + setSuggestions(filteredSuggestions); } else { setSuggestions([]); diff --git a/src/components/searchbarSubjectList.tsx b/src/components/searchbarSubjectList.tsx deleted file mode 100644 index 7a0d8c1..0000000 --- a/src/components/searchbarSubjectList.tsx +++ /dev/null @@ -1,146 +0,0 @@ -"use client"; - -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 { type ICourses } from "@/interface"; - -function SearchbarSubjectList({ - setSubject, - resetSearch, -}: { - setSubject: React.Dispatch>; - resetSearch: boolean; -}) { - const [searchText, setSearchText] = useState(""); - const [suggestions, setSuggestions] = useState([]); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); - const [courses, setCourses] = useState([]); - const suggestionsRef = useRef(null); - - - - const debouncedSearch = useCallback( - debounce((text: string) => { - if (text.length > 0) { - setLoading(true); - const escapedSearchText = text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - - const regex = new RegExp(escapedSearchText, "i"); - const filteredSubjects = courses - .filter((subject) => subject.search(regex) !== -1) - .slice(0, 10); - - if (filteredSubjects.length === 0) { - setError("Subject not found"); - setSuggestions([]); - setLoading(false); - return; - } - setSuggestions(filteredSubjects); - setError(null); - setLoading(false); - } else { - setSuggestions([]); - } - }, 500), - [courses], - ); - - const handleSearchChange = (e: React.ChangeEvent) => { - const text = e.target.value; - setSearchText(text); - if (text.length <= 0) { - setSuggestions([]); - } - debouncedSearch(text); - }; - - const handleSelectSuggestion = (suggestion: string) => { - setSearchText(suggestion); - setSuggestions([]); - setSubject(suggestion); - }; - - const handleClickOutside = (event: MouseEvent) => { - if ( - suggestionsRef.current && - !suggestionsRef.current.contains(event.target as Node) - ) { - setSuggestions([]); - } - }; - - useEffect(() => { - document.addEventListener("mousedown", handleClickOutside); - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, []); - - useEffect(() => { - if (resetSearch) { - setSearchText(""); - setSuggestions([]); - } - }, [resetSearch]); - - return ( -
-
-
- - - {loading && ( -
- Loading suggestions... -
- )} - {(suggestions.length > 0 || error) && !loading && ( -
    - {error ? ( -
  • {error}
  • - ) : ( - suggestions.map((suggestion, index) => ( -
  • handleSelectSuggestion(suggestion)} - className="cursor-pointer truncate p-2 hover:opacity-50" - style={{ - width: "100%", - overflow: "hidden", - whiteSpace: "nowrap", - textOverflow: "ellipsis", - }} - > - {suggestion} -
  • - )) - )} -
- )} -
-
-
- ); -} - -export default SearchbarSubjectList;