Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
H
Hire-Guru
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Rishikumar
Hire-Guru
Commits
d2648061
Commit
d2648061
authored
Apr 15, 2026
by
AravindR-K
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Fixed quiz genreation bugs + editing bugs
parent
1d54e11a
Changes
14
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
885 additions
and
759 deletions
+885
-759
Backend/.env
Backend/.env
+1
-1
Backend/models/Quiz.js
Backend/models/Quiz.js
+3
-3
Backend/package.json
Backend/package.json
+1
-1
Backend/routes/admin.js
Backend/routes/admin.js
+532
-488
Backend/routes/auth.js
Backend/routes/auth.js
+1
-1
Backend/server.js
Backend/server.js
+77
-77
Frontend/src/app/interceptors/auth.interceptor.ts
Frontend/src/app/interceptors/auth.interceptor.ts
+1
-1
Frontend/src/app/pages/admin/edit-quiz/edit-quiz.ts
Frontend/src/app/pages/admin/edit-quiz/edit-quiz.ts
+42
-23
Frontend/src/app/pages/admin/generate-quiz/generate-quiz.html
...tend/src/app/pages/admin/generate-quiz/generate-quiz.html
+64
-36
Frontend/src/app/pages/admin/generate-quiz/generate-quiz.ts
Frontend/src/app/pages/admin/generate-quiz/generate-quiz.ts
+25
-5
Frontend/src/app/pages/admin/user-history/user-history.css
Frontend/src/app/pages/admin/user-history/user-history.css
+1
-1
Frontend/src/app/services/auth.service.ts
Frontend/src/app/services/auth.service.ts
+8
-8
Frontend/src/app/services/quiz.service.ts
Frontend/src/app/services/quiz.service.ts
+114
-114
Frontend/src/app/services/theme.service.ts
Frontend/src/app/services/theme.service.ts
+15
-0
No files found.
Backend/.env
View file @
d2648061
...
@@ -2,4 +2,4 @@ PORT=5000
...
@@ -2,4 +2,4 @@ PORT=5000
#MONGODB_URI=mongodb://localhost:27017/quiz_app
#MONGODB_URI=mongodb://localhost:27017/quiz_app
MONGODB_URI=mongodb://127.0.0.1:27017/quiz_app
MONGODB_URI=mongodb://127.0.0.1:27017/quiz_app
JWT_SECRET=quiz_app_super_secret_key_2026
JWT_SECRET=quiz_app_super_secret_key_2026
JWT_EXPIRES_IN=
7
d
JWT_EXPIRES_IN=
1
d
Backend/models/Quiz.js
View file @
d2648061
...
@@ -43,10 +43,10 @@ const quizSchema = new mongoose.Schema({
...
@@ -43,10 +43,10 @@ const quizSchema = new mongoose.Schema({
}],
}],
difficulty
:
{
difficulty
:
{
type
:
String
,
type
:
String
,
enum
:
[
'
Beginner
'
,
'
Intermediate
'
,
'
Advance
d
'
],
enum
:
[
'
easy
'
,
'
medium
'
,
'
har
d
'
],
default
:
'
Intermediate
'
default
:
'
medium
'
},
},
topic
:
{
topic
:
{
type
:
String
,
type
:
String
,
trim
:
true
trim
:
true
},
},
...
...
Backend/package.json
View file @
d2648061
...
@@ -4,7 +4,7 @@
...
@@ -4,7 +4,7 @@
"main"
:
"server.js"
,
"main"
:
"server.js"
,
"scripts"
:
{
"scripts"
:
{
"start"
:
"node server.js"
,
"start"
:
"node server.js"
,
"dev"
:
"nodemon server.js"
,
"dev"
:
"nodemon
--ignore uploads/
server.js"
,
"seed"
:
"node seedAdmin.js"
,
"seed"
:
"node seedAdmin.js"
,
"test"
:
"echo
\"
Error: no test specified
\"
&& exit 1"
"test"
:
"echo
\"
Error: no test specified
\"
&& exit 1"
},
},
...
...
Backend/routes/admin.js
View file @
d2648061
This diff is collapsed.
Click to expand it.
Backend/routes/auth.js
View file @
d2648061
...
@@ -7,7 +7,7 @@ const router = express.Router();
...
@@ -7,7 +7,7 @@ const router = express.Router();
// Generate JWT Token
// Generate JWT Token
const
generateToken
=
(
id
)
=>
{
const
generateToken
=
(
id
)
=>
{
return
jwt
.
sign
({
id
},
process
.
env
.
JWT_SECRET
,
{
return
jwt
.
sign
({
id
},
process
.
env
.
JWT_SECRET
,
{
expiresIn
:
process
.
env
.
JWT_EXPIRES_IN
||
'
7
d
'
expiresIn
:
process
.
env
.
JWT_EXPIRES_IN
||
'
1
d
'
});
});
};
};
...
...
Backend/server.js
View file @
d2648061
const
express
=
require
(
'
express
'
);
const
express
=
require
(
'
express
'
);
const
cors
=
require
(
'
cors
'
);
const
cors
=
require
(
'
cors
'
);
const
cookieParser
=
require
(
'
cookie-parser
'
);
const
cookieParser
=
require
(
'
cookie-parser
'
);
const
dotenv
=
require
(
'
dotenv
'
);
const
dotenv
=
require
(
'
dotenv
'
);
const
connectDB
=
require
(
'
./config/db
'
);
const
connectDB
=
require
(
'
./config/db
'
);
// Load env vars
// Load env vars
dotenv
.
config
();
dotenv
.
config
();
// Connect to database
// Connect to database
connectDB
();
connectDB
();
const
app
=
express
();
const
app
=
express
();
// Middleware
// Middleware
const
allowedOrigins
=
[
const
allowedOrigins
=
[
'
http://localhost:4200
'
,
'
http://localhost:4200
'
,
'
http://localhost:3000
'
,
'
http://localhost:3000
'
,
process
.
env
.
FRONTEND_URL
process
.
env
.
FRONTEND_URL
].
filter
(
Boolean
);
].
filter
(
Boolean
);
app
.
use
(
cors
({
app
.
use
(
cors
({
origin
:
function
(
origin
,
callback
)
{
origin
:
function
(
origin
,
callback
)
{
// Allow requests with no origin (mobile apps, curl, Postman, server-to-server)
// Allow requests with no origin (mobile apps, curl, Postman, server-to-server)
if
(
!
origin
)
return
callback
(
null
,
true
);
if
(
!
origin
)
return
callback
(
null
,
true
);
// Allow any vercel.app previews and production deployments
// Allow any vercel.app previews and production deployments
if
(
origin
.
endsWith
(
'
.vercel.app
'
)
||
origin
.
includes
(
'
vercel.app
'
))
{
if
(
origin
.
endsWith
(
'
.vercel.app
'
)
||
origin
.
includes
(
'
vercel.app
'
))
{
return
callback
(
null
,
true
);
return
callback
(
null
,
true
);
}
// Allow explicitly listed origins
if
(
allowedOrigins
.
includes
(
origin
))
{
return
callback
(
null
,
true
);
}
// Block everything else
console
.
warn
(
`CORS blocked origin:
${
origin
}
`
);
return
callback
(
new
Error
(
`CORS policy: origin
${
origin
}
not allowed`
),
false
);
},
credentials
:
true
,
methods
:
[
'
GET
'
,
'
POST
'
,
'
PUT
'
,
'
DELETE
'
,
'
OPTIONS
'
],
allowedHeaders
:
[
'
Content-Type
'
,
'
Authorization
'
]
}));
app
.
use
(
express
.
json
());
app
.
use
(
express
.
urlencoded
({
extended
:
true
}));
app
.
use
(
cookieParser
());
// Routes
app
.
use
(
'
/api/auth
'
,
require
(
'
./routes/auth
'
));
app
.
use
(
'
/api/admin
'
,
require
(
'
./routes/admin
'
));
app
.
use
(
'
/api/hr
'
,
require
(
'
./routes/hr
'
));
app
.
use
(
'
/api/candidate
'
,
require
(
'
./routes/candidate
'
));
// Keep backward compatibility for old student endpoints
app
.
use
(
'
/api/student
'
,
require
(
'
./routes/candidate
'
));
// Health check
app
.
get
(
'
/api/health
'
,
(
req
,
res
)
=>
{
res
.
json
({
status
:
'
OK
'
,
message
:
'
QuizMaster Pro API is running
'
});
});
// Error handling middleware
app
.
use
((
err
,
req
,
res
,
next
)
=>
{
console
.
error
(
err
.
stack
);
if
(
err
.
name
===
'
MulterError
'
)
{
if
(
err
.
code
===
'
LIMIT_FILE_SIZE
'
)
{
return
res
.
status
(
400
).
json
({
message
:
'
File size exceeds 5MB limit
'
});
}
return
res
.
status
(
400
).
json
({
message
:
err
.
message
});
}
}
// Allow explicitly listed origins
res
.
status
(
500
).
json
({
message
:
'
Something went wrong!
'
,
error
:
err
.
message
});
if
(
allowedOrigins
.
includes
(
origin
))
{
});
return
callback
(
null
,
true
);
}
// Block everything else
console
.
warn
(
`CORS blocked origin:
${
origin
}
`
);
return
callback
(
new
Error
(
`CORS policy: origin
${
origin
}
not allowed`
),
false
);
},
credentials
:
true
,
methods
:
[
'
GET
'
,
'
POST
'
,
'
PUT
'
,
'
DELETE
'
,
'
OPTIONS
'
],
allowedHeaders
:
[
'
Content-Type
'
,
'
Authorization
'
]
}));
app
.
use
(
express
.
json
());
app
.
use
(
express
.
urlencoded
({
extended
:
true
}));
app
.
use
(
cookieParser
());
// Routes
app
.
use
(
'
/api/auth
'
,
require
(
'
./routes/auth
'
));
app
.
use
(
'
/api/admin
'
,
require
(
'
./routes/admin
'
));
app
.
use
(
'
/api/hr
'
,
require
(
'
./routes/hr
'
));
app
.
use
(
'
/api/candidate
'
,
require
(
'
./routes/candidate
'
));
// Keep backward compatibility for old student endpoints
app
.
use
(
'
/api/student
'
,
require
(
'
./routes/candidate
'
));
// Health check
app
.
get
(
'
/api/health
'
,
(
req
,
res
)
=>
{
res
.
json
({
status
:
'
OK
'
,
message
:
'
QuizMaster Pro API is running
'
});
});
// Error handling middleware
app
.
use
((
err
,
req
,
res
,
next
)
=>
{
console
.
error
(
err
.
stack
);
if
(
err
.
name
===
'
MulterError
'
)
{
if
(
err
.
code
===
'
LIMIT_FILE_SIZE
'
)
{
return
res
.
status
(
400
).
json
({
message
:
'
File size exceeds 5MB limit
'
});
}
return
res
.
status
(
400
).
json
({
message
:
err
.
message
});
}
res
.
status
(
500
).
json
({
message
:
'
Something went wrong!
'
,
error
:
err
.
message
});
});
const
PORT
=
process
.
env
.
PORT
||
5000
;
const
PORT
=
process
.
env
.
PORT
||
5000
;
app
.
listen
(
PORT
,
()
=>
{
app
.
listen
(
PORT
,
()
=>
{
console
.
log
(
`Server running on port
${
PORT
}
`
);
console
.
log
(
`Server running on port
${
PORT
}
`
);
});
});
Frontend/src/app/interceptors/auth.interceptor.ts
View file @
d2648061
import
{
HttpInterceptorFn
}
from
'
@angular/common/http
'
;
import
{
HttpInterceptorFn
}
from
'
@angular/common/http
'
;
export
const
authInterceptor
:
HttpInterceptorFn
=
(
req
,
next
)
=>
{
export
const
authInterceptor
:
HttpInterceptorFn
=
(
req
,
next
)
=>
{
const
token
=
localStorage
.
getItem
(
'
token
'
);
const
token
=
sessionStorage
.
getItem
(
'
token
'
);
if
(
token
)
{
if
(
token
)
{
const
cloned
=
req
.
clone
({
const
cloned
=
req
.
clone
({
...
...
Frontend/src/app/pages/admin/edit-quiz/edit-quiz.ts
View file @
d2648061
...
@@ -56,34 +56,53 @@ export class EditQuizComponent implements OnInit {
...
@@ -56,34 +56,53 @@ export class EditQuizComponent implements OnInit {
}
}
});
});
}
}
onSave
():
void
{
if
(
!
this
.
title
.
trim
())
{
this
.
error
.
set
(
'
Title is required
'
);
return
;
}
this
.
saving
.
set
(
true
);
this
.
error
.
set
(
''
);
onSave
():
void
{
// 🔥 Format questions correctly
if
(
!
this
.
title
.
trim
())
{
this
.
error
.
set
(
'
Title is required
'
);
return
;
}
const
formattedQuestions
=
this
.
questions
().
map
(
q
=>
{
this
.
saving
.
set
(
true
);
const
options
=
q
.
options
;
this
.
error
.
set
(
''
);
const
data
=
{
const
correctIndex
=
options
.
findIndex
(
title
:
this
.
title
,
(
opt
:
string
)
=>
opt
==
q
.
correctAnswer
timer
:
this
.
timer
,
);
category
:
this
.
category
,
difficulty
:
this
.
difficulty
,
return
{
topic
:
this
.
topic
,
question
:
q
.
question
,
questions
:
this
.
questions
()
options
,
correctAnswers
:
[
correctIndex
.
toString
()],
type
:
'
single
'
};
};
});
// ✅ closes map()
this
.
quizService
.
updateQuiz
(
this
.
quizId
,
data
).
subscribe
({
// 🔥 Final data
next
:
()
=>
{
const
data
=
{
this
.
saving
.
set
(
false
);
title
:
this
.
title
,
this
.
success
.
set
(
'
Quiz updated successfully!
'
);
timer
:
this
.
timer
,
setTimeout
(()
=>
this
.
router
.
navigate
([
'
/admin/quizzes
'
]),
1200
);
category
:
this
.
category
,
},
difficulty
:
this
.
difficulty
,
error
:
(
err
)
=>
{
topic
:
this
.
topic
,
this
.
saving
.
set
(
false
);
questions
:
formattedQuestions
this
.
error
.
set
(
err
.
error
?.
message
||
'
Failed to update quiz
'
);
};
// ✅ closes data object
}
});
}
this
.
quizService
.
updateQuiz
(
this
.
quizId
,
data
).
subscribe
({
next
:
()
=>
{
this
.
saving
.
set
(
false
);
this
.
success
.
set
(
'
Quiz updated successfully!
'
);
setTimeout
(()
=>
this
.
router
.
navigate
([
'
/admin/quizzes
'
]),
1200
);
},
error
:
(
err
)
=>
{
this
.
saving
.
set
(
false
);
this
.
error
.
set
(
err
.
error
?.
message
||
'
Failed to update quiz
'
);
}
});
// ✅ closes subscribe
}
// ✅ closes function
updateQuestion
(
index
:
number
,
field
:
string
,
value
:
any
):
void
{
updateQuestion
(
index
:
number
,
field
:
string
,
value
:
any
):
void
{
const
q
=
[...
this
.
questions
()];
const
q
=
[...
this
.
questions
()];
q
[
index
]
=
{
...
q
[
index
],
[
field
]:
value
};
q
[
index
]
=
{
...
q
[
index
],
[
field
]:
value
};
...
...
Frontend/src/app/pages/admin/generate-quiz/generate-quiz.html
View file @
d2648061
<div
class=
"dashboard-layout"
>
<div
class=
"dashboard-layout"
>
<aside
class=
"sidebar"
>
<aside
class=
"sidebar"
>
<div
class=
"sidebar-header"
>
<div
class=
"sidebar-header"
>
<span
class=
"logo-icon"
>
📝
</span><h2>
QuizMaster
</h2><span
class=
"role-badge"
>
Admin
</span>
<span
class=
"logo-icon"
>
📝
</span>
<h2>
QuizMaster
</h2><span
class=
"role-badge"
>
Admin
</span>
</div>
</div>
<nav
class=
"sidebar-nav"
>
<nav
class=
"sidebar-nav"
>
<a
routerLink=
"/admin/dashboard"
class=
"nav-item"
><span
class=
"nav-icon"
>
🏠
</span><span>
Dashboard
</span></a>
<a
routerLink=
"/admin/dashboard"
class=
"nav-item"
><span
class=
"nav-icon"
>
🏠
</span><span>
Dashboard
</span></a>
<a
routerLink=
"/admin/users"
class=
"nav-item"
><span
class=
"nav-icon"
>
👥
</span><span>
Users
</span></a>
<a
routerLink=
"/admin/users"
class=
"nav-item"
><span
class=
"nav-icon"
>
👥
</span><span>
Users
</span></a>
<a
routerLink=
"/admin/generate-quiz"
class=
"nav-item active"
><span
class=
"nav-icon"
>
➕
</span><span>
Generate Quiz
</span></a>
<a
routerLink=
"/admin/generate-quiz"
class=
"nav-item active"
><span
class=
"nav-icon"
>
➕
</span><span>
Generate
Quiz
</span></a>
</nav>
</nav>
<div
class=
"sidebar-footer"
>
<div
class=
"sidebar-footer"
>
<div
class=
"user-info"
>
<div
class=
"user-info"
>
<div
class=
"user-avatar"
>
{{ authService.currentUser()?.name?.charAt(0) || 'A' }}
</div>
<div
class=
"user-avatar"
>
{{ authService.currentUser()?.name?.charAt(0) || 'A' }}
</div>
<div
class=
"user-details"
><span
class=
"user-name"
>
{{ authService.currentUser()?.name }}
</span><span
class=
"user-email"
>
{{ authService.currentUser()?.email }}
</span></div>
<div
class=
"user-details"
><span
class=
"user-name"
>
{{ authService.currentUser()?.name }}
</span><span
class=
"user-email"
>
{{ authService.currentUser()?.email }}
</span></div>
</div>
</div>
<button
class=
"logout-btn"
(click)=
"logout()"
><span>
🚪
</span>
Logout
</button>
<button
class=
"logout-btn"
(click)=
"logout()"
><span>
🚪
</span>
Logout
</button>
</div>
</div>
...
@@ -24,10 +27,10 @@
...
@@ -24,10 +27,10 @@
</div>
</div>
@if (success()) {
@if (success()) {
<div
class=
"alert alert-success"
>
✅ {{ success() }}
</div>
<div
class=
"alert alert-success"
>
✅ {{ success() }}
</div>
}
}
@if (error()) {
@if (error()) {
<div
class=
"alert alert-error"
>
⚠️ {{ error() }}
</div>
<div
class=
"alert alert-error"
>
⚠️ {{ error() }}
</div>
}
}
<div
class=
"create-section"
>
<div
class=
"create-section"
>
...
@@ -35,7 +38,8 @@
...
@@ -35,7 +38,8 @@
<div
class=
"form-row"
>
<div
class=
"form-row"
>
<div
class=
"form-group"
>
<div
class=
"form-group"
>
<label
for=
"title"
>
Quiz Name
</label>
<label
for=
"title"
>
Quiz Name
</label>
<input
type=
"text"
id=
"title"
[(ngModel)]=
"title"
name=
"title"
placeholder=
"e.g., JavaScript Fundamentals"
/>
<input
type=
"text"
id=
"title"
[(ngModel)]=
"title"
name=
"title"
placeholder=
"e.g., JavaScript Fundamentals"
/>
</div>
</div>
<div
class=
"form-group"
>
<div
class=
"form-group"
>
<label
for=
"timer"
>
Timer (minutes)
</label>
<label
for=
"timer"
>
Timer (minutes)
</label>
...
@@ -48,53 +52,77 @@
...
@@ -48,53 +52,77 @@
<div
class=
"file-upload"
(click)=
"fileInput.click()"
>
<div
class=
"file-upload"
(click)=
"fileInput.click()"
>
<input
#fileInput
type=
"file"
accept=
".xlsx,.xls"
(change)=
"onFileSelected($event)"
hidden
/>
<input
#fileInput
type=
"file"
accept=
".xlsx,.xls"
(change)=
"onFileSelected($event)"
hidden
/>
@if (fileName()) {
@if (fileName()) {
<span
class=
"file-icon"
>
📄
</span>
<span
class=
"file-icon"
>
📄
</span>
<span
class=
"file-name"
>
{{ fileName() }}
</span>
<span
class=
"file-name"
>
{{ fileName() }}
</span>
<span
class=
"file-change"
>
Change
</span>
<span
class=
"file-change"
>
Change
</span>
} @else {
} @else {
<span
class=
"upload-icon"
>
📤
</span>
<span
class=
"upload-icon"
>
📤
</span>
<span
class=
"upload-text"
>
Click to upload Excel file
</span>
<span
class=
"upload-text"
>
Click to upload Excel file
</span>
<span
class=
"upload-hint"
>
Format: Question | Option1 | Option2 | Option3 | Option4 | Correct
</span>
<span
class=
"upload-hint"
>
Format: Question | Option1 | Option2 | Option3 | Option4 | Correct
</span>
}
}
</div>
</div>
</div>
</div>
<button
type=
"submit"
class=
"btn btn-primary"
[disabled]=
"loading()"
>
<div
class=
"form-group"
>
<label>
Assign Quiz
</label>
<div
style=
"margin-bottom: 10px;"
>
<label>
<input
type=
"checkbox"
[checked]=
"assignToAll()"
(change)=
"assignToAll.set(!assignToAll())"
/>
Assign to All Users
</label>
</div>
@if (!assignToAll()) {
<div
class=
"group-list"
>
@for (group of groups(); track group._id) {
<label
style=
"display:block; margin-bottom:6px;"
>
<input
type=
"checkbox"
[value]=
"group.name"
(change)=
"onGroupToggle(group.name, $event)"
/>
{{ group.name }}
</label>
}
</div>
}
</div>
<button
mat-raised-button
color=
"primary"
type=
"submit"
class=
"btn btn-primary"
[disabled]=
"loading()"
>
@if (loading()) {
@if (loading()) {
<span
class=
"spinner"
></span>
Creating Quiz...
<span
class=
"spinner"
></span>
Creating Quiz...
} @else {
} @else {
🚀 Create Quiz
🚀 Create Quiz
}
}
</button>
</button>
</form>
</form>
</div>
</div>
<!-- Existing Quizzes -->
<!-- Existing Quizzes -->
<h2
class=
"section-title"
>
Existing Quizzes
</h2>
<h2
class=
"section-title"
>
Existing Quizzes
</h2>
@if (loadingQuizzes()) {
@if (loadingQuizzes()) {
<div
class=
"loading-state"
><div
class=
"loader"
></div></div>
<div
class=
"loading-state"
>
<div
class=
"loader"
></div>
</div>
} @else if (quizzes().length === 0) {
} @else if (quizzes().length === 0) {
<div
class=
"empty-state"
>
<div
class=
"empty-state"
>
<span
class=
"empty-icon"
>
📋
</span>
<span
class=
"empty-icon"
>
📋
</span>
<h3>
No quizzes yet
</h3>
<h3>
No quizzes yet
</h3>
<p>
Create your first quiz above
</p>
<p>
Create your first quiz above
</p>
</div>
</div>
} @else {
} @else {
<div
class=
"quiz-grid"
>
<div
class=
"quiz-grid"
>
@for (quiz of quizzes(); track quiz._id) {
@for (quiz of quizzes(); track quiz._id) {
<div
class=
"quiz-card"
>
<div
class=
"quiz-card"
>
<div
class=
"quiz-card-header"
>
<div
class=
"quiz-card-header"
>
<h4>
{{ quiz.title }}
</h4>
<h4>
{{ quiz.title }}
</h4>
<button
class=
"delete-btn"
(click)=
"deleteQuiz(quiz._id)"
>
🗑️
</button>
<button
class=
"delete-btn"
(click)=
"deleteQuiz(quiz._id)"
>
🗑️
</button>
</div>
</div>
<div
class=
"quiz-meta"
>
<div
class=
"quiz-meta"
>
<span>
⏱️ {{ quiz.timer }} min
</span>
<span>
⏱️ {{ quiz.timer }} min
</span>
<span>
📝 {{ quiz.totalQuestions }} questions
</span>
<span>
📝 {{ quiz.totalQuestions }} questions
</span>
</div>
</div>
<span
class=
"quiz-date"
>
Created {{ quiz.createdAt | date:'mediumDate' }}
</span>
<span
class=
"quiz-date"
>
Created {{ quiz.createdAt | date:'mediumDate' }}
</span>
</div>
}
</div>
</div>
}
</div>
}
}
</main>
</main>
</div>
</div>
\ No newline at end of file
Frontend/src/app/pages/admin/generate-quiz/generate-quiz.ts
View file @
d2648061
...
@@ -4,11 +4,12 @@ import { FormsModule } from '@angular/forms';
...
@@ -4,11 +4,12 @@ import { FormsModule } from '@angular/forms';
import
{
RouterLink
}
from
'
@angular/router
'
;
import
{
RouterLink
}
from
'
@angular/router
'
;
import
{
AuthService
}
from
'
../../../services/auth.service
'
;
import
{
AuthService
}
from
'
../../../services/auth.service
'
;
import
{
QuizService
}
from
'
../../../services/quiz.service
'
;
import
{
QuizService
}
from
'
../../../services/quiz.service
'
;
import
{
MatButtonModule
}
from
'
@angular/material/button
'
;
@
Component
({
@
Component
({
selector
:
'
app-generate-quiz
'
,
selector
:
'
app-generate-quiz
'
,
standalone
:
true
,
standalone
:
true
,
imports
:
[
CommonModule
,
FormsModule
,
RouterLink
],
imports
:
[
CommonModule
,
FormsModule
,
RouterLink
,
MatButtonModule
],
templateUrl
:
'
./generate-quiz.html
'
,
templateUrl
:
'
./generate-quiz.html
'
,
styleUrl
:
'
./generate-quiz.css
'
styleUrl
:
'
./generate-quiz.css
'
})
})
...
@@ -17,7 +18,9 @@ export class GenerateQuizComponent implements OnInit {
...
@@ -17,7 +18,9 @@ export class GenerateQuizComponent implements OnInit {
timer
=
30
;
timer
=
30
;
selectedFile
:
File
|
null
=
null
;
selectedFile
:
File
|
null
=
null
;
fileName
=
signal
<
string
>
(
''
);
fileName
=
signal
<
string
>
(
''
);
groups
=
signal
<
any
[]
>
([]);
selectedGroups
=
signal
<
string
[]
>
([]);
assignToAll
=
signal
<
boolean
>
(
true
);
loading
=
signal
<
boolean
>
(
false
);
loading
=
signal
<
boolean
>
(
false
);
success
=
signal
<
string
>
(
''
);
success
=
signal
<
string
>
(
''
);
error
=
signal
<
string
>
(
''
);
error
=
signal
<
string
>
(
''
);
...
@@ -25,11 +28,18 @@ export class GenerateQuizComponent implements OnInit {
...
@@ -25,11 +28,18 @@ export class GenerateQuizComponent implements OnInit {
quizzes
=
signal
<
any
[]
>
([]);
quizzes
=
signal
<
any
[]
>
([]);
loadingQuizzes
=
signal
<
boolean
>
(
true
);
loadingQuizzes
=
signal
<
boolean
>
(
true
);
constructor
(
public
authService
:
AuthService
,
private
quizService
:
QuizService
)
{}
constructor
(
public
authService
:
AuthService
,
private
quizService
:
QuizService
)
{
}
ngOnInit
():
void
{
ngOnInit
():
void
{
console
.
log
(
"
Component loaded
"
);
this
.
loadQuizzes
();
this
.
loadQuizzes
();
this
.
quizService
.
getAdminGroups
().
subscribe
({
next
:
(
res
)
=>
{
this
.
groups
.
set
(
res
.
groups
||
[]);
},
error
:
()
=>
{
console
.
log
(
"
Failed to load groups
"
);
}
});
}
}
loadQuizzes
():
void
{
loadQuizzes
():
void
{
...
@@ -72,7 +82,8 @@ export class GenerateQuizComponent implements OnInit {
...
@@ -72,7 +82,8 @@ export class GenerateQuizComponent implements OnInit {
formData
.
append
(
'
title
'
,
this
.
title
);
formData
.
append
(
'
title
'
,
this
.
title
);
formData
.
append
(
'
timer
'
,
this
.
timer
.
toString
());
formData
.
append
(
'
timer
'
,
this
.
timer
.
toString
());
formData
.
append
(
'
questionsFile
'
,
this
.
selectedFile
);
formData
.
append
(
'
questionsFile
'
,
this
.
selectedFile
);
formData
.
append
(
'
assignToAll
'
,
this
.
assignToAll
().
toString
());
formData
.
append
(
'
assignedGroups
'
,
JSON
.
stringify
(
this
.
selectedGroups
()));
this
.
quizService
.
createQuiz
(
formData
).
subscribe
({
this
.
quizService
.
createQuiz
(
formData
).
subscribe
({
next
:
(
res
)
=>
{
next
:
(
res
)
=>
{
this
.
loading
.
set
(
false
);
this
.
loading
.
set
(
false
);
...
@@ -98,6 +109,15 @@ export class GenerateQuizComponent implements OnInit {
...
@@ -98,6 +109,15 @@ export class GenerateQuizComponent implements OnInit {
});
});
}
}
}
}
onGroupToggle
(
groupName
:
string
,
event
:
any
):
void
{
const
selected
=
this
.
selectedGroups
();
if
(
event
.
target
.
checked
)
{
this
.
selectedGroups
.
set
([...
selected
,
groupName
]);
}
else
{
this
.
selectedGroups
.
set
(
selected
.
filter
(
g
=>
g
!==
groupName
));
}
}
logout
():
void
{
logout
():
void
{
this
.
authService
.
logout
();
this
.
authService
.
logout
();
...
...
Frontend/src/app/pages/admin/user-history/user-history.css
View file @
d2648061
...
@@ -148,7 +148,7 @@
...
@@ -148,7 +148,7 @@
.main-content
{
.main-content
{
flex
:
1
;
flex
:
1
;
margin-left
:
26
0px
;
margin-left
:
0px
;
padding
:
24px
32px
;
padding
:
24px
32px
;
}
}
...
...
Frontend/src/app/services/auth.service.ts
View file @
d2648061
...
@@ -31,8 +31,8 @@ export class AuthService {
...
@@ -31,8 +31,8 @@ export class AuthService {
}
}
private
loadUserFromStorage
():
void
{
private
loadUserFromStorage
():
void
{
const
token
=
local
Storage
.
getItem
(
'
token
'
);
const
token
=
session
Storage
.
getItem
(
'
token
'
);
const
user
=
localStorage
.
getItem
(
'
user
'
);
const
user
=
sessionStorage
.
getItem
(
'
user
'
);
if
(
token
&&
user
)
{
if
(
token
&&
user
)
{
this
.
currentUser
.
set
(
JSON
.
parse
(
user
));
this
.
currentUser
.
set
(
JSON
.
parse
(
user
));
this
.
isLoggedIn
.
set
(
true
);
this
.
isLoggedIn
.
set
(
true
);
...
@@ -49,7 +49,7 @@ export class AuthService {
...
@@ -49,7 +49,7 @@ export class AuthService {
}
}
logout
():
void
{
logout
():
void
{
const
token
=
localStorage
.
getItem
(
'
token
'
);
const
token
=
sessionStorage
.
getItem
(
'
token
'
);
if
(
token
)
{
if
(
token
)
{
this
.
http
.
post
(
`
${
this
.
apiUrl
}
/logout`
,
{}).
subscribe
({
this
.
http
.
post
(
`
${
this
.
apiUrl
}
/logout`
,
{}).
subscribe
({
complete
:
()
=>
this
.
clearAuth
(),
complete
:
()
=>
this
.
clearAuth
(),
...
@@ -62,23 +62,23 @@ export class AuthService {
...
@@ -62,23 +62,23 @@ export class AuthService {
private
handleAuth
(
res
:
AuthResponse
):
void
{
private
handleAuth
(
res
:
AuthResponse
):
void
{
if
(
res
.
token
)
{
if
(
res
.
token
)
{
local
Storage
.
setItem
(
'
token
'
,
res
.
token
);
session
Storage
.
setItem
(
'
token
'
,
res
.
token
);
}
}
localStorage
.
setItem
(
'
user
'
,
JSON
.
stringify
(
res
.
user
));
sessionStorage
.
setItem
(
'
user
'
,
JSON
.
stringify
(
res
.
user
));
this
.
currentUser
.
set
(
res
.
user
);
this
.
currentUser
.
set
(
res
.
user
);
this
.
isLoggedIn
.
set
(
true
);
this
.
isLoggedIn
.
set
(
true
);
}
}
private
clearAuth
():
void
{
private
clearAuth
():
void
{
local
Storage
.
removeItem
(
'
token
'
);
session
Storage
.
removeItem
(
'
token
'
);
local
Storage
.
removeItem
(
'
user
'
);
session
Storage
.
removeItem
(
'
user
'
);
this
.
currentUser
.
set
(
null
);
this
.
currentUser
.
set
(
null
);
this
.
isLoggedIn
.
set
(
false
);
this
.
isLoggedIn
.
set
(
false
);
this
.
router
.
navigate
([
'
/login
'
]);
this
.
router
.
navigate
([
'
/login
'
]);
}
}
getToken
():
string
|
null
{
getToken
():
string
|
null
{
return
localStorage
.
getItem
(
'
token
'
);
return
sessionStorage
.
getItem
(
'
token
'
);
}
}
getUserRole
():
string
|
null
{
getUserRole
():
string
|
null
{
...
...
Frontend/src/app/services/quiz.service.ts
View file @
d2648061
import
{
Injectable
}
from
'
@angular/core
'
;
import
{
Injectable
}
from
'
@angular/core
'
;
import
{
HttpClient
}
from
'
@angular/common/http
'
;
import
{
HttpClient
}
from
'
@angular/common/http
'
;
import
{
Observable
}
from
'
rxjs
'
;
import
{
Observable
}
from
'
rxjs
'
;
@
Injectable
({
@
Injectable
({
providedIn
:
'
root
'
providedIn
:
'
root
'
})
})
export
class
QuizService
{
export
class
QuizService
{
private
adminUrl
=
'
http://localhost:5000/api/admin
'
;
private
adminUrl
=
'
http://localhost:5000/api/admin
'
;
private
hrUrl
=
'
http://localhost:5000/api/hr
'
;
private
hrUrl
=
'
http://localhost:5000/api/hr
'
;
private
candidateUrl
=
'
http://localhost:5000/api/candidate
'
;
private
candidateUrl
=
'
http://localhost:5000/api/candidate
'
;
constructor
(
private
http
:
HttpClient
)
{}
constructor
(
private
http
:
HttpClient
)
{}
// ========== ADMIN ENDPOINTS ==========
// ========== ADMIN ENDPOINTS ==========
getAdminStats
():
Observable
<
any
>
{
getAdminStats
():
Observable
<
any
>
{
return
this
.
http
.
get
(
`
${
this
.
adminUrl
}
/stats`
);
return
this
.
http
.
get
(
`
${
this
.
adminUrl
}
/stats`
);
}
}
getUsers
(
role
?:
string
):
Observable
<
any
>
{
getUsers
(
role
?:
string
):
Observable
<
any
>
{
const
url
=
role
?
`
${
this
.
adminUrl
}
/users?role=
${
role
}
`
:
`
${
this
.
adminUrl
}
/users`
;
const
url
=
role
?
`
${
this
.
adminUrl
}
/users?role=
${
role
}
`
:
`
${
this
.
adminUrl
}
/users`
;
return
this
.
http
.
get
(
url
);
return
this
.
http
.
get
(
url
);
}
}
getLoggedInUsers
():
Observable
<
any
>
{
getLoggedInUsers
():
Observable
<
any
>
{
return
this
.
http
.
get
(
`
${
this
.
adminUrl
}
/users/logged-in`
);
return
this
.
http
.
get
(
`
${
this
.
adminUrl
}
/users/logged-in`
);
}
}
createHRUser
(
data
:
{
name
:
string
;
email
:
string
;
password
:
string
}):
Observable
<
any
>
{
createHRUser
(
data
:
{
name
:
string
;
email
:
string
;
password
:
string
}):
Observable
<
any
>
{
return
this
.
http
.
post
(
`
${
this
.
adminUrl
}
/users/create-hr`
,
data
);
return
this
.
http
.
post
(
`
${
this
.
adminUrl
}
/users/create-hr`
,
data
);
}
}
deleteUser
(
userId
:
string
):
Observable
<
any
>
{
deleteUser
(
userId
:
string
):
Observable
<
any
>
{
return
this
.
http
.
delete
(
`
${
this
.
adminUrl
}
/users/
${
userId
}
`
);
return
this
.
http
.
delete
(
`
${
this
.
adminUrl
}
/users/
${
userId
}
`
);
}
}
getUserHistory
(
userId
:
string
):
Observable
<
any
>
{
getUserHistory
(
userId
:
string
):
Observable
<
any
>
{
return
this
.
http
.
get
(
`
${
this
.
adminUrl
}
/users/
${
userId
}
/history`
);
return
this
.
http
.
get
(
`
${
this
.
adminUrl
}
/users/
${
userId
}
/history`
);
}
}
getSubmissionDetails
(
submissionId
:
string
):
Observable
<
any
>
{
getSubmissionDetails
(
submissionId
:
string
):
Observable
<
any
>
{
return
this
.
http
.
get
(
`
${
this
.
adminUrl
}
/submissions/
${
submissionId
}
`
);
return
this
.
http
.
get
(
`
${
this
.
adminUrl
}
/submissions/
${
submissionId
}
`
);
}
}
createQuiz
(
formData
:
FormData
):
Observable
<
any
>
{
createQuiz
(
formData
:
FormData
):
Observable
<
any
>
{
return
this
.
http
.
post
(
`
${
this
.
adminUrl
}
/quiz/create`
,
formData
);
return
this
.
http
.
post
(
`
${
this
.
adminUrl
}
/quiz/create`
,
formData
);
}
}
createQuizManual
(
data
:
any
):
Observable
<
any
>
{
createQuizManual
(
data
:
any
):
Observable
<
any
>
{
return
this
.
http
.
post
(
`
${
this
.
adminUrl
}
/quiz/create-manual`
,
data
);
return
this
.
http
.
post
(
`
${
this
.
adminUrl
}
/quiz/create-manual`
,
data
);
}
}
getAdminQuizzes
():
Observable
<
any
>
{
getAdminQuizzes
():
Observable
<
any
>
{
return
this
.
http
.
get
(
`
${
this
.
adminUrl
}
/quizzes`
);
return
this
.
http
.
get
(
`
${
this
.
adminUrl
}
/quizzes`
);
}
}
getAdminQuiz
(
quizId
:
string
):
Observable
<
any
>
{
getAdminQuiz
(
quizId
:
string
):
Observable
<
any
>
{
return
this
.
http
.
get
(
`
${
this
.
adminUrl
}
/quiz/
${
quizId
}
`
);
return
this
.
http
.
get
(
`
${
this
.
adminUrl
}
/quiz/
${
quizId
}
`
);
}
}
updateQuiz
(
quizId
:
string
,
data
:
any
):
Observable
<
any
>
{
updateQuiz
(
quizId
:
string
,
data
:
any
):
Observable
<
any
>
{
return
this
.
http
.
put
(
`
${
this
.
adminUrl
}
/quiz/
${
quizId
}
`
,
data
);
return
this
.
http
.
put
(
`
${
this
.
adminUrl
}
/quiz/
${
quizId
}
`
,
data
);
}
}
assignQuiz
(
quizId
:
string
,
data
:
any
):
Observable
<
any
>
{
assignQuiz
(
quizId
:
string
,
data
:
any
):
Observable
<
any
>
{
return
this
.
http
.
put
(
`
${
this
.
adminUrl
}
/quiz/
${
quizId
}
/assign`
,
data
);
return
this
.
http
.
put
(
`
${
this
.
adminUrl
}
/quiz/
${
quizId
}
/assign`
,
data
);
}
}
deleteQuiz
(
quizId
:
string
):
Observable
<
any
>
{
deleteQuiz
(
quizId
:
string
):
Observable
<
any
>
{
return
this
.
http
.
delete
(
`
${
this
.
adminUrl
}
/quiz/
${
quizId
}
`
);
return
this
.
http
.
delete
(
`
${
this
.
adminUrl
}
/quiz/
${
quizId
}
`
);
}
}
getAdminCategories
():
Observable
<
any
>
{
getAdminCategories
():
Observable
<
any
>
{
return
this
.
http
.
get
(
`
${
this
.
adminUrl
}
/categories`
);
return
this
.
http
.
get
(
`
${
this
.
adminUrl
}
/categories`
);
}
}
getAdminGroups
():
Observable
<
any
>
{
getAdminGroups
():
Observable
<
any
>
{
return
this
.
http
.
get
(
`
${
this
.
adminUrl
}
/groups`
);
return
this
.
http
.
get
(
`
${
this
.
adminUrl
}
/groups`
);
}
}
// ========== HR ENDPOINTS ==========
// ========== HR ENDPOINTS ==========
getHRStats
():
Observable
<
any
>
{
getHRStats
():
Observable
<
any
>
{
return
this
.
http
.
get
(
`
${
this
.
hrUrl
}
/stats`
);
return
this
.
http
.
get
(
`
${
this
.
hrUrl
}
/stats`
);
}
}
getHRQuizzes
():
Observable
<
any
>
{
getHRQuizzes
():
Observable
<
any
>
{
return
this
.
http
.
get
(
`
${
this
.
hrUrl
}
/quizzes`
);
return
this
.
http
.
get
(
`
${
this
.
hrUrl
}
/quizzes`
);
}
}
getHRQuiz
(
quizId
:
string
):
Observable
<
any
>
{
getHRQuiz
(
quizId
:
string
):
Observable
<
any
>
{
return
this
.
http
.
get
(
`
${
this
.
hrUrl
}
/quiz/
${
quizId
}
`
);
return
this
.
http
.
get
(
`
${
this
.
hrUrl
}
/quiz/
${
quizId
}
`
);
}
}
createHRQuiz
(
formData
:
FormData
):
Observable
<
any
>
{
createHRQuiz
(
formData
:
FormData
):
Observable
<
any
>
{
return
this
.
http
.
post
(
`
${
this
.
hrUrl
}
/quiz/create`
,
formData
);
return
this
.
http
.
post
(
`
${
this
.
hrUrl
}
/quiz/create`
,
formData
);
}
}
createHRQuizManual
(
data
:
any
):
Observable
<
any
>
{
createHRQuizManual
(
data
:
any
):
Observable
<
any
>
{
return
this
.
http
.
post
(
`
${
this
.
hrUrl
}
/quiz/create-manual`
,
data
);
return
this
.
http
.
post
(
`
${
this
.
hrUrl
}
/quiz/create-manual`
,
data
);
}
}
updateHRQuiz
(
quizId
:
string
,
data
:
any
):
Observable
<
any
>
{
updateHRQuiz
(
quizId
:
string
,
data
:
any
):
Observable
<
any
>
{
return
this
.
http
.
put
(
`
${
this
.
hrUrl
}
/quiz/
${
quizId
}
`
,
data
);
return
this
.
http
.
put
(
`
${
this
.
hrUrl
}
/quiz/
${
quizId
}
`
,
data
);
}
}
deleteHRQuiz
(
quizId
:
string
):
Observable
<
any
>
{
deleteHRQuiz
(
quizId
:
string
):
Observable
<
any
>
{
return
this
.
http
.
delete
(
`
${
this
.
hrUrl
}
/quiz/
${
quizId
}
`
);
return
this
.
http
.
delete
(
`
${
this
.
hrUrl
}
/quiz/
${
quizId
}
`
);
}
}
getHRCandidates
():
Observable
<
any
>
{
getHRCandidates
():
Observable
<
any
>
{
return
this
.
http
.
get
(
`
${
this
.
hrUrl
}
/candidates`
);
return
this
.
http
.
get
(
`
${
this
.
hrUrl
}
/candidates`
);
}
}
getHRCandidateHistory
(
userId
:
string
):
Observable
<
any
>
{
getHRCandidateHistory
(
userId
:
string
):
Observable
<
any
>
{
return
this
.
http
.
get
(
`
${
this
.
hrUrl
}
/candidates/
${
userId
}
/history`
);
return
this
.
http
.
get
(
`
${
this
.
hrUrl
}
/candidates/
${
userId
}
/history`
);
}
}
getHRSubmissionDetails
(
submissionId
:
string
):
Observable
<
any
>
{
getHRSubmissionDetails
(
submissionId
:
string
):
Observable
<
any
>
{
return
this
.
http
.
get
(
`
${
this
.
hrUrl
}
/submissions/
${
submissionId
}
`
);
return
this
.
http
.
get
(
`
${
this
.
hrUrl
}
/submissions/
${
submissionId
}
`
);
}
}
getHRCategories
():
Observable
<
any
>
{
getHRCategories
():
Observable
<
any
>
{
return
this
.
http
.
get
(
`
${
this
.
hrUrl
}
/categories`
);
return
this
.
http
.
get
(
`
${
this
.
hrUrl
}
/categories`
);
}
}
getHRGroups
():
Observable
<
any
>
{
getHRGroups
():
Observable
<
any
>
{
return
this
.
http
.
get
(
`
${
this
.
hrUrl
}
/groups`
);
return
this
.
http
.
get
(
`
${
this
.
hrUrl
}
/groups`
);
}
}
// ========== CANDIDATE ENDPOINTS ==========
// ========== CANDIDATE ENDPOINTS ==========
getAvailableQuizzes
():
Observable
<
any
>
{
getAvailableQuizzes
():
Observable
<
any
>
{
return
this
.
http
.
get
(
`
${
this
.
candidateUrl
}
/quizzes`
);
return
this
.
http
.
get
(
`
${
this
.
candidateUrl
}
/quizzes`
);
}
}
getQuizForTaking
(
quizId
:
string
):
Observable
<
any
>
{
getQuizForTaking
(
quizId
:
string
):
Observable
<
any
>
{
return
this
.
http
.
get
(
`
${
this
.
candidateUrl
}
/quiz/
${
quizId
}
`
);
return
this
.
http
.
get
(
`
${
this
.
candidateUrl
}
/quiz/
${
quizId
}
`
);
}
}
submitQuiz
(
quizId
:
string
,
answers
:
any
[],
timeTaken
:
number
):
Observable
<
any
>
{
submitQuiz
(
quizId
:
string
,
answers
:
any
[],
timeTaken
:
number
):
Observable
<
any
>
{
return
this
.
http
.
post
(
`
${
this
.
candidateUrl
}
/quiz/
${
quizId
}
/submit`
,
{
answers
,
timeTaken
});
return
this
.
http
.
post
(
`
${
this
.
candidateUrl
}
/quiz/
${
quizId
}
/submit`
,
{
answers
,
timeTaken
});
}
}
getCandidateProfile
():
Observable
<
any
>
{
getCandidateProfile
():
Observable
<
any
>
{
return
this
.
http
.
get
(
`
${
this
.
candidateUrl
}
/profile`
);
return
this
.
http
.
get
(
`
${
this
.
candidateUrl
}
/profile`
);
}
}
getResultDetails
(
submissionId
:
string
):
Observable
<
any
>
{
getResultDetails
(
submissionId
:
string
):
Observable
<
any
>
{
return
this
.
http
.
get
(
`
${
this
.
candidateUrl
}
/results/
${
submissionId
}
`
);
return
this
.
http
.
get
(
`
${
this
.
candidateUrl
}
/results/
${
submissionId
}
`
);
}
}
}
}
Frontend/src/app/services/theme.service.ts
View file @
d2648061
...
@@ -33,6 +33,21 @@ export class ThemeService {
...
@@ -33,6 +33,21 @@ export class ThemeService {
}
}
private
applyTheme
(
theme
:
ThemeMode
):
void
{
private
applyTheme
(
theme
:
ThemeMode
):
void
{
// Existing (your custom theme)
document
.
documentElement
.
setAttribute
(
'
data-theme
'
,
theme
);
document
.
documentElement
.
setAttribute
(
'
data-theme
'
,
theme
);
// NEW (Material theme)
const
body
=
document
.
body
;
body
.
classList
.
remove
(
'
azure-theme
'
,
'
magenta-theme
'
,
'
cyan-theme
'
,
'
rose-theme
'
);
// Map your themes → material themes
const
themeMap
:
Record
<
ThemeMode
,
string
>
=
{
light
:
'
azure-theme
'
,
dark
:
'
magenta-theme
'
,
blue
:
'
cyan-theme
'
};
body
.
classList
.
add
(
themeMap
[
theme
]
||
'
azure-theme
'
);
}
}
}
}
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