Prakhar Singh commited on
Commit
76a77bc
·
1 Parent(s): c1209d2

Chore: The SideBar Username is Showing and The mcq page is created

Browse files
Backend/app/api/v1/endpoints/auth.py CHANGED
@@ -2,7 +2,8 @@ from fastapi import APIRouter, HTTPException, Depends, status
2
  from sqlalchemy.ext.asyncio import AsyncSession
3
  from sqlalchemy import select
4
  from datetime import timedelta
5
- from app.schema import UserCreate, Token, LoginRequest
 
6
  # from app.schema.models import LoginRequest
7
  from app.models import User
8
  from app.core import verify_password, get_password_hash, create_access_token
@@ -41,32 +42,32 @@ async def register(user: UserCreate, db: AsyncSession = Depends(get_db)):
41
  detail=f'registered failed: {str(e)}'
42
  )
43
 
44
- @router.post("/login", response_model=Token)
45
  async def login(request: LoginRequest, db: AsyncSession = Depends(get_db)):
46
- # Access the data via the request object
47
- email = request.email
48
- password = request.password
49
-
50
- # The rest of your logic remains the same
51
  try:
52
- result = await db.execute(select(User).filter(User.email==email))
53
  user = result.scalar_one_or_none()
54
 
55
- if not user or not verify_password(password, user.hashed_password):
56
  raise HTTPException(
57
  status_code=status.HTTP_401_UNAUTHORIZED,
58
- detail="Incorrect username or password",
59
  headers={"WWW-Authenticate": "Bearer"},
60
  )
 
61
  access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
62
  access_token = create_access_token(
63
- data={'sub':user.username},
64
  expires_deltas=access_token_expires
65
  )
66
 
67
- return {"access_token":access_token, "token_type":"bearer","username":user.username}
 
 
 
 
68
  except HTTPException:
69
- raise
70
  except Exception as e:
71
  raise HTTPException(
72
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
 
2
  from sqlalchemy.ext.asyncio import AsyncSession
3
  from sqlalchemy import select
4
  from datetime import timedelta
5
+ from app.schema import UserCreate, LoginRequest
6
+ from app.schema.models import LoginResponse
7
  # from app.schema.models import LoginRequest
8
  from app.models import User
9
  from app.core import verify_password, get_password_hash, create_access_token
 
42
  detail=f'registered failed: {str(e)}'
43
  )
44
 
45
+ @router.post("/login", response_model=LoginResponse)
46
  async def login(request: LoginRequest, db: AsyncSession = Depends(get_db)):
 
 
 
 
 
47
  try:
48
+ result = await db.execute(select(User).filter(User.email == request.email))
49
  user = result.scalar_one_or_none()
50
 
51
+ if not user or not verify_password(request.password, user.hashed_password):
52
  raise HTTPException(
53
  status_code=status.HTTP_401_UNAUTHORIZED,
54
+ detail="Incorrect email or password",
55
  headers={"WWW-Authenticate": "Bearer"},
56
  )
57
+
58
  access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
59
  access_token = create_access_token(
60
+ data={"sub": user.username},
61
  expires_deltas=access_token_expires
62
  )
63
 
64
+ return LoginResponse(
65
+ access_token=access_token,
66
+ token_type="bearer",
67
+ username=user.username
68
+ )
69
  except HTTPException:
70
+ raise
71
  except Exception as e:
