Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
S
Scenarioswebapp
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
TokaKaram
Scenarioswebapp
Commits
482c9d87
Commit
482c9d87
authored
Jan 11, 2026
by
TokaKaram
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
refactor quiz flow
parent
51309db4
Changes
7
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
149 additions
and
46 deletions
+149
-46
quizController.js
backend/src/controllers/quizController.js
+16
-1
userController.js
backend/src/controllers/userController.js
+2
-1
auth.js
backend/src/middleware/auth.js
+4
-4
Navbar.jsx
frontend/src/components/Layout/Navbar.jsx
+117
-34
QuestionCard.jsx
frontend/src/components/Quiz/QuestionCard.jsx
+2
-1
QuizTake.jsx
frontend/src/components/Quiz/QuizTake.jsx
+7
-4
Login.jsx
frontend/src/components/auth/Login.jsx
+1
-1
No files found.
backend/src/controllers/quizController.js
View file @
482c9d87
...
...
@@ -42,11 +42,13 @@ exports.getQuizById = async (req, res) => {
exports
.
startQuiz
=
async
(
req
,
res
)
=>
{
try
{
// console.log("Start Quiz");
const
[
existing
]
=
await
pool
.
execute
(
'SELECT * FROM quiz_attempts WHERE user_id = ? AND quiz_id = ? AND status = "in_progress"'
,
[
req
.
user
.
id
,
req
.
params
.
id
]
);
if
(
existing
.
length
>
0
)
{
// console.log("existing", existing);
return
res
.
json
({
attemptId
:
existing
[
0
].
id
,
message
:
"Continuing existing attempt"
,
...
...
@@ -56,6 +58,7 @@ exports.startQuiz = async (req, res) => {
"SELECT COUNT(*) as count FROM quiz_scenarios WHERE quiz_id = ?"
,
[
req
.
params
.
id
]
);
// console.log("secenarios", scenarios[0].count);
const
[
result
]
=
await
pool
.
execute
(
"INSERT INTO quiz_attempts (user_id, quiz_id, total_questions) VALUES (?, ?, ?)"
,
[
req
.
user
.
id
,
req
.
params
.
id
,
scenarios
[
0
].
count
]
...
...
@@ -69,6 +72,7 @@ exports.startQuiz = async (req, res) => {
exports
.
submitAnswer
=
async
(
req
,
res
)
=>
{
try
{
const
{
attemptId
,
scenarioId
,
selectedAnswer
}
=
req
.
body
;
// console.log("req.body", req.body);
const
[
scenarios
]
=
await
pool
.
execute
(
"SELECT * FROM scenarios WHERE id = ?"
,
[
scenarioId
]
...
...
@@ -86,7 +90,12 @@ exports.submitAnswer = async (req, res) => {
"SELECT answers FROM quiz_attempts WHERE id = ?"
,
[
attemptId
]
);
let
answers
=
attempt
[
0
].
answers
?
JSON
.
parse
(
attempt
[
0
].
answers
)
:
{};
let
answers
=
attempt
[
0
].
answers
?
typeof
attempt
[
0
].
answers
===
"string"
?
JSON
.
parse
(
attempt
[
0
].
answers
)
:
attempt
[
0
].
answers
:
{};
answers
[
scenarioId
]
=
{
selectedAnswer
,
isCorrect
};
const
correctCount
=
Object
.
values
(
answers
).
filter
(
(
a
)
=>
a
.
isCorrect
...
...
@@ -101,6 +110,7 @@ exports.submitAnswer = async (req, res) => {
rationale
:
scenario
.
best_answer_rationale
,
});
}
catch
(
error
)
{
console
.
log
(
"error"
,
error
);
res
.
status
(
500
).
json
({
message
:
"Server error"
,
error
:
error
.
message
});
}
};
...
...
@@ -108,6 +118,7 @@ exports.submitAnswer = async (req, res) => {
exports
.
completeQuiz
=
async
(
req
,
res
)
=>
{
try
{
const
{
attemptId
,
timeTaken
}
=
req
.
body
;
// console.log("req.body", req.body);
const
[
attempts
]
=
await
pool
.
execute
(
"SELECT * FROM quiz_attempts WHERE id = ? AND user_id = ?"
,
[
attemptId
,
req
.
user
.
id
]
...
...
@@ -116,6 +127,9 @@ exports.completeQuiz = async (req, res) => {
return
res
.
status
(
404
).
json
({
message
:
"Attempt not found"
});
}
const
attempt
=
attempts
[
0
];
// console.log("attempt", attempt);
// console.log("attempt.correct_answers=>", attempt.correct_answers);
// console.log(" attempt.total_questions=>", attempt.total_questions);
const
score
=
Math
.
round
(
(
attempt
.
correct_answers
/
attempt
.
total_questions
)
*
100
);
...
...
@@ -133,6 +147,7 @@ exports.completeQuiz = async (req, res) => {
totalQuestions
:
attempt
.
total_questions
,
});
}
catch
(
error
)
{
console
.
log
(
"erroir"
,
error
);
res
.
status
(
500
).
json
({
message
:
"Server error"
,
error
:
error
.
message
});
}
};
...
...
backend/src/controllers/userController.js
View file @
482c9d87
...
...
@@ -57,6 +57,7 @@ exports.getHistory = async (req, res) => {
FROM quiz_attempts qa
JOIN quizzes q ON qa.quiz_id = q.id
WHERE qa.user_id = ?
AND qa.total_questions > 0
ORDER BY qa.started_at DESC
`
,
[
req
.
user
.
id
]
...
...
backend/src/middleware/auth.js
View file @
482c9d87
...
...
@@ -3,16 +3,16 @@ const pool = require("../config/database");
module
.
exports
=
async
(
req
,
res
,
next
)
=>
{
try
{
console
.
log
(
"token"
,
req
.
header
(
"Authorization"
));
//
console.log("token", req.header("Authorization"));
const
token
=
req
.
header
(
"Authorization"
)?.
replace
(
"Bearer "
,
""
);
console
.
log
(
"token without bearer"
,
token
);
//
console.log("token without bearer", token);
if
(
!
token
)
{
return
res
.
status
(
401
)
.
json
({
message
:
"No token, authorization denied"
});
}
const
decoded
=
jwt
.
verify
(
token
,
process
.
env
.
JWT_SECRET
);
console
.
log
(
"decoded"
,
decoded
);
//
console.log("decoded", decoded);
const
[
users
]
=
await
pool
.
execute
(
"SELECT id, username, email, role FROM users WHERE id = ?"
,
[
decoded
.
id
]
...
...
@@ -23,7 +23,7 @@ module.exports = async (req, res, next) => {
req
.
user
=
users
[
0
];
next
();
}
catch
(
error
)
{
console
.
log
(
"error"
,
error
);
//
console.log("error", error);
res
.
status
(
401
).
json
({
message
:
"Token is not valid error"
});
}
};
frontend/src/components/Layout/Navbar.jsx
View file @
482c9d87
import
{
NavLink
,
useNavigate
}
from
'react-router-dom'
;
import
{
useAuth
}
from
'../../context/AuthContext'
;
import
{
FiHome
,
FiUser
,
FiLogOut
,
FiAward
,
FiClock
,
FiSettings
}
from
'react-icons/fi'
;
import
{
FiX
,
FiMenu
,
FiHome
,
FiUser
,
FiLogOut
,
FiAward
,
FiClock
,
FiSettings
}
from
'react-icons/fi'
;
import
{
useState
}
from
'react'
;
export
default
function
Navbar
()
{
const
[
isOpen
,
setIsOpen
]
=
useState
(
false
);
const
toggleMenu
=
()
=>
setIsOpen
(
!
isOpen
);
const
navLinkClass
=
({
isActive
})
=>
`flex items-center gap-2 transition px-4 py-2 rounded-md
${
isActive
?
'text-indigo-600 bg-indigo-50 lg:bg-transparent'
:
'text-gray-600 hover:text-indigo-600 hover:bg-gray-50 lg:hover:bg-transparent'
}
`
;
const
{
user
,
logout
}
=
useAuth
();
const
navigate
=
useNavigate
();
...
...
@@ -10,43 +19,117 @@ export default function Navbar() {
logout
();
navigate
(
'/login'
);
};
return
(
<
nav
className=
"bg-white shadow-lg"
>
return
(
<
nav
className=
"bg-white shadow-lg relative z-50"
>
<
div
className=
"container mx-auto px-4"
>
<
div
className=
"flex justify-between items-center h-16"
>
<
NavLink
to=
"/"
className=
"text-2xl font-bold text-indigo-600"
>
MCQ App
</
NavLink
>
{
user
?
(
<
div
className=
"flex items-center gap-6"
>
<
NavLink
to=
"/"
className=
{
({
isActive
})
=>
(
isActive
?
'text-indigo-600 flex items-center gap-2 hover:text-indigo-600 transition'
:
'flex items-center gap-2 text-gray-600 hover:text-indigo-600 transition'
)
}
>
<
FiHome
/>
Quizzes
</
NavLink
>
<
NavLink
to=
"/leaderboard"
className=
{
({
isActive
})
=>
(
isActive
?
'text-indigo-600 flex items-center gap-2 hover:text-indigo-600 transition'
:
'flex items-center gap-2 text-gray-600 hover:text-indigo-600 transition'
)
}
>
<
FiAward
/>
Leaderboard
</
NavLink
>
<
NavLink
to=
"/history"
className=
{
({
isActive
})
=>
(
isActive
?
'text-indigo-600 flex items-center gap-2 hover:text-indigo-600 transition'
:
'flex items-center gap-2 text-gray-600 hover:text-indigo-600 transition'
)
}
>
<
FiClock
/>
History
</
NavLink
>
{
user
.
role
===
'admin'
&&
(
<
NavLink
to=
"/admin"
className=
{
({
isActive
})
=>
(
isActive
?
'text-indigo-600 flex items-center gap-2 hover:text-indigo-600 transition'
:
'flex items-center gap-2 text-gray-600 hover:text-indigo-600 transition'
)
}
>
<
FiSettings
/>
Admin
{
/* Logo */
}
<
NavLink
to=
"/"
className=
"text-2xl font-bold text-indigo-600"
>
MCQ App
</
NavLink
>
)
}
<
NavLink
to=
"/profile"
className=
{
({
isActive
})
=>
(
isActive
?
'text-indigo-600 flex items-center gap-2 hover:text-indigo-600 transition'
:
'flex items-center gap-2 text-gray-600 hover:text-indigo-600 transition'
)
}
>
<
FiUser
/>
{
user
.
username
}
</
NavLink
>
<
button
onClick=
{
handleLogout
}
className=
"flex items-center gap-2 text-red-500 hover:text-red-700 transition"
>
<
FiLogOut
/>
Logout
{
/* Hamburger Button (Visible on mobile/tablet) */
}
<
div
className=
"lg:hidden"
>
<
button
onClick=
{
toggleMenu
}
className=
"text-gray-600 focus:outline-none p-2"
>
{
isOpen
?
<
FiX
size=
{
24
}
/>
:
<
FiMenu
size=
{
24
}
/>
}
</
button
>
</
div
>
)
:
(
<
div
className=
"flex items-center gap-4"
>
<
NavLink
to=
"/login"
className=
"text-gray-600 hover:text-indigo-600 transition"
>
Login
</
NavLink
>
<
NavLink
to=
"/register"
className=
"bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 transition"
>
Sign Up
</
NavLink
>
{
/* Desktop Menu */
}
<
div
className=
"hidden lg:flex items-center gap-6"
>
<
NavContent
user=
{
user
}
handleLogout=
{
handleLogout
}
navLinkClass=
{
navLinkClass
}
/>
</
div
>
</
div
>
{
/* Mobile & Tablet Menu (Dropdown) */
}
<
div
className=
{
`lg:hidden overflow-hidden transition-all duration-300 ease-in-out ${
isOpen ? 'max-h-screen border-t py-4' : 'max-h-0'
}`
}
>
<
div
className=
"flex flex-col gap-2"
>
<
NavContent
user=
{
user
}
handleLogout=
{
handleLogout
}
navLinkClass=
{
navLinkClass
}
isMobile=
{
true
}
closeMenu=
{
()
=>
setIsOpen
(
false
)
}
/>
</
div
>
)
}
</
div
>
</
div
>
</
nav
>
);
};
// مكون داخلي لتجنب تكرار الروابط
const
NavContent
=
({
user
,
handleLogout
,
navLinkClass
,
isMobile
,
closeMenu
})
=>
{
if
(
!
user
)
{
return
(
<
div
className=
{
`flex ${isMobile ? 'flex-col gap-2' : 'items-center gap-4'}`
}
>
<
NavLink
to=
"/login"
onClick=
{
closeMenu
}
className=
"text-gray-600 hover:text-indigo-600 px-4 py-2"
>
Login
</
NavLink
>
<
NavLink
to=
"/register"
onClick=
{
closeMenu
}
className=
"bg-indigo-600 text-white px-4 py-2 rounded-lg text-center"
>
Sign Up
</
NavLink
>
</
div
>
);
}
return
(
<>
<
NavLink
to=
"/"
onClick=
{
closeMenu
}
className=
{
navLinkClass
}
><
FiHome
/>
Quizzes
</
NavLink
>
<
NavLink
to=
"/leaderboard"
onClick=
{
closeMenu
}
className=
{
navLinkClass
}
><
FiAward
/>
Leaderboard
</
NavLink
>
<
NavLink
to=
"/history"
onClick=
{
closeMenu
}
className=
{
navLinkClass
}
><
FiClock
/>
History
</
NavLink
>
{
user
.
role
===
'admin'
&&
(
<
NavLink
to=
"/admin"
onClick=
{
closeMenu
}
className=
{
navLinkClass
}
><
FiSettings
/>
Admin
</
NavLink
>
)
}
<
NavLink
to=
"/profile"
onClick=
{
closeMenu
}
className=
{
navLinkClass
}
><
FiUser
/>
{
user
.
username
}
</
NavLink
>
<
button
onClick=
{
()
=>
{
handleLogout
();
if
(
isMobile
)
closeMenu
();
}
}
className=
"flex items-center gap-2 text-red-500 hover:text-red-700 transition px-4 py-2"
>
<
FiLogOut
/>
Logout
</
button
>
</>
);
// return (
// <nav className="bg-white shadow-lg">
// <div className="container mx-auto px-4">
// <div className="flex justify-between items-center h-16">
// <NavLink to="/" className="text-2xl font-bold text-indigo-600">MCQ App</NavLink>
// {user ? (
// <div className="flex items-center gap-6">
// <NavLink to="/" className={({ isActive }) => (isActive ? 'text-indigo-600 flex items-center gap-2 hover:text-indigo-600 transition' : 'flex items-center gap-2 text-gray-600 hover:text-indigo-600 transition')} >
// <FiHome /> Quizzes
// </NavLink>
// <NavLink to="/leaderboard" className={({ isActive }) => (isActive ? 'text-indigo-600 flex items-center gap-2 hover:text-indigo-600 transition' : 'flex items-center gap-2 text-gray-600 hover:text-indigo-600 transition')}>
// <FiAward /> Leaderboard
// </NavLink>
// <NavLink to="/history" className={({ isActive }) => (isActive ? 'text-indigo-600 flex items-center gap-2 hover:text-indigo-600 transition' : 'flex items-center gap-2 text-gray-600 hover:text-indigo-600 transition')}>
// <FiClock /> History
// </NavLink>
// {user.role === 'admin' && (
// <NavLink to="/admin" className={({ isActive }) => (isActive ? 'text-indigo-600 flex items-center gap-2 hover:text-indigo-600 transition' : 'flex items-center gap-2 text-gray-600 hover:text-indigo-600 transition')}>
// <FiSettings /> Admin
// </NavLink>
// )}
// <NavLink to="/profile" className={({ isActive }) => (isActive ? 'text-indigo-600 flex items-center gap-2 hover:text-indigo-600 transition' : 'flex items-center gap-2 text-gray-600 hover:text-indigo-600 transition')}>
// <FiUser /> {user.username}
// </NavLink>
// <button onClick={handleLogout} className="flex items-center gap-2 text-red-500 hover:text-red-700 transition">
// <FiLogOut /> Logout
// </button>
// </div>
// ) : (
// <div className="flex items-center gap-4">
// <NavLink to="/login" className="text-gray-600 hover:text-indigo-600 transition">Login</NavLink>
// <NavLink to="/register" className="bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 transition">Sign Up</NavLink>
// </div>
// )}
// </div>
// </div>
// </nav>
// );
}
\ No newline at end of file
frontend/src/components/Quiz/QuestionCard.jsx
View file @
482c9d87
import
{
useMemo
,
useState
}
from
"react"
;
export
default
function
QuestionCard
({
scenario
,
selectedAnswer
,
onSelectAnswer
,
showResult
,
answerResult
})
{
const
givensTable
=
typeof
scenario
.
givens_table
===
'string'
?
JSON
.
parse
(
scenario
.
givens_table
)
:
scenario
.
givens_table
;
// console.log("giventtable=>", scenario);
const
givensTable
=
typeof
scenario
?.
givens_table
===
'string'
?
JSON
.
parse
(
scenario
.
givens_table
)
:
scenario
.
givens_table
;
const
options
=
useMemo
(()
=>
{
return
[
...
...
frontend/src/components/Quiz/QuizTake.jsx
View file @
482c9d87
...
...
@@ -24,6 +24,7 @@ export default function QuizTake() {
const
loadQuiz
=
async
()
=>
{
try
{
const
quizRes
=
await
api
.
get
(
`/quiz/
${
id
}
`
);
console
.
log
(
"quizRes"
,
quizRes
);
setQuiz
(
quizRes
.
data
);
const
startRes
=
await
api
.
post
(
`/quiz/
${
id
}
/start`
);
setAttemptId
(
startRes
.
data
.
attemptId
);
...
...
@@ -75,21 +76,23 @@ export default function QuizTake() {
if
(
loading
)
return
<
Loading
/>;
if
(
!
quiz
)
return
null
;
const
currentScenario
=
quiz
.
scenarios
[
currentIndex
];
const
progress
=
((
currentIndex
+
1
)
/
quiz
.
scenarios
.
length
)
*
100
;
const
currentScenario
=
quiz
?
.
scenarios
[
currentIndex
];
const
progress
=
((
currentIndex
+
1
)
/
quiz
?
.
scenarios
.
length
)
*
100
;
return
(
<
div
className=
"max-w-4xl mx-auto"
>
<
div
className=
"bg-white rounded-xl shadow-lg p-6 mb-6"
>
<
div
className=
"flex justify-between items-center mb-4"
>
<
h1
className=
"text-2xl font-bold text-gray-800"
>
{
quiz
.
title
}
</
h1
>
<
span
className=
"text-gray-500"
>
Question
{
currentIndex
+
1
}
of
{
quiz
.
scenarios
.
length
}
</
span
>
<
span
className=
"text-gray-500"
>
Question
{
quiz
.
scenarios
.
length
===
0
?
0
:
currentIndex
+
1
}
of
{
quiz
.
scenarios
.
length
}
</
span
>
</
div
>
<
div
className=
"w-full bg-gray-200 rounded-full h-2"
>
<
div
className=
"bg-indigo-600 h-2 rounded-full transition-all duration-300"
style=
{
{
width
:
`${progress}%`
}
}
></
div
>
</
div
>
</
div
>
<
QuestionCard
scenario=
{
currentScenario
}
selectedAnswer=
{
selectedAnswer
}
onSelectAnswer=
{
setSelectedAnswer
}
showResult=
{
showResult
}
answerResult=
{
answerResult
}
/>
{
quiz
.
scenarios
.
length
===
0
?
<
div
className=
"bg-white rounded-xl shadow-lg p-6 mb-6"
>
No questions found for this quiz.
</
div
>
:
<
QuestionCard
scenario=
{
currentScenario
}
selectedAnswer=
{
selectedAnswer
}
onSelectAnswer=
{
setSelectedAnswer
}
showResult=
{
showResult
}
answerResult=
{
answerResult
}
/>
}
<
div
className=
"mt-6 flex justify-end"
>
{
!
showResult
?
(
<
button
onClick=
{
handleSubmitAnswer
}
disabled=
{
submitting
||
!
selectedAnswer
}
className=
"bg-indigo-600 text-white px-6 py-3 rounded-lg font-semibold hover:bg-indigo-700 transition flex items-center gap-2 disabled:opacity-50"
>
...
...
frontend/src/components/auth/Login.jsx
View file @
482c9d87
...
...
@@ -16,7 +16,7 @@ export default function Login() {
setLoading
(
true
);
try
{
const
r
=
await
login
(
email
,
password
);
console
.
log
(
"r=>"
,
r
);
//
console.log("r=>",r);
toast
.
success
(
'Login successful!'
);
if
(
r
.
user
.
role
===
'admin'
)
navigate
(
'/admin'
);
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment