From f55431def46a484f55b7ead213cb6d13b966942d Mon Sep 17 00:00:00 2001 From: Nasser AlKawari <60303522@udst.edu.qa> Date: Sat, 11 Apr 2026 17:16:42 +0300 Subject: [PATCH 1/4] feat: add input validation to login form --- src/pages/LoginPage.jsx | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx index a8b63b8..6c5d512 100644 --- a/src/pages/LoginPage.jsx +++ b/src/pages/LoginPage.jsx @@ -8,15 +8,47 @@ function LoginPage() { password: "", }); + const [errors, setErrors] = useState({}); + + const validateForm = () => { + const newErrors = {}; + + if (!formData.email.trim()) { + newErrors.email = "Email is required."; + } else if (!/\S+@\S+\.\S+/.test(formData.email)) { + newErrors.email = "Please enter a valid email address."; + } + + if (!formData.password.trim()) { + newErrors.password = "Password is required."; + } else if (formData.password.length < 6) { + newErrors.password = "Password must be at least 6 characters."; + } + + return newErrors; + }; + const handleChange = (e) => { setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value, })); + + setErrors((prev) => ({ + ...prev, + [e.target.name]: "", + })); }; const handleSubmit = (e) => { e.preventDefault(); + + const validationErrors = validateForm(); + if (Object.keys(validationErrors).length > 0) { + setErrors(validationErrors); + return; + } + navigate("/landing"); }; @@ -38,6 +70,7 @@ function LoginPage() { value={formData.email} onChange={handleChange} /> + {errors.email &&

{errors.email}

} + {errors.password &&

{errors.password}

} - )} - +
Upload
+ {errors.difficulty && ( +

{errors.difficulty}

+ )}
@@ -182,19 +169,31 @@ function GenerateQuizPage() { ))}
+ {errors.numQuestions && ( +

{errors.numQuestions}

+ )}
{error &&

{error}

} - @@ -218,15 +217,24 @@ function GenerateQuizPage() {

Instant Synthesis

-

Our AI engine transforms complex raw material into teachable concepts.

+

+ Our AI engine transforms complex raw material into teachable + concepts. +

Adaptive Feedback

-

Receive custom explanations for every answer to strengthen learning.

+

+ Receive custom explanations for every answer to strengthen + learning. +

Smart Repository

-

Every quiz is archived and categorized for future review and analysis.

+

+ Every quiz is archived and categorized for future review and + analysis. +

@@ -247,186 +255,4 @@ function GenerateQuizPage() { ); } -export default GenerateQuizPage; - -// REAL VERSION AFTER AI GENERATION IS IMPLEMENTED -// import { useState } from "react"; -// import { useNavigate } from "react-router-dom"; -// import { generateQuiz } from "../services/api"; - -// function GenerateQuizPage() { -// const navigate = useNavigate(); -// const [topic, setTopic] = useState(""); -// const [numQuestions, setNumQuestions] = useState(10); -// const [difficulty, setDifficulty] = useState("medium"); -// const [loading, setLoading] = useState(false); -// const [error, setError] = useState(""); - -// const handleGenerate = async (e) => { -// e.preventDefault(); - -// if (!topic.trim()) { -// setError("Please enter a topic or paste study material."); -// return; -// } - -// try { -// setLoading(true); -// setError(""); - -// const data = await generateQuiz({ -// topic, -// numQuestions, -// difficulty, -// }); - -// navigate("/quiz", { -// state: { -// topic, -// questions: data.questions || [], -// }, -// }); -// } catch (err) { -// setError(err.message || "Failed to generate quiz."); -// } finally { -// setLoading(false); -// } -// }; - -// return ( -//
-//
-//
Quizey
- -// - -//
Profile
-//
- -//
-//
-//
AI Powered Learning
-//

-// Craft your -//
-// intellectual -//
-// path. -//

-//

-// Input any topic and let AI architect a customized assessment designed -// for deep comprehension and long-term retention. -//

- -//
-//
-//
-//
-// 2.8m+ quizzes generated today -//
-//
- -//
-//
-// -// setTopic(e.target.value)} -// /> - -// -//
Upload
- -//
-//
-// -// -//
- -//
-// -//
-// {[5, 10, 15, 20].map((num) => ( -// -// ))} -//
-//
-//
- -// {error &&

{error}

} - -// - -//
-//
-// QUESTION PREVIEW -// LIVE PREVIEW -//
-//
-//
-//
-//
-//
-//
-//
- -//
-//

Why learn with Quizey?

-//
- -//
-//
-//

Instant Synthesis

-//

Our AI engine transforms complex raw material into teachable concepts.

-//
-//
-//

Adaptive Feedback

-//

Receive custom explanations for every answer to strengthen learning.

-//
-//
-//

Smart Repository

-//

Every quiz is archived and categorized for future review and analysis.

-//
-//
-//
- -// -//
-// ); -// } - -// export default GenerateQuizPage; \ No newline at end of file +export default GenerateQuizPage; \ No newline at end of file From 512347ec3628ec532e6fd624d9b499ffa6390790 Mon Sep 17 00:00:00 2001 From: Nasser AlKawari <60303522@udst.edu.qa> Date: Sat, 11 Apr 2026 18:56:17 +0300 Subject: [PATCH 4/4] feat: integrate frontend quiz flow with backend API and scoring --- backend/package-lock.json | 8 +- backend/server.js | 23 +++--- src/pages/FeedbackPage.jsx | 61 +++++++++------ src/pages/GenerateQuizPage.jsx | 2 +- src/pages/QuizPage.jsx | 133 ++++++++++++++++++++++++++------- 5 files changed, 166 insertions(+), 61 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index e30d78f..a6b9f9f 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -56,6 +56,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1512,6 +1513,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1805,6 +1807,7 @@ "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "peerDependencies": { "bare-abort-controller": "*" }, @@ -2034,6 +2037,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -2751,6 +2755,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3392,7 +3397,6 @@ "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", @@ -3408,7 +3412,6 @@ "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", @@ -3424,7 +3427,6 @@ "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", diff --git a/backend/server.js b/backend/server.js index a181bf2..a8844cc 100644 --- a/backend/server.js +++ b/backend/server.js @@ -19,17 +19,18 @@ app.use(express.json({ limit: '50mb' })); app.use(express.urlencoded({ limit: '50mb', extended: true })); app.use(express.text({ limit: '50mb' })); -// Routes +// API routes app.use('/api/quiz', quizRoutes); -// Serve frontend static files in production -const distPath = path.resolve(__dirname, '../dist'); -app.use(express.static(distPath)); +// Only serve frontend build in production +if (process.env.NODE_ENV === 'production') { + const distPath = path.resolve(__dirname, '../dist'); + app.use(express.static(distPath)); -// SPA fallback — serve index.html for any non-API route -app.get('*', (req, res) => { - res.sendFile(path.join(distPath, 'index.html')); -}); + app.get('*', (req, res) => { + res.sendFile(path.join(distPath, 'index.html')); + }); +} // Error handler app.use((err, req, res, next) => { @@ -39,7 +40,11 @@ app.use((err, req, res, next) => { details: 'File size exceeds the 50MB limit. Please upload a smaller document.', }); } - res.status(500).json({ error: 'Internal Server Error', details: err.message }); + + res.status(500).json({ + error: 'Internal Server Error', + details: err.message, + }); }); app.listen(PORT, () => { diff --git a/src/pages/FeedbackPage.jsx b/src/pages/FeedbackPage.jsx index 2b2829f..a296376 100644 --- a/src/pages/FeedbackPage.jsx +++ b/src/pages/FeedbackPage.jsx @@ -9,6 +9,12 @@ function FeedbackPage() { const selectedAnswers = location.state?.selectedAnswers || {}; const currentIndex = location.state?.currentIndex ?? 0; + // NEW: backend result support + const score = location.state?.score ?? 0; + const total = location.state?.total ?? questions.length; + const correctAnswers = location.state?.correctAnswers ?? score; + const wrongAnswers = location.state?.wrongAnswers ?? total - score; + if (!questions.length) { return (
@@ -43,28 +49,34 @@ function FeedbackPage() {
-

Keep going, Alex!

-

Mistakes are the best way to master complex logic.

+

+ {isCorrect ? "Great job!" : "Keep going!"} +

+

+ {isCorrect + ? "You're on the right track." + : "Mistakes are the best way to master complex logic."} +

@@ -76,7 +88,8 @@ function FeedbackPage() { let boxClass = "feedback-option-box"; if (option === correctAnswer) boxClass += " correct-box"; - if (userAnswer === option && option !== correctAnswer) boxClass += " wrong-box"; + if (userAnswer === option && option !== correctAnswer) + boxClass += " wrong-box"; return (
@@ -94,20 +107,27 @@ function FeedbackPage() {

AI Explanation

-
LUCID MENTOR ANALYSIS
+
+ LUCID MENTOR ANALYSIS +

{isCorrect - ? "Great job. Your answer matches the expected reasoning. This concept works because the underlying condition is already organized in a way that supports efficient searching." - : "Think of this like finding a name in a sorted phone book. Because the data is ordered, you can repeatedly eliminate half of the remaining items. That is why the more efficient approach depends on sorted input."} + ? "Great job. Your answer matches the expected reasoning." + : "Think of this like searching in a sorted structure. Efficient algorithms eliminate large portions quickly, which is why ordering matters."}

- + -
@@ -115,16 +135,15 @@ function FeedbackPage() {
Pro Tip

- Efficient searching often depends on structure. Ordered data creates - stronger algorithmic shortcuts. + Efficient searching depends on structure. Ordered data allows + faster algorithms.

Confused?

- Our interactive visualizer can show how the algorithm narrows the - search space. + Use visual explanations to better understand how the algorithm works.

diff --git a/src/pages/GenerateQuizPage.jsx b/src/pages/GenerateQuizPage.jsx index 0256129..d74cdbf 100644 --- a/src/pages/GenerateQuizPage.jsx +++ b/src/pages/GenerateQuizPage.jsx @@ -11,7 +11,7 @@ function GenerateQuizPage() { const [error, setError] = useState(""); const [errors, setErrors] = useState({}); - const API_URL = "http://localhost:5000/generate"; + const API_URL = "http://localhost:5000/api/quiz/generate"; const validateForm = () => { const newErrors = {}; diff --git a/src/pages/QuizPage.jsx b/src/pages/QuizPage.jsx index 0d369a9..3c19346 100644 --- a/src/pages/QuizPage.jsx +++ b/src/pages/QuizPage.jsx @@ -7,9 +7,12 @@ function QuizPage() { const questions = location.state?.questions || []; const topic = location.state?.topic || "Data Structures & Algorithms"; + const quizId = location.state?.quizId || null; const [currentIndex, setCurrentIndex] = useState(0); const [selectedAnswers, setSelectedAnswers] = useState({}); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); if (!questions.length) { return ( @@ -35,20 +38,61 @@ function QuizPage() { })); }; - const handleSubmit = () => { - const score = questions.reduce((total, question, index) => { - return total + (selectedAnswers[index] === question.correctAnswer ? 1 : 0); - }, 0); - - navigate("/feedback", { - state: { - topic, - questions, - selectedAnswers, - score, - currentIndex, - }, - }); + const handleNext = () => { + if (currentIndex < questions.length - 1) { + setCurrentIndex((prev) => prev + 1); + } + }; + + const handlePrevious = () => { + if (currentIndex > 0) { + setCurrentIndex((prev) => prev - 1); + } + }; + + const handleSubmit = async () => { + try { + setLoading(true); + setError(""); + + const answersArray = questions.map((_, index) => selectedAnswers[index] || null); + + const response = await fetch("http://localhost:5000/api/quiz/submit", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + quizId, + questions, + answers: answersArray, + }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || "Failed to submit quiz."); + } + + navigate("/feedback", { + state: { + topic, + questions, + selectedAnswers, + currentIndex, + score: data.score, + total: data.total, + correctAnswers: data.correctAnswers, + wrongAnswers: data.wrongAnswers, + resultId: data.resultId, + }, + }); + } catch (err) { + setError(err.message || "Failed to submit quiz."); + } finally { + setLoading(false); + } }; return ( @@ -82,7 +126,10 @@ function QuizPage() {
-
+
@@ -116,19 +163,51 @@ function QuizPage() { ))} - + {error &&

{error}

} + +
+ + + {currentIndex < questions.length - 1 ? ( + + ) : ( + + )} +
-