72
  raise HTTPException(
73
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
Frontend/src/App.tsx CHANGED
@@ -13,12 +13,11 @@ import ProtectedRoute from "./routes/ProtectedRoute";
13
 
14
  const DashboardLayout = () => {
15
  // 1. Retrieve both logout and username from the AuthContext
16
- const { logout, username } = useAuth();
17
 
18
  return (
19
  <div className="flex h-screen bg-gray-100">
20
  {/* 2. Pass the retrieved username prop to the Sidebar */}
21
- <Sidebar onLogout={logout} username={username} />
22
 
23
  <main className="flex-1 overflow-y-auto">
24
  <Routes>
 
13
 
14
  const DashboardLayout = () => {
15
  // 1. Retrieve both logout and username from the AuthContext
 
16
 
17
  return (
18
  <div className="flex h-screen bg-gray-100">
19
  {/* 2. Pass the retrieved username prop to the Sidebar */}
20
+ <Sidebar/>
21
 
22
  <main className="flex-1 overflow-y-auto">
23
  <Routes>
Frontend/src/components/auth/SignIn.tsx CHANGED
@@ -1,55 +1,43 @@
1
  import React, { useState, type FormEvent } from "react";
2
- import { Mail, Lock, LogIn, Chrome } from "lucide-react";
3
  import API from "../../api/api";
 
4
 
5
  interface SignInProps {
6
- onClose: () => void;
7
- onSwitchToSignUp: () => void;
8
- onAuthSuccess: () => void;
9
  }
10
 
11
- const SignIn: React.FC<SignInProps> = ({
12
- onClose,
13
- onSwitchToSignUp,
14
- onAuthSuccess,
15
- }) => {
16
  const [email, setEmail] = useState("");
17
  const [password, setPassword] = useState("");
18
  const [error, setError] = useState("");
 
19
 
20
  const handleSubmit = async (e: FormEvent) => {
21
  e.preventDefault();
22
  setError("");
23
 
24
  try {
25
- // 1. Send JSON body with keys matching the backend's LoginRequest schema
26
- const res = await API.post("/auth/login", {
27
- email: email, // Backend expects 'email' key in the JSON body
28
- password,
29
- });
30
 
31
  localStorage.setItem("token", res.data.access_token);
32
- onAuthSuccess();
33
  } catch (err: any) {
34
  console.error("Login error:", err);
35
- let errorMessage = "Login failed due to an unknown error.";
36
 
37
- // --- Robust Error Handling for 422 (Pydantic validation) ---
38
- if (err.response && err.response.data && err.response.data.detail) {
 
39
  const detail = err.response.data.detail;
40
 
41
- if (typeof detail === 'string') {
42
- // Handle simple error messages like "Incorrect username or password" (from 401)
43
- errorMessage = detail;
44
- } else if (Array.isArray(detail) && detail.length > 0) {
45
- // Handle 422 Pydantic validation error (list of error objects)
46
- // We extract the human-readable message from the first object
47
- const firstError = detail[0];
48
- errorMessage = `Validation Error on '${firstError.loc.join(' -> ')}': ${firstError.msg}`;
49
  }
50
- } else if (err.message) {
51
- // Fallback for network errors (e.g., server offline)
52
- errorMessage = err.message;
53
  }
54
 
55
  setError(errorMessage);
@@ -58,11 +46,8 @@ const SignIn: React.FC<SignInProps> = ({
58
 
59
  return (
60
  <>
61
- <h2 className="text-3xl font-bold mb-8 text-center text-white">
62
- Welcome Back
63
- </h2>
64
 
65
- {/* 2. This ensures only a string is rendered, fixing the React error */}
66
  {error && <p className="text-red-400 text-center mb-3">{error}</p>}
67
 
68
  <form className="space-y-5" onSubmit={handleSubmit}>
@@ -71,7 +56,7 @@ const SignIn: React.FC<SignInProps> = ({
71
  <div className="relative">
72
  <Mail className="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-blue-400" />
73
  <input
74
- type="string"
75
  placeholder="username or email"
76
  value={email}
77
  onChange={(e) => setEmail(e.target.value)}
@@ -86,7 +71,7 @@ const SignIn: React.FC<SignInProps> = ({
86
  <div className="relative">
87
  <Lock className="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-blue-400" />
88
  <input
89
- type="password" // Changed type to 'password' for browser security
90
  placeholder="••••••••"
91
  value={password}
92
  onChange={(e) => setPassword(e.target.value)}
@@ -97,15 +82,16 @@ const SignIn: React.FC<SignInProps> = ({
97
  </div>
98
 
99
  <button
100
- type="submit" // Added type="submit" for proper form submission
101
- className="w-full px-5 py-3 rounded-lg bg-linear-to-r from-blue-500 to-gray-500 text-white flex items-center justify-center gap-2">
 
102
  <LogIn className="w-5 h-5" />
103
  Sign In
104
  </button>
105
  </form>
106
 
107
  <p className="mt-8 text-center text-sm text-gray-400">
108
- Don’t have an account?
109
  <button onClick={onSwitchToSignUp} className="ml-2 text-blue-400">
110
  Sign Up
111
  </button>
 
1
  import React, { useState, type FormEvent } from "react";
2
+ import { Mail, Lock, LogIn } from "lucide-react";
3
  import API from "../../api/api";
4
+ import { useAuth } from "../context/AuthContext";
5
 
6
  interface SignInProps {
7
+ onSwitchToSignUp: () => void; // ✅ Add this if switching modal
 
 
8
  }
9
 
10
+ const SignIn: React.FC<SignInProps> = ({ onSwitchToSignUp }) => {
 
 
 
 
11
  const [email, setEmail] = useState("");
12
  const [password, setPassword] = useState("");
13
  const [error, setError] = useState("");
14
+ const { login } = useAuth();
15
 
16
  const handleSubmit = async (e: FormEvent) => {
17
  e.preventDefault();
18
  setError("");
19
 
20
  try {
21
+ const res = await API.post("/auth/login", { email, password });
22
+
23
+ // FIXED use res instead of response
24
+ login(res.data.username || res.data.user?.username || "User");
 
25
 
26
  localStorage.setItem("token", res.data.access_token);
27
+
28
  } catch (err: any) {
29
  console.error("Login error:", err);
 
30
 
31
+ let errorMessage = "Login failed.";
32
+
33
+ if (err.response?.data?.detail) {
34
  const detail = err.response.data.detail;
35
 
36
+ if (typeof detail === "string") errorMessage = detail;
37
+ else if (Array.isArray(detail)) {
38
+ const first = detail[0];
39
+ errorMessage = `${first.loc.join(" -> ")}: ${first.msg}`;
 
 
 
 
40
  }
 
 
 
41
  }
42
 
43
  setError(errorMessage);
 
46
 
47
  return (
48
  <>
49
+ <h2 className="text-3xl font-bold mb-8 text-center text-white">Welcome Back</h2>
 
 
50
 
 
51
  {error && <p className="text-red-400 text-center mb-3">{error}</p>}
52
 
53
  <form className="space-y-5" onSubmit={handleSubmit}>
 
56
  <div className="relative">
57
  <Mail className="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-blue-400" />
58
  <input
59
+ type="text"
60
  placeholder="username or email"
61
  value={email}
62
  onChange={(e) => setEmail(e.target.value)}
 
71
  <div className="relative">
72
  <Lock className="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-blue-400" />
73
  <input
74
+ type="password"
75
  placeholder="••••••••"
76
  value={password}
77
  onChange={(e) => setPassword(e.target.value)}
 
82
  </div>
83
 
84
  <button
85
+ type="submit"
86
+ className="w-full px-5 py-3 rounded-lg bg-linear-to-r from-blue-500 to-gray-500 text-white flex items-center justify-center gap-2"
87
+ >
88
  <LogIn className="w-5 h-5" />
89
  Sign In
90
  </button>
91
  </form>
92
 
93
  <p className="mt-8 text-center text-sm text-gray-400">
94
+ Don’t have an account?{" "}
95
  <button onClick={onSwitchToSignUp} className="ml-2 text-blue-400">
96
  Sign Up
97
  </button>
Frontend/src/components/auth/SignUp.tsx CHANGED
@@ -126,7 +126,7 @@ const SignUp: React.FC<SignUpProps> = ({ onSwitchToSignIn, onAuthSuccess }) => {
126
 
127
  <button
128
  onClick={handleSubmit}
129
- className="w-full px-5 py-3 rounded-lg bg-gradient-to-r from-blue-500 to-purple-500 text-white flex items-center justify-center gap-2 hover:from-blue-600 hover:to-purple-600 transition"
130
  >
131
  <UserPlus className="w-5 h-5" />
132
  Sign Up
 
126
 
127
  <button
128
  onClick={handleSubmit}
129
+ className="w-full px-5 py-3 rounded-lg bg-linear-to-r from-blue-500 to-purple-500 text-white flex items-center justify-center gap-2 hover:from-blue-600 hover:to-purple-600 transition"
130
  >
131
  <UserPlus className="w-5 h-5" />
132
  Sign Up
Frontend/src/components/dashboard/Sidebar.tsx CHANGED
@@ -4,14 +4,12 @@ import {
4
  Home, FileText, Brain, BookOpen,
5
  Settings, User, LogOut
6
  } from "lucide-react";
 
7
 
8
- interface SidebarProps {
9
- onLogout?: () => void; // Function to handle logging out
10
- username: string;
11
- }
12
 
13
- const Sidebar: React.FC<SidebarProps> = ({ onLogout, username }) => {
14
  const location = useLocation();
 
15
 
16
  const navItems = [
17
  { path: "/dashboard", label: "Dashboard", icon: <Home size={18} /> },
@@ -55,12 +53,12 @@ const Sidebar: React.FC<SidebarProps> = ({ onLogout, username }) => {
55
  {/* User Profile Button - Now displays the actual username */}
56
  <button className="flex items-center space-x-3 p-3 rounded-lg hover:bg-gray-800 transition w-full text-left text-gray-300">
57
  <User size={18} />
58
- <span>{username || "Guest User"}</span>
59
  </button>
60
 
61
  {/* Logout Button (Click handler added here) */}
62
  <button
63
- onClick={onLogout}
64
  className="flex items-center space-x-3 p-3 rounded-lg bg-red-500 hover:bg-red-600 text-black hover:text-white transition w-full text-left font-medium"
65
  >
66
  <LogOut size={18} />
 
4
  Home, FileText, Brain, BookOpen,
5
  Settings, User, LogOut
6
  } from "lucide-react";
7
+ import { useAuth } from "../context/AuthContext";
8
 
 
 
 
 
9
 
10
+ const Sidebar: React.FC = () => {
11
  const location = useLocation();
12
+ const { username, logout } = useAuth();
13
 
14
  const navItems = [
15
  { path: "/dashboard", label: "Dashboard", icon: <Home size={18} /> },
 
53
  {/* User Profile Button - Now displays the actual username */}
54
  <button className="flex items-center space-x-3 p-3 rounded-lg hover:bg-gray-800 transition w-full text-left text-gray-300">
55
  <User size={18} />
56
+ <span>{username}</span>
57
  </button>
58
 
59
  {/* Logout Button (Click handler added here) */}
60
  <button
61
+ onClick={logout}
62
  className="flex items-center space-x-3 p-3 rounded-lg bg-red-500 hover:bg-red-600 text-black hover:text-white transition w-full text-left font-medium"
63
  >
64
  <LogOut size={18} />
Frontend/src/components/quize/mcq.tsx ADDED
@@ -0,0 +1,504 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useCallback, useEffect, useMemo, useState } from "react";
2
+ import {
3
+ ChevronRight,
4
+ CheckCircle,
5
+ XCircle,
6
+ ArrowLeft,
7
+ } from "lucide-react";
8
+
9
+ interface MCQQuizPageProps {
10
+ onBack: () => void;
11
+ // optional: total time in seconds (defaults to 15 minutes)
12
+ totalTimeSeconds?: number;
13
+ }
14
+
15
+ type QStatus = "notVisited" | "visited" | "answered" | "markedForReview";
16
+
17
+ interface QuestionItem {
18
+ id: number;
19
+ question: string;
20
+ options: string[];
21
+ answer: string;
22
+ // runtime fields
23
+ selected?: string | null;
24
+ status?: QStatus;
25
+ }
26
+
27
+ const initialMockData: QuestionItem[] = [
28
+ {
29
+ id: 1,
30
+ question: "Which hook is used to perform side effects in React?",
31
+ options: ["useState", "useContext", "useEffect", "useReducer"],
32
+ answer: "useEffect",
33
+ },
34
+ {
35
+ id: 2,
36
+ question: "What does the 'FileSpreadsheet' icon represent?",
37
+ options: [
38
+ "A config file",
39
+ "A PDF or document upload",
40
+ "A database query",
41
+ "A CSS file",
42
+ ],
43
+ answer: "A PDF or document upload",
44
+ },
45
+ {
46
+ id: 3,
47
+ question: "Which Tailwind class keeps an element sticky during scroll?",
48
+ options: ["sticky", "absolute", "fixed", "relative"],
49
+ answer: "sticky",
50
+ },
51
+ {
52
+ id: 4,
53
+ question: "Which prop was NOT passed earlier to OutputTypeOption?",
54
+ options: ["icon", "title", "desc", "onClick"],
55
+ answer: "onClick",
56
+ },
57
+ ];
58
+
59
+ const formatTime = (seconds: number) => {
60
+ const mm = Math.floor(seconds / 60)
61
+ .toString()
62
+ .padStart(2, "0");
63
+ const ss = (seconds % 60).toString().padStart(2, "0");
64
+ return `${mm}:${ss}`;
65
+ };
66
+
67
+ const MCQQuizPage: React.FC<MCQQuizPageProps> = ({
68
+ onBack,
69
+ totalTimeSeconds = 15 * 60, // default 15 minutes
70
+ }) => {
71
+ const [questions, setQuestions] = useState<QuestionItem[]>(
72
+ () =>
73
+ initialMockData.map((q, idx) => ({
74
+ ...q,
75
+ selected: null,
76
+ status: idx === 0 ? "visited" : "notVisited",
77
+ }))
78
+ );
79
+
80
+ const [currentIndex, setCurrentIndex] = useState(0);
81
+ const [isAnswered, setIsAnswered] = useState(false); // tracks current question answered state
82
+ const [showScore, setShowScore] = useState(false);
83
+ const [timeLeft, setTimeLeft] = useState(totalTimeSeconds);
84
+
85
+ const totalQuestions = questions.length;
86
+ const currentQuestion = questions[currentIndex];
87
+
88
+ // Initialize isAnswered for current question when index changes
89
+ useEffect(() => {
90
+ setIsAnswered(Boolean(currentQuestion?.selected));
91
+ }, [currentQuestion]);
92
+
93
+ // Timer with auto-submit
94
+ useEffect(() => {
95
+ if (showScore) return;
96
+ if (timeLeft <= 0) {
97
+ handleAutoSubmit();
98
+ return;
99
+ }
100
+ const t = setInterval(() => setTimeLeft((s) => s - 1), 1000);
101
+ return () => clearInterval(t);
102
+ // eslint-disable-next-line react-hooks/exhaustive-deps
103
+ }, [timeLeft, showScore]);
104
+
105
+ const saveCurrentSelection = useCallback(
106
+ (selected: string | null) => {
107
+ setQuestions((prev) => {
108
+ const copy = prev.map((q, idx) => {
109
+ if (idx !== currentIndex) return q;
110
+ return {
111
+ ...q,
112
+ selected,
113
+ status:
114
+ selected !== null
115
+ ? "answered"
116
+ : q.status === "markedForReview"
117
+ ? "markedForReview"
118
+ : "visited",
119
+ };
120
+ });
121
+ return copy;
122
+ });
123
+ },
124
+ [currentIndex]
125
+ );
126
+
127
+ const handleAnswerClick = useCallback(
128
+ (option: string) => {
129
+ // mark selected immediately
130
+ setQuestions((prev) =>
131
+ prev.map((q, idx) =>
132
+ idx === currentIndex
133
+ ? { ...q, selected: option, status: "answered" }
134
+ : q
135
+ )
136
+ );
137
+ setIsAnswered(true);
138
+ },
139
+ [currentIndex]
140
+ );
141
+
142
+ const goToQuestion = useCallback(
143
+ (index: number) => {
144
+ setQuestions((prev) =>
145
+ prev.map((q, idx) => {
146
+ if (idx === index) {
147
+ // mark visited if not visited
148
+ return {
149
+ ...q,
150
+ status: q.status === "notVisited" ? "visited" : q.status,
151
+ };
152
+ }
153
+ // also ensure current question remains with existing status
154
+ return q;
155
+ })
156
+ );
157
+ setCurrentIndex(index);
158
+ },
159
+ []
160
+ );
161
+
162
+ const handleSaveAndNext = useCallback(() => {
163
+ // ensure status updated even if answered or not
164
+ setQuestions((prev) =>
165
+ prev.map((q, idx) => {
166
+ if (idx === currentIndex) {
167
+ return {
168
+ ...q,
169
+ status:
170
+ q.status === "markedForReview"
171
+ ? "markedForReview"
172
+ : q.selected
173
+ ? "answered"
174
+ : "visited",
175
+ };
176
+ }
177
+ return q;
178
+ })
179
+ );
180
+
181
+ const next = currentIndex + 1;
182
+ if (next < totalQuestions) {
183
+ goToQuestion(next);
184
+ } else {
185
+ // at end -> submit
186
+ handleSubmit();
187
+ }
188
+ }, [currentIndex, goToQuestion, totalQuestions]);
189
+
190
+ const handleMarkForReview = useCallback(() => {
191
+ setQuestions((prev) =>
192
+ prev.map((q, idx) =>
193
+ idx === currentIndex
194
+ ? {
195
+ ...q,
196
+ status: "markedForReview",
197
+ }
198
+ : q
199
+ )
200
+ );
201
+ // move to next question
202
+ const next = currentIndex + 1;
203
+ if (next < totalQuestions) goToQuestion(next);
204
+ }, [currentIndex, goToQuestion, totalQuestions]);
205
+
206
+ const handleClearResponse = useCallback(() => {
207
+ setQuestions((prev) =>
208
+ prev.map((q, idx) =>
209
+ idx === currentIndex ? { ...q, selected: null, status: "visited" } : q
210
+ )
211
+ );
212
+ setIsAnswered(false);
213
+ }, [currentIndex]);
214
+
215
+ const computeScore = useCallback(() => {
216
+ let s = 0;
217
+ questions.forEach((q) => {
218
+ if (q.selected && q.selected === q.answer) s += 1;
219
+ });
220
+ return s;
221
+ }, [questions]);
222
+
223
+ const handleSubmit = useCallback(() => {
224
+ setShowScore(true);
225
+ }, []);
226
+
227
+ const handleAutoSubmit = useCallback(() => {
228
+ // ensure final statuses saved and then show score
229
+ setShowScore(true);
230
+ }, []);
231
+
232
+ // palette helpers
233
+ const statusClassForPalette = (q: QuestionItem, idx: number) => {
234
+ // Blue = current; Green = answered; Yellow = markedForReview; Red = visited but not answered; Gray = notVisited
235
+ if (idx === currentIndex)
236
+ return "bg-blue-500 text-white ring-2 ring-blue-300";
237
+ switch (q.status) {
238
+ case "answered":
239
+ return "bg-green-500 text-white";
240
+ case "markedForReview":
241
+ return "bg-yellow-400 text-black";
242
+ case "visited":
243
+ return "bg-red-500 text-white";
244
+ case "notVisited":
245
+ default:
246
+ return "bg-slate-300 text-slate-700";
247
+ }
248
+ };
249
+
250
+ const score = useMemo(() => computeScore(), [computeScore]);
251
+
252
+ // Render Score screen
253
+ if (showScore) {
254
+ return (
255
+ <div className="min-h-screen bg-gray-100 flex items-center justify-center p-6">
256
+ <div className="w-full max-w-3xl p-8 bg-white rounded-xl shadow-lg text-center">
257
+ <CheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
258
+ <h2 className="text-3xl font-bold mb-2">Test Complete</h2>
259
+ <p className="text-gray-600 mb-6">Your results are below.</p>
260
+
261
+ <div className="text-center mb-6">
262
+ <div className="text-xl text-gray-500">Score</div>
263
+ <div className="text-5xl font-extrabold text-blue-600">{score}</div>
264
+ <div className="text-sm text-gray-500">out of {totalQuestions}</div>
265
+ </div>
266
+
267
+ <div className="flex justify-center gap-3">
268
+ <button
269
+ onClick={() => {
270
+ // simple retry: reset everything
271
+ setQuestions(
272
+ initialMockData.map((q, idx) => ({
273
+ ...q,
274
+ selected: null,
275
+ status: idx === 0 ? "visited" : "notVisited",
276
+ }))
277
+ );
278
+ setCurrentIndex(0);
279
+ setShowScore(false);
280
+ setTimeLeft(totalTimeSeconds);
281
+ setIsAnswered(false);
282
+ }}
283
+ className="px-6 py-3 rounded-lg bg-linear-to-r from-blue-500 to-blue-700 text-white font-medium hover:opacity-90"
284
+ >
285
+ Retry
286
+ </button>
287
+
288
+ <button
289
+ onClick={onBack}
290
+ className="px-6 py-3 rounded-lg border border-slate-300 text-slate-700 bg-white hover:bg-slate-50"
291
+ >
292
+ ← Back
293
+ </button>
294
+ </div>
295
+ </div>
296
+ </div>
297
+ );
298
+ }
299
+
300
+ // Main quiz UI
301
+ return (
302
+ <div className="min-h-screen bg-gray-50 p-6 lg:p-12">
303
+ <div className="max-w-7xl mx-auto grid grid-cols-12 gap-6">
304
+ {/* Left: Main Question area (8 cols) */}
305
+ <div className="col-span-12 lg:col-span-8">
306
+ <div className="mb-4 flex items-center justify-between">
307
+ <div>
308
+ <button
309
+ onClick={onBack}
310
+ className="flex items-center text-blue-600 hover:text-blue-500"
311
+ >
312
+ <ArrowLeft className="w-4 h-4 mr-2" />
313
+ Back to Generator
314
+ </button>
315
+ <h1 className="text-2xl lg:text-3xl font-bold mt-3">
316
+ Generated MCQ Quiz
317
+ </h1>
318
+ <p className="text-sm text-gray-500 mt-1">
319
+ Mixed section — complete the test before time ends.
320
+ </p>
321
+ </div>
322
+
323
+ <div className="hidden md:flex flex-col items-end text-sm text-gray-600">
324
+ <div>Question</div>
325
+ <div className="text-xl font-bold text-blue-600">
326
+ {currentIndex + 1} / {totalQuestions}
327
+ </div>
328
+ <div className="mt-1">Score: <span className="font-semibold text-green-600">{score}</span></div>
329
+ </div>
330
+ </div>
331
+
332
+ <div className="bg-white rounded-xl shadow p-6 mb-6 border border-slate-200">
333
+ <div className="mb-6">
334
+ <div className="text-sm text-gray-500 mb-2">Quant - Question</div>
335
+ <h2 className="text-xl lg:text-2xl font-semibold text-slate-800">
336
+ {currentQuestion.question}
337
+ </h2>
338
+ </div>
339
+
340
+ <div className="grid gap-4">
341
+ {currentQuestion.options.map((opt, i) => {
342
+ const isSelected = currentQuestion.selected === opt;
343
+ const optionClass = isAnswered
344
+ ? opt === currentQuestion.answer
345
+ ? "bg-green-600/80 text-white border-green-500 shadow"
346
+ : isSelected
347
+ ? "bg-red-600/80 text-white border-red-500 shadow"
348
+ : "bg-slate-50 border-slate-200 text-slate-700 opacity-70"
349
+ : isSelected
350
+ ? "bg-blue-600/80 text-white"
351
+ : "bg-white hover:bg-slate-50 border border-slate-200 text-slate-800";
352
+
353
+ return (
354
+ <button
355
+ key={i}
356
+ onClick={() => handleAnswerClick(opt)}
357
+ disabled={isAnswered && opt !== currentQuestion.answer && isSelected === false && false}
358
+ className={`p-4 rounded-lg text-left border transition flex justify-between items-center ${optionClass}`}
359
+ >
360
+ <span>{opt}</span>
361
+
362
+ {isAnswered && opt === currentQuestion.answer && (
363
+ <CheckCircle className="w-5 h-5 text-green-100" />
364
+ )}
365
+ {isAnswered &&
366
+ currentQuestion.selected === opt &&
367
+ opt !== currentQuestion.answer && (
368
+ <XCircle className="w-5 h-5 text-red-200" />
369
+ )}
370
+ {!isAnswered && isSelected && (
371
+ <span className="text-sm text-blue-50">Selected</span>
372
+ )}
373
+ </button>
374
+ );
375
+ })}
376
+ </div>
377
+
378
+ {/* Bottom actions */}
379
+ <div className="mt-6 flex flex-col md:flex-row gap-3 md:gap-4 items-center justify-between">
380
+ <div className="flex gap-2 flex-wrap">
381
+ <button
382
+ onClick={handleMarkForReview}
383
+ className="px-4 py-2 rounded-md bg-yellow-400 text-black font-medium"
384
+ >
385
+ Mark for Review
386
+ </button>
387
+
388
+ <button
389
+ onClick={handleClearResponse}
390
+ className="px-4 py-2 rounded-md border border-slate-300 text-slate-700 bg-white"
391
+ >
392
+ Clear Response
393
+ </button>
394
+
395
+ <button
396
+ onClick={handleSaveAndNext}
397
+ className="px-4 py-2 rounded-md bg-linear-to-r from-blue-500 to-blue-700 text-white font-medium flex items-center gap-2"
398
+ >
399
+ Save & Next <ChevronRight className="w-4 h-4" />
400
+ </button>
401
+ </div>
402
+
403
+ <div className="text-sm text-gray-600">
404
+ {currentIndex === totalQuestions - 1 ? (
405
+ <span className="font-medium">Finish test to submit</span>
406
+ ) : (
407
+ <span>{currentIndex + 1} of {totalQuestions}</span>
408
+ )}
409
+ </div>
410
+ </div>
411
+ </div>
412
+ </div>
413
+
414
+ {/* Right: Sidebar (4 cols) */}
415
+ <aside className="col-span-12 lg:col-span-4">
416
+ <div className="sticky top-6 space-y-4">
417
+ {/* Timer / top card */}
418
+ <div className="bg-white rounded-xl p-4 shadow border border-slate-200 text-center">
419
+ <div className="text-sm text-gray-500">Time Left</div>
420
+ <div className="mt-2 text-3xl font-bold text-red-600">
421
+ {formatTime(Math.max(0, timeLeft))}
422
+ </div>
423
+ <div className="mt-3 text-xs text-gray-500">Auto-submit when timer ends</div>
424
+ </div>
425
+
426
+ {/* Palette Grid */}
427
+ <div className="bg-white rounded-xl p-4 shadow border border-slate-200">
428
+ <div className="flex items-center justify-between mb-3">
429
+ <h3 className="text-sm font-semibold">Question Palette</h3>
430
+ <div className="text-xs text-gray-500">Tap to jump</div>
431
+ </div>
432
+
433
+ <div className="grid grid-cols-5 gap-2">
434
+ {questions.map((q, idx) => (
435
+ <button
436
+ key={q.id}
437
+ onClick={() => goToQuestion(idx)}
438
+ className={`w-full aspect-square rounded-md flex items-center justify-center font-medium ${statusClassForPalette(
439
+ q,
440
+ idx
441
+ )}`}
442
+ title={`Q ${idx + 1} — ${q.status}`}
443
+ >
444
+ {idx + 1}
445
+ </button>
446
+ ))}
447
+ </div>
448
+
449
+ {/* Legend */}
450
+ <div className="mt-4 text-xs text-slate-600 grid grid-cols-2 gap-2">
451
+ <div className="flex items-center gap-2">
452
+ <span className="w-4 h-4 bg-blue-500 rounded" /> Current
453
+ </div>
454
+ <div className="flex items-center gap-2">
455
+ <span className="w-4 h-4 bg-green-500 rounded" /> Answered
456
+ </div>
457
+ <div className="flex items-center gap-2">
458
+ <span className="w-4 h-4 bg-yellow-400 rounded" /> Review
459
+ </div>
460
+ <div className="flex items-center gap-2">
461
+ <span className="w-4 h-4 bg-red-500 rounded" /> Visited
462
+ </div>
463
+ <div className="flex items-center gap-2">
464
+ <span className="w-4 h-4 bg-slate-300 rounded" /> Not
465
+ Visited
466
+ </div>
467
+ </div>
468
+ </div>
469
+
470
+ {/* Submit button */}
471
+ <div className="bg-white rounded-xl p-4 shadow border border-slate-200 flex gap-3">
472
+ <button
473
+ onClick={() => {
474
+ // quick confirm before submitting
475
+ // In real app, you'd show a modal
476
+ if (window.confirm("Submit test now?")) {
477
+ handleSubmit();
478
+ }
479
+ }}
480
+ className="flex-1 px-4 py-2 rounded-md bg-red-600 text-white font-semibold"
481
+ >
482
+ Submit Test
483
+ </button>
484
+
485
+ <button
486
+ onClick={() => {
487
+ // jump to first unanswered
488
+ const idx = questions.findIndex((q) => q.status === "notVisited" || q.status === "visited");
489
+ if (idx >= 0) goToQuestion(idx);
490
+ else alert("No unanswered questions left.");
491
+ }}
492
+ className="px-3 py-2 rounded-md border border-slate-200 text-slate-700"
493
+ >
494
+ Next Unanswered
495
+ </button>
496
+ </div>
497
+ </div>
498
+ </aside>
499
+ </div>
500
+ </div>
501
+ );
502
+ };
503
+
504
+ export default MCQQuizPage;
Frontend/src/pages/quize.tsx CHANGED
@@ -1,125 +1,147 @@
1
- import React from "react";
2
- import { FileText, FileSpreadsheet, Code2, ListChecks, UploadCloud, Type } from "lucide-react";
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
- // The structure of this component is heavily modified to meet the new layout requirements.
5
  const ResumeGeneratedQuize: React.FC = () => {
6
-
7
- // Consistent purple button style
8
- const buttonClass = "w-full py-3 rounded-lg bg-gradient-to-r from-blue-500 to-blue-700 hover:from-blue-400 hover:to-blue-600 font-medium transition shadow-lg shadow-blue-500/30 text-white mt-4";
9
-
10
- const OutputTypeOption = ({ icon, title, desc }: { icon: React.ReactNode; title: string; desc: string }) => (
11
- // These cards inherently take full width of their parent container (which is now the right column)
12
- <div className="p-4 bg-slate-900/50 rounded-xl border border-slate-700 hover:bg-slate-800/60 transition cursor-pointer">
13
- <div className="flex items-center gap-3 mb-2">
14
- {icon}
15
- <h4 className="text-xl font-semibold text-gray-50">{title}</h4>
16
- </div>
17
- <p className="text-gray-400 text-sm">{desc}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  </div>
19
- );
20
-
21
- const SourceCard = ({ icon, title, desc, actionText }: { icon: React.ReactNode; title: string; desc: string; actionText: string }) => (
22
- <div className="p-6 bg-slate-900/60 rounded-2xl shadow-xl border border-slate-700 hover:bg-slate-800/70 transition flex flex-col h-full">
23
- <div className="flex items-start gap-4 mb-4">
24
- {icon}
25
- <div>
26
- <h2 className="text-2xl font-bold">{title}</h2>
27
- <p className="text-gray-400 mt-1 text-sm">{desc}</p>
28
- </div>
29
- </div>
30
- {/* The SourceCard button width is intentionally set to w-fit px-6 to be narrower */}
31
- <button className={buttonClass.replace('w-full', 'w-fit px-6')} >
32
- {actionText}
33
- </button>
 
 
 
 
 
 
 
 
 
 
 
34
  </div>
35
- );
36
-
37
- return (
38
- <div className="min-h-screen bg-black text-white py-20 px-6 lg:px-12">
39
-
40
- {/* Page Title */}
41
- <h1 className="text-4xl font-bold mb-16 text-center">
42
- Smart AI-Powered Quiz Generator
43
- </h1>
44
-
45
- {/* ===== MAIN TWO-COLUMN LAYOUT (Source Selection vs. Configuration) ===== */}
46
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
47
-
48
- {/* --- LEFT SIDE: INPUT SOURCE SELECTION --- */}
49
- <div>
50
- <h3 className="text-3xl font-bold mb-6 text-gray-100 border-b border-blue-700 pb-2">
51
- 1. Select Quiz Source
52
- </h3>
53
- <div className="space-y-8">
54
- {/* Consolidated Resume/Note PDF Upload Card */}
55
- <SourceCard
56
- icon={<FileSpreadsheet className="w-8 h-8 text-cyan-400" />}
57
- title="Resume/Note PDF Upload"
58
- desc="Generate quizzes based on skills and experience listed in your resume file."
59
- actionText="Upload"
60
- />
61
- </div>
62
- </div>
63
-
64
-
65
- {/* --- RIGHT SIDE: PROMPT & OUTPUT CONFIGURATION (The full width section) --- */}
66
- <div className="lg:sticky lg:top-8 self-start">
67
- <h3 className="text-3xl font-bold mb-6 text-gray-100 border-b border-blue-700 pb-2">
68
- 2. Configure & Generate
69
- </h3>
70
-
71
- {/* Prompt/Text Area Section */}
72
- <div className="bg-slate-900/60 rounded-2xl shadow-2xl p-6 border border-slate-800 mb-8">
73
- <label htmlFor="prompt-area" className="text-lg font-semibold block mb-3 text-gray-200">
74
- Custom Prompt/Instructions (Optional)
75
- </label>
76
- <textarea
77
- id="prompt-area"
78
- rows={4}
79
- placeholder="e.g., 'Focus only on Python and OOP concepts' or 'Generate a quiz on the content uploaded in step 1'"
80
- className="w-full p-3 rounded-lg bg-black/40 border border-slate-700 text-gray-100 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-600 transition"
81
- ></textarea>
82
- <p className="text-gray-500 text-sm mt-2">
83
- This prompt refines the quiz generation based on the selected source.
84
- </p>
85
- </div>
86
 
87
- {/* Output Type Section - NOW CORRECTLY PLACED INSIDE THE RIGHT COLUMN */}
88
- </div>
89
-
90
- </div>
91
-
92
- <div className="lg:sticky lg:top-8 self-start">
93
- <h4 className="text-2xl font-semibold mb-4 text-gray-200">
94
- Choose Output Type:
95
- </h4>
96
-
97
- {/* NEW: Use grid-cols-2 and gap-4 to place the options side-by-side */}
98
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
99
-
100
- {/* MCQ Quiz Option - takes full width of its column */}
101
- <OutputTypeOption
102
- icon={<ListChecks className="w-6 h-6 text-blue-400" />}
103
- title="Multiple Choice Quiz (MCQ)"
104
- desc="Ideal for quick assessment of knowledge and comprehension."
105
- />
106
-
107
- {/* Coding Quiz Option - takes full width of its column */}
108
- <OutputTypeOption
109
- icon={<Code2 className="w-6 h-6 text-blue-400" />}
110
- title="Coding Challenge Quiz"
111
- desc="Generates problems requiring code snippets or full functions for evaluation."
112
- />
113
- </div>
114
-
115
- {/* Main Generation Button - Full width (w-full is in buttonClass) */}
116
- <button className={buttonClass + ' text-xl mt-8'}>
117
- Generate Quiz Now
118
- </button>
119
-
120
- </div>
121
  </div>
122
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  };
124
 
125
  export default ResumeGeneratedQuize;
 
1
+ import React, { useState } from "react";
2
+ import { FileSpreadsheet, Code2, ListChecks } from "lucide-react";
3
+ import MCQQuizPage from "../components/quize/mcq";
4
+
5
+ const CodingQuizPage = ({ onBack }: any) => (
6
+ <div className="min-h-screen bg-black text-white flex flex-col items-center justify-center">
7
+ <h1 className="text-4xl font-bold mb-6">Coding Quiz Coming Soon...</h1>
8
+ <button
9
+ onClick={onBack}
10
+ className="px-6 py-3 bg-blue-600 rounded-lg text-white"
11
+ >
12
+ ← Back
13
+ </button>
14
+ </div>
15
+ );
16
 
 
17
  const ResumeGeneratedQuize: React.FC = () => {
18
+ const [showQuiz, setShowQuiz] = useState(false);
19
+ const [quizType, setQuizType] = useState<"mcq" | "coding" | null>(null);
20
+
21
+ const buttonClass =
22
+ "w-full py-3 rounded-lg bg-gradient-to-r from-blue-500 to-blue-700 hover:from-blue-400 hover:to-blue-600 font-medium transition shadow-lg shadow-blue-500/30 text-white mt-4";
23
+
24
+ // Load Quiz Page
25
+ if (showQuiz) {
26
+ if (quizType === "mcq")
27
+ return <MCQQuizPage onBack={() => setShowQuiz(false)} />;
28
+
29
+ if (quizType === "coding")
30
+ return <CodingQuizPage onBack={() => setShowQuiz(false)} />;
31
+ }
32
+
33
+ const OutputTypeOption = ({ icon, title, desc, value }: any) => (
34
+ <div
35
+ onClick={() => setQuizType(value)}
36
+ className={`p-4 rounded-xl border transition cursor-pointer
37
+ ${
38
+ quizType === value
39
+ ? "bg-blue-600/30 border-blue-500 shadow-lg shadow-blue-500/30"
40
+ : "bg-slate-900/50 border-slate-700 hover:bg-slate-800/60"
41
+ }
42
+ `}
43
+ >
44
+ <div className="flex items-center gap-3 mb-2">
45
+ {icon}
46
+ <h4 className="text-xl font-semibold text-gray-50">{title}</h4>
47
+ </div>
48
+ <p className="text-gray-400 text-sm">{desc}</p>
49
+ </div>
50
+ );
51
+
52
+ const SourceCard = ({ icon, title, desc, actionText }: any) => (
53
+ <div className="p-6 bg-slate-900/60 rounded-2xl shadow-xl border border-slate-700 hover:bg-slate-800/70 transition flex flex-col h-full">
54
+ <div className="flex items-start gap-4 mb-4">
55
+ {icon}
56
+ <div>
57
+ <h2 className="text-2xl font-bold">{title}</h2>
58
+ <p className="text-gray-400 mt-1 text-sm">{desc}</p>
59
  </div>
60
+ </div>
61
+ <button className={buttonClass.replace("w-full", "w-fit px-6")}>
62
+ {actionText}
63
+ </button>
64
+ </div>
65
+ );
66
+
67
+ return (
68
+ <div className="min-h-screen bg-black text-white py-20 px-6 lg:px-12">
69
+ <h1 className="text-4xl font-bold mb-16 text-center">
70
+ Smart AI-Powered Quiz Generator
71
+ </h1>
72
+
73
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
74
+ <div>
75
+ <h3 className="text-3xl font-bold mb-6 text-gray-100 border-b border-blue-700 pb-2">
76
+ 1. Select Quiz Source
77
+ </h3>
78
+ <div className="space-y-8">
79
+ <SourceCard
80
+ icon={<FileSpreadsheet className="w-8 h-8 text-cyan-400" />}
81
+ title="Resume/Note PDF Upload"
82
+ desc="Generate quizzes based on your uploaded resume or notes."
83
+ actionText="Upload"
84
+ />
85
+ </div>
86
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
+ <div className="lg:sticky lg:top-8 self-start">
89
+ <h3 className="text-3xl font-bold mb-6 text-gray-100 border-b border-blue-700 pb-2">
90
+ 2. Configure & Generate
91
+ </h3>
92
+
93
+ <div className="bg-slate-900/60 rounded-2xl shadow-2xl p-6 border border-slate-800 mb-8">
94
+ <label className="text-lg font-semibold block mb-3 text-gray-200">
95
+ Custom Prompt/Instructions (Optional)
96
+ </label>
97
+ <textarea
98
+ rows={4}
99
+ placeholder="e.g., 'Focus on Python only'"
100
+ className="w-full p-3 rounded-lg bg-black/40 border border-slate-700 text-gray-100 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-600 transition"
101
+ ></textarea>
102
+ <p className="text-gray-500 text-sm mt-2">
103
+ This prompt influences the quiz generation.
104
+ </p>
105
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  </div>
107
+ </div>
108
+
109
+ <div className="lg:sticky lg:top-8 self-start mt-8">
110
+ <h4 className="text-2xl font-semibold mb-4 text-gray-200">
111
+ Choose Output Type:
112
+ </h4>
113
+
114
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
115
+ <OutputTypeOption
116
+ value="mcq"
117
+ icon={<ListChecks className="w-6 h-6 text-blue-400" />}
118
+ title="Multiple Choice Quiz (MCQ)"
119
+ desc="Ideal for quick assessment."
120
+ />
121
+ <OutputTypeOption
122
+ value="coding"
123
+ icon={<Code2 className="w-6 h-6 text-blue-400" />}
124
+ title="Coding Challenge Quiz"
125
+ desc="Generate code-based evaluation tasks."
126
+ />
127
+ </div>
128
+
129
+ <button
130
+ className={
131
+ buttonClass +
132
+ " text-xl mt-8 " +
133
+ (!quizType
134
+ ? "opacity-50 cursor-not-allowed"
135
+ : "opacity-100 cursor-pointer")
136
+ }
137
+ disabled={!quizType}
138
+ onClick={() => setShowQuiz(true)}
139
+ >
140
+ Generate Quiz Now
141
+ </button>
142
+ </div>
143
+ </div>
144
+ );
145
  };
146
 
147
  export default ResumeGeneratedQuize;