diff --git a/frontend/src/components/Common/PaperEditModal.tsx b/frontend/src/components/Common/PaperEditModal.tsx index 797ecb0..20fe410 100644 --- a/frontend/src/components/Common/PaperEditModal.tsx +++ b/frontend/src/components/Common/PaperEditModal.tsx @@ -3,7 +3,7 @@ import toast from "react-hot-toast"; import { MdCancel } from "react-icons/md"; import Fuse from 'fuse.js'; -import { validate } from "../../utils/validateInput"; +import { validate, validateCourseCode, validateExam, validateSemester, validateYear } from "../../utils/validateInput"; import { Exam, IAdminDashboardQP, IErrorMessage, IQuestionPaperFile, Semester } from "../../types/question_paper"; import { extractDetailsFromText, extractTextFromPDF, getCodeFromCourse, getCourseFromCode, IExtractedDetails } from "../../utils/autofillData"; import './styles/paper_edit_modal.scss'; @@ -13,6 +13,9 @@ import Spinner from "../Spinner/Spinner"; import { FormGroup, RadioGroup, NumberInput, SuggestionTextInput } from "./Form"; import COURSE_CODE_MAP from "../../data/courses.json"; +import { makeRequest } from "../../utils/backend"; +import { IEndpointTypes } from "../../types/backend"; +import { useAuthContext } from "../../utils/auth"; type UpdateQPHandler = (qp: T) => void; interface IPaperEditModalProps { @@ -22,6 +25,8 @@ interface IPaperEditModalProps { }; function PaperEditModal(props: IPaperEditModalProps) { + const auth = useAuthContext(); + const [data, setData] = useState(props.qPaper); const [validationErrors, setValidationErrors] = useState(validate(props.qPaper)); const [isDataValid, setIsDataValid] = useState(false); @@ -29,6 +34,9 @@ function PaperEditModal(props: const [ocrDetails, setOcrDetails] = useState(null); const [awaitingOcr, setAwaitingOcr] = useState(false); + const [similarPapers, setSimilarPapers] = useState([]); + const [awaitingSimilarPapers, setAwaitingSimilarPapers] = useState(false); + const changeData = (property: K, value: T[K]) => { setData((prev_data) => { return { @@ -72,11 +80,43 @@ function PaperEditModal(props: setAwaitingOcr(false); } - useEffect(() => { - if ('filelink' in props.qPaper) { - getOcrData(props.qPaper.filelink); + if ('filelink' in props.qPaper) { + useEffect(() => { + if ('filelink' in props.qPaper) { + getOcrData(props.qPaper.filelink); + } + }, []) + } + + const getSimilarPapers = async (details: IEndpointTypes['similar']['request']) => { + setAwaitingSimilarPapers(true); + const response = await makeRequest('similar', 'get', details, auth.jwt); + + if (response.status === "success") { + setSimilarPapers(response.data); + } else { + toast.error(`Error getting similar papers: ${response.message} (${response.status_code})`); } - }, []) + + setAwaitingSimilarPapers(false); + }; + + if ('filelink' in props.qPaper) { + useEffect(() => { + if (validateCourseCode(data.course_code)) { + const similarityDetails: IEndpointTypes['similar']['request'] = { + course_code: data.course_code + } + + if (validateYear(data.year)) similarityDetails['year'] = data.year; + if (validateExam(data.exam) && data.exam !== 'unknown' && data.exam !== 'ct') similarityDetails['exam'] = data.exam; + if (validateSemester(data.semester)) similarityDetails['semester'] = data.semester; + + getSimilarPapers(similarityDetails); + } + + }, [data.course_code, data.year, data.exam, data.semester]) + } const courseCodes = Object.keys(COURSE_CODE_MAP); const courseNames = Object.values(COURSE_CODE_MAP); @@ -154,7 +194,7 @@ function PaperEditModal(props: value={data.course_code} onValueChange={(value) => changeData('course_code', value.toUpperCase())} suggestions={trimSuggestions(courseCodes.filter((code) => code.startsWith(data.course_code)))} - inputProps={{required: true}} + inputProps={{ required: true }} /> (props: validationError={validationErrors.courseNameErr} > changeData('course_name', value.toUpperCase())} - suggestions={trimSuggestions(courseNamesFuse.search(data.course_name).map((result) => result.item))} - inputProps={{required: true}} - /> + value={data.course_name} + onValueChange={(value) => changeData('course_name', value.toUpperCase())} + suggestions={trimSuggestions(courseNamesFuse.search(data.course_name).map((result) => result.item))} + inputProps={{ required: true }} + /> (props: {'filelink' in data &&

Similar Papers

+ { + awaitingSimilarPapers ?
: +
+ +
+ }
} ; diff --git a/frontend/src/components/Search/SearchForm.tsx b/frontend/src/components/Search/SearchForm.tsx index 486daaa..e1c7fbf 100644 --- a/frontend/src/components/Search/SearchForm.tsx +++ b/frontend/src/components/Search/SearchForm.tsx @@ -31,7 +31,7 @@ function CourseSearchForm() { params.append("exam", exam); setAwaitingResponse(true); - const response = await makeRequest(`search?${params}`, 'get'); + const response = await makeRequest('search', 'get', {course: query, exam}); if (response.status === 'success') { const data: ISearchResult[] = response.data; diff --git a/frontend/src/types/backend.ts b/frontend/src/types/backend.ts index 715c09f..6958d58 100644 --- a/frontend/src/types/backend.ts +++ b/frontend/src/types/backend.ts @@ -1,4 +1,4 @@ -import { IAdminDashboardQP, ISearchResult } from "./question_paper"; +import { Exam, IAdminDashboardQP, ISearchResult, Semester } from "./question_paper"; export type AllowedBackendMethods = "get" | "post"; @@ -18,8 +18,11 @@ export type BackendResponse = IOkResponse | IErrorResponse; export interface IEndpointTypes { - [route: `search?${string}`]: { - request: null, + search: { + request: { + course: string; + exam: Exam | ""; + }, response: ISearchResult[] }, oauth: { @@ -61,5 +64,14 @@ export interface IEndpointTypes { response: { username: string; } + }, + similar: { + request: { + course_code: string; + year?: number; + semester?: Semester; + exam?: Exam; + }, + response: IAdminDashboardQP[]; } } \ No newline at end of file diff --git a/frontend/src/utils/backend.ts b/frontend/src/utils/backend.ts index b76b5e0..1f0aebf 100644 --- a/frontend/src/utils/backend.ts +++ b/frontend/src/utils/backend.ts @@ -2,11 +2,16 @@ import { AllowedBackendMethods, BackendResponse, IEndpointTypes } from "../types export const BACKEND_URL: string = import.meta.env.VITE_BACKEND_URL; -async function makeBackendRequest( +interface IBodyTypes { + get: Object; + post: Object | FormData; +} + +async function makeBackendRequest( endpoint: string, - method: AllowedBackendMethods, + method: M, jwt: string | null, - body: Object | FormData | null, + body: IBodyTypes[M] | null ): Promise { const headers: { "Content-Type"?: string; @@ -23,7 +28,12 @@ async function makeBackendRequest( switch (method) { case "get": - return await fetch(`${BACKEND_URL}/${endpoint}`, { + const requestURL = new URL(`${BACKEND_URL}/${endpoint}`); + if (body !== null) { + Object.entries(body).map(([key, value]) => requestURL.searchParams.set(key, value)); + } + + return await fetch(requestURL, { method: "get", headers, }); @@ -33,6 +43,8 @@ async function makeBackendRequest( headers, body: body instanceof FormData ? body : JSON.stringify(body ?? {}), }); + default: + throw 'This should not happen'; } }