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
Vigneswaran Shanmugam
Hire-Guru
Commits
68e5aeae
Commit
68e5aeae
authored
May 19, 2026
by
Aravind RK
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Refactor: minor ui bug fixes and changes
parent
99f0042f
Changes
29
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
29 changed files
with
409 additions
and
463 deletions
+409
-463
Backend/models/Interview.js
Backend/models/Interview.js
+1
-0
Backend/routes/admin.js
Backend/routes/admin.js
+14
-11
Backend/routes/candidate.js
Backend/routes/candidate.js
+61
-29
Backend/routes/hr.js
Backend/routes/hr.js
+4
-1
Backend/routes/interview.js
Backend/routes/interview.js
+4
-2
Frontend/src/app/components/layout/layout.css
Frontend/src/app/components/layout/layout.css
+101
-0
Frontend/src/app/components/layout/layout.html
Frontend/src/app/components/layout/layout.html
+27
-1
Frontend/src/app/components/layout/layout.ts
Frontend/src/app/components/layout/layout.ts
+6
-0
Frontend/src/app/pages/admin/dashboard/dashboard.css
Frontend/src/app/pages/admin/dashboard/dashboard.css
+43
-4
Frontend/src/app/pages/admin/group-interview/group-interview.html
.../src/app/pages/admin/group-interview/group-interview.html
+5
-2
Frontend/src/app/pages/admin/group-interview/group-interview.ts
...nd/src/app/pages/admin/group-interview/group-interview.ts
+2
-1
Frontend/src/app/pages/admin/hr-users/hr-users.css
Frontend/src/app/pages/admin/hr-users/hr-users.css
+5
-3
Frontend/src/app/pages/admin/hr-users/hr-users.html
Frontend/src/app/pages/admin/hr-users/hr-users.html
+1
-1
Frontend/src/app/pages/admin/individual-interview/individual-interview.html
...ages/admin/individual-interview/individual-interview.html
+5
-2
Frontend/src/app/pages/admin/individual-interview/individual-interview.ts
.../pages/admin/individual-interview/individual-interview.ts
+2
-1
Frontend/src/app/pages/admin/manage-groups/manage-groups.css
Frontend/src/app/pages/admin/manage-groups/manage-groups.css
+1
-1
Frontend/src/app/pages/admin/manage-groups/manage-groups.ts
Frontend/src/app/pages/admin/manage-groups/manage-groups.ts
+3
-0
Frontend/src/app/pages/candidate/take-quiz/take-quiz.css
Frontend/src/app/pages/candidate/take-quiz/take-quiz.css
+10
-2
Frontend/src/app/pages/candidate/take-quiz/take-quiz.html
Frontend/src/app/pages/candidate/take-quiz/take-quiz.html
+3
-3
Frontend/src/app/pages/candidate/take-quiz/take-quiz.ts
Frontend/src/app/pages/candidate/take-quiz/take-quiz.ts
+47
-2
Frontend/src/app/pages/hr/dashboard/dashboard.css
Frontend/src/app/pages/hr/dashboard/dashboard.css
+44
-5
Frontend/src/app/pages/hr/group-interview/group-interview.html
...end/src/app/pages/hr/group-interview/group-interview.html
+5
-2
Frontend/src/app/pages/hr/group-interview/group-interview.ts
Frontend/src/app/pages/hr/group-interview/group-interview.ts
+2
-1
Frontend/src/app/pages/hr/individual-interview/individual-interview.html
...p/pages/hr/individual-interview/individual-interview.html
+5
-2
Frontend/src/app/pages/hr/individual-interview/individual-interview.ts
...app/pages/hr/individual-interview/individual-interview.ts
+2
-1
Frontend/src/app/pages/hr/manage-groups/manage-groups.css
Frontend/src/app/pages/hr/manage-groups/manage-groups.css
+1
-1
Frontend/src/app/pages/hr/manage-groups/manage-groups.ts
Frontend/src/app/pages/hr/manage-groups/manage-groups.ts
+3
-0
Frontend/src/styles.css
Frontend/src/styles.css
+2
-0
ITL_Intern_Evaluation_Form.html
ITL_Intern_Evaluation_Form.html
+0
-385
No files found.
Backend/models/Interview.js
View file @
68e5aeae
...
@@ -25,6 +25,7 @@ const interviewSchema = new mongoose.Schema({
...
@@ -25,6 +25,7 @@ const interviewSchema = new mongoose.Schema({
techStack
:
{
type
:
String
,
default
:
''
,
trim
:
true
},
techStack
:
{
type
:
String
,
default
:
''
,
trim
:
true
},
source
:
{
type
:
String
,
default
:
''
,
trim
:
true
},
source
:
{
type
:
String
,
default
:
''
,
trim
:
true
},
dateOfInterview
:
{
type
:
Date
,
default
:
Date
.
now
},
dateOfInterview
:
{
type
:
Date
,
default
:
Date
.
now
},
timeOfInterview
:
{
type
:
String
,
default
:
''
},
// Quiz assignments (aptitude + technical)
// Quiz assignments (aptitude + technical)
quizzes
:
[{
quizzes
:
[{
...
...
Backend/routes/admin.js
View file @
68e5aeae
const
express
=
require
(
'
express
'
);
const
express
=
require
(
'
express
'
);
const
xlsx
=
require
(
'
xlsx
'
);
const
xlsx
=
require
(
'
xlsx
'
);
const
fs
=
require
(
'
fs
'
);
const
fs
=
require
(
'
fs
'
);
const
User
=
require
(
'
../models/User
'
);
const
User
=
require
(
'
../models/User
'
);
const
Group
=
require
(
'
../models/Group
'
);
const
Group
=
require
(
'
../models/Group
'
);
const
Quiz
=
require
(
'
../models/Quiz
'
);
const
Quiz
=
require
(
'
../models/Quiz
'
);
const
Question
=
require
(
'
../models/Question
'
);
const
Question
=
require
(
'
../models/Question
'
);
const
Submission
=
require
(
'
../models/Submission
'
);
const
Submission
=
require
(
'
../models/Submission
'
);
const
{
protect
,
authorize
}
=
require
(
'
../middleware/auth
'
);
const
Interview
=
require
(
'
../models/Interview
'
);
const
upload
=
require
(
'
../middleware/upload
'
);
const
{
protect
,
authorize
}
=
require
(
'
../middleware/auth
'
);
const
router
=
express
.
Router
();
const
upload
=
require
(
'
../middleware/upload
'
);
const
router
=
express
.
Router
();
// All admin routes require authentication + admin role
// All admin routes require authentication + admin role
router
.
use
(
protect
,
authorize
(
'
admin
'
));
router
.
use
(
protect
,
authorize
(
'
admin
'
));
...
@@ -852,6 +853,7 @@ IMPORTANT: The "correct" value must be the EXACT TEXT of the option, not the opt
...
@@ -852,6 +853,7 @@ IMPORTANT: The "correct" value must be the EXACT TEXT of the option, not the opt
await
Group
.
findOneAndUpdate
({
name
:
oldName
},
{
name
:
newName
.
trim
()
});
await
Group
.
findOneAndUpdate
({
name
:
oldName
},
{
name
:
newName
.
trim
()
});
await
User
.
updateMany
({
group
:
oldName
},
{
group
:
newName
.
trim
()
});
await
User
.
updateMany
({
group
:
oldName
},
{
group
:
newName
.
trim
()
});
await
Quiz
.
updateMany
({
assignedGroups
:
oldName
},
{
$set
:
{
"
assignedGroups.$
"
:
newName
.
trim
()
}
});
await
Quiz
.
updateMany
({
assignedGroups
:
oldName
},
{
$set
:
{
"
assignedGroups.$
"
:
newName
.
trim
()
}
});
await
Interview
.
updateMany
({
groupId
:
oldName
},
{
groupId
:
newName
.
trim
()
});
res
.
json
({
message
:
'
Group updated successfully
'
});
res
.
json
({
message
:
'
Group updated successfully
'
});
}
catch
(
error
)
{
}
catch
(
error
)
{
...
@@ -869,6 +871,7 @@ IMPORTANT: The "correct" value must be the EXACT TEXT of the option, not the opt
...
@@ -869,6 +871,7 @@ IMPORTANT: The "correct" value must be the EXACT TEXT of the option, not the opt
await
Group
.
deleteOne
({
name
});
await
Group
.
deleteOne
({
name
});
await
User
.
updateMany
({
group
:
name
},
{
group
:
'
General
'
});
await
User
.
updateMany
({
group
:
name
},
{
group
:
'
General
'
});
await
Quiz
.
updateMany
({
assignedGroups
:
name
},
{
$pull
:
{
assignedGroups
:
name
}
});
await
Quiz
.
updateMany
({
assignedGroups
:
name
},
{
$pull
:
{
assignedGroups
:
name
}
});
await
Interview
.
updateMany
({
groupId
:
name
},
{
groupId
:
'
General
'
});
res
.
json
({
message
:
'
Group deleted successfully
'
});
res
.
json
({
message
:
'
Group deleted successfully
'
});
}
catch
(
error
)
{
}
catch
(
error
)
{
...
...
Backend/routes/candidate.js
View file @
68e5aeae
...
@@ -23,8 +23,29 @@ router.get('/interviews', async (req, res) => {
...
@@ -23,8 +23,29 @@ router.get('/interviews', async (req, res) => {
.
populate
(
'
createdBy
'
,
'
name
'
)
.
populate
(
'
createdBy
'
,
'
name
'
)
.
populate
(
'
quizzes.quizId
'
,
'
title timer totalQuestions category difficulty
'
)
.
populate
(
'
quizzes.quizId
'
,
'
title timer totalQuestions category difficulty
'
)
.
sort
({
createdAt
:
-
1
});
.
sort
({
createdAt
:
-
1
});
const
now
=
new
Date
();
res
.
json
({
interviews
});
// Filter out interviews scheduled in the future
const
activeInterviews
=
interviews
.
filter
(
inv
=>
{
if
(
!
inv
.
dateOfInterview
)
return
true
;
// If no date, show it
const
invDate
=
new
Date
(
inv
.
dateOfInterview
);
// If time is provided, parse and set it on the date object
if
(
inv
.
timeOfInterview
)
{
const
[
hours
,
minutes
]
=
inv
.
timeOfInterview
.
split
(
'
:
'
).
map
(
Number
);
if
(
!
isNaN
(
hours
)
&&
!
isNaN
(
minutes
))
{
invDate
.
setHours
(
hours
,
minutes
,
0
,
0
);
}
}
else
{
// If no time is provided, we can assume it's available at the start of the day
invDate
.
setHours
(
0
,
0
,
0
,
0
);
}
return
now
>=
invDate
;
});
res
.
json
({
interviews
:
activeInterviews
});
}
catch
(
error
)
{
}
catch
(
error
)
{
res
.
status
(
500
).
json
({
message
:
'
Server error
'
,
error
:
error
.
message
});
res
.
status
(
500
).
json
({
message
:
'
Server error
'
,
error
:
error
.
message
});
}
}
...
@@ -198,7 +219,7 @@ router.post('/quiz/:quizId/submit', async (req, res) => {
...
@@ -198,7 +219,7 @@ router.post('/quiz/:quizId/submit', async (req, res) => {
// Update Interview if applicable
// Update Interview if applicable
const
Interview
=
require
(
'
../models/Interview
'
);
const
Interview
=
require
(
'
../models/Interview
'
);
await
Interview
.
findOneAndUpdate
(
const
updatedInterview
=
await
Interview
.
findOneAndUpdate
(
{
candidateId
:
req
.
user
.
_id
,
'
quizzes.quizId
'
:
quizId
},
{
candidateId
:
req
.
user
.
_id
,
'
quizzes.quizId
'
:
quizId
},
{
{
$set
:
{
$set
:
{
...
@@ -208,9 +229,18 @@ router.post('/quiz/:quizId/submit', async (req, res) => {
...
@@ -208,9 +229,18 @@ router.post('/quiz/:quizId/submit', async (req, res) => {
'
quizzes.$.completed
'
:
true
,
'
quizzes.$.completed
'
:
true
,
'
quizzes.$.violation
'
:
!!
violation
'
quizzes.$.violation
'
:
!!
violation
}
}
}
},
{
new
:
true
}
);
);
if
(
updatedInterview
)
{
const
allCompleted
=
updatedInterview
.
quizzes
.
every
(
q
=>
q
.
completed
);
if
(
allCompleted
&&
(
updatedInterview
.
status
===
'
quiz_phase
'
||
updatedInterview
.
status
===
'
pending
'
))
{
updatedInterview
.
status
=
'
coding_phase
'
;
await
updatedInterview
.
save
();
}
}
res
.
status
(
201
).
json
({
res
.
status
(
201
).
json
({
message
:
'
Quiz submitted successfully
'
,
message
:
'
Quiz submitted successfully
'
,
result
:
{
result
:
{
...
@@ -426,7 +456,8 @@ router.delete('/profile/resume', async (req, res) => {
...
@@ -426,7 +456,8 @@ router.delete('/profile/resume', async (req, res) => {
await
user
.
save
();
await
user
.
save
();
}
}
res
.
json
({
message
:
'
Resume deleted successfully
'
,
user
:
{
res
.
json
({
message
:
'
Resume deleted successfully
'
,
user
:
{
_id
:
user
.
_id
,
_id
:
user
.
_id
,
name
:
user
.
name
,
name
:
user
.
name
,
email
:
user
.
email
,
email
:
user
.
email
,
...
@@ -434,7 +465,8 @@ router.delete('/profile/resume', async (req, res) => {
...
@@ -434,7 +465,8 @@ router.delete('/profile/resume', async (req, res) => {
resume
:
user
.
resume
,
resume
:
user
.
resume
,
level
:
user
.
level
,
level
:
user
.
level
,
topicsOfInterest
:
user
.
topicsOfInterest
topicsOfInterest
:
user
.
topicsOfInterest
}
});
}
});
}
catch
(
error
)
{
}
catch
(
error
)
{
res
.
status
(
500
).
json
({
message
:
'
Server error
'
,
error
:
error
.
message
});
res
.
status
(
500
).
json
({
message
:
'
Server error
'
,
error
:
error
.
message
});
}
}
...
...
Backend/routes/hr.js
View file @
68e5aeae
const
express
=
require
(
'
express
'
);
const
express
=
require
(
'
express
'
);
const
xlsx
=
require
(
'
xlsx
'
);
const
xlsx
=
require
(
'
xlsx
'
);
const
fs
=
require
(
'
fs
'
);
const
fs
=
require
(
'
fs
'
);
const
User
=
require
(
'
../models/User
'
);
const
User
=
require
(
'
../models/User
'
);
...
@@ -6,6 +6,7 @@
...
@@ -6,6 +6,7 @@
const
Quiz
=
require
(
'
../models/Quiz
'
);
const
Quiz
=
require
(
'
../models/Quiz
'
);
const
Question
=
require
(
'
../models/Question
'
);
const
Question
=
require
(
'
../models/Question
'
);
const
Submission
=
require
(
'
../models/Submission
'
);
const
Submission
=
require
(
'
../models/Submission
'
);
const
Interview
=
require
(
'
../models/Interview
'
);
const
{
protect
,
authorize
}
=
require
(
'
../middleware/auth
'
);
const
{
protect
,
authorize
}
=
require
(
'
../middleware/auth
'
);
const
upload
=
require
(
'
../middleware/upload
'
);
const
upload
=
require
(
'
../middleware/upload
'
);
const
router
=
express
.
Router
();
const
router
=
express
.
Router
();
...
@@ -816,6 +817,7 @@ IMPORTANT: The "correct" value must be the EXACT TEXT of the option, not the opt
...
@@ -816,6 +817,7 @@ IMPORTANT: The "correct" value must be the EXACT TEXT of the option, not the opt
await
Group
.
findOneAndUpdate
({
name
:
oldName
},
{
name
:
newName
.
trim
()
});
await
Group
.
findOneAndUpdate
({
name
:
oldName
},
{
name
:
newName
.
trim
()
});
await
User
.
updateMany
({
group
:
oldName
},
{
group
:
newName
.
trim
()
});
await
User
.
updateMany
({
group
:
oldName
},
{
group
:
newName
.
trim
()
});
await
Quiz
.
updateMany
({
assignedGroups
:
oldName
},
{
$set
:
{
"
assignedGroups.$
"
:
newName
.
trim
()
}
});
await
Quiz
.
updateMany
({
assignedGroups
:
oldName
},
{
$set
:
{
"
assignedGroups.$
"
:
newName
.
trim
()
}
});
await
Interview
.
updateMany
({
groupId
:
oldName
},
{
groupId
:
newName
.
trim
()
});
res
.
json
({
message
:
'
Group updated successfully
'
});
res
.
json
({
message
:
'
Group updated successfully
'
});
}
catch
(
error
)
{
}
catch
(
error
)
{
...
@@ -833,6 +835,7 @@ IMPORTANT: The "correct" value must be the EXACT TEXT of the option, not the opt
...
@@ -833,6 +835,7 @@ IMPORTANT: The "correct" value must be the EXACT TEXT of the option, not the opt
await
Group
.
deleteOne
({
name
});
await
Group
.
deleteOne
({
name
});
await
User
.
updateMany
({
group
:
name
},
{
group
:
'
General
'
});
await
User
.
updateMany
({
group
:
name
},
{
group
:
'
General
'
});
await
Quiz
.
updateMany
({
assignedGroups
:
name
},
{
$pull
:
{
assignedGroups
:
name
}
});
await
Quiz
.
updateMany
({
assignedGroups
:
name
},
{
$pull
:
{
assignedGroups
:
name
}
});
await
Interview
.
updateMany
({
groupId
:
name
},
{
groupId
:
'
General
'
});
res
.
json
({
message
:
'
Group deleted successfully
'
});
res
.
json
({
message
:
'
Group deleted successfully
'
});
}
catch
(
error
)
{
}
catch
(
error
)
{
...
...
Backend/routes/interview.js
View file @
68e5aeae
...
@@ -19,7 +19,7 @@ router.use(protect);
...
@@ -19,7 +19,7 @@ router.use(protect);
// ============================================================
// ============================================================
router
.
post
(
'
/
'
,
authorize
(
'
admin
'
,
'
hr
'
,
'
pm
'
),
async
(
req
,
res
)
=>
{
router
.
post
(
'
/
'
,
authorize
(
'
admin
'
,
'
hr
'
,
'
pm
'
),
async
(
req
,
res
)
=>
{
try
{
try
{
const
{
candidateId
,
interviewerId
,
assignedInterviewers
,
assignedHRs
,
assignedPMs
,
position
,
techStack
,
source
,
dateOfInterview
,
quizIds
}
=
req
.
body
;
const
{
candidateId
,
interviewerId
,
assignedInterviewers
,
assignedHRs
,
assignedPMs
,
position
,
techStack
,
source
,
dateOfInterview
,
timeOfInterview
,
quizIds
}
=
req
.
body
;
// Use interviewerId as fallback, or assignedInterviewers[0] as interviewerId for backward compatibility
// Use interviewerId as fallback, or assignedInterviewers[0] as interviewerId for backward compatibility
const
mainInterviewerId
=
interviewerId
||
(
assignedInterviewers
&&
assignedInterviewers
.
length
>
0
?
assignedInterviewers
[
0
]
:
null
);
const
mainInterviewerId
=
interviewerId
||
(
assignedInterviewers
&&
assignedInterviewers
.
length
>
0
?
assignedInterviewers
[
0
]
:
null
);
...
@@ -56,6 +56,7 @@ router.post('/', authorize('admin', 'hr', 'pm'), async (req, res) => {
...
@@ -56,6 +56,7 @@ router.post('/', authorize('admin', 'hr', 'pm'), async (req, res) => {
techStack
:
techStack
||
''
,
techStack
:
techStack
||
''
,
source
:
source
||
''
,
source
:
source
||
''
,
dateOfInterview
:
dateOfInterview
||
new
Date
(),
dateOfInterview
:
dateOfInterview
||
new
Date
(),
timeOfInterview
:
timeOfInterview
||
''
,
quizzes
,
quizzes
,
status
:
quizzes
.
length
>
0
?
'
quiz_phase
'
:
'
pending
'
,
status
:
quizzes
.
length
>
0
?
'
quiz_phase
'
:
'
pending
'
,
type
:
'
individual
'
,
type
:
'
individual
'
,
...
@@ -222,7 +223,7 @@ router.post('/group', authorize('admin', 'hr', 'pm'), async (req, res) => {
...
@@ -222,7 +223,7 @@ router.post('/group', authorize('admin', 'hr', 'pm'), async (req, res) => {
try
{
try
{
const
{
const
{
groupName
,
assignedInterviewers
,
assignedHRs
,
assignedPMs
,
groupName
,
assignedInterviewers
,
assignedHRs
,
assignedPMs
,
position
,
techStack
,
source
,
dateOfInterview
,
quizSets
position
,
techStack
,
source
,
dateOfInterview
,
timeOfInterview
,
quizSets
}
=
req
.
body
;
}
=
req
.
body
;
if
(
!
groupName
||
!
position
)
{
if
(
!
groupName
||
!
position
)
{
...
@@ -286,6 +287,7 @@ router.post('/group', authorize('admin', 'hr', 'pm'), async (req, res) => {
...
@@ -286,6 +287,7 @@ router.post('/group', authorize('admin', 'hr', 'pm'), async (req, res) => {
techStack
:
techStack
||
''
,
techStack
:
techStack
||
''
,
source
:
source
||
''
,
source
:
source
||
''
,
dateOfInterview
:
dateOfInterview
||
new
Date
(),
dateOfInterview
:
dateOfInterview
||
new
Date
(),
timeOfInterview
:
timeOfInterview
||
''
,
quizzes
,
quizzes
,
status
:
quizzes
.
length
>
0
?
'
quiz_phase
'
:
'
pending
'
,
status
:
quizzes
.
length
>
0
?
'
quiz_phase
'
:
'
pending
'
,
type
:
'
group
'
,
type
:
'
group
'
,
...
...
Frontend/src/app/components/layout/layout.css
View file @
68e5aeae
...
@@ -438,6 +438,27 @@
...
@@ -438,6 +438,27 @@
margin
:
-8px
-12px
-12px
-8px
;
/* Compensate padding to maintain perfect visual alignment */
margin
:
-8px
-12px
-12px
-8px
;
/* Compensate padding to maintain perfect visual alignment */
}
}
.scrollable-options
{
max-height
:
242px
;
}
.modal-options
::-webkit-scrollbar
{
width
:
6px
;
}
.modal-options
::-webkit-scrollbar-track
{
background
:
transparent
;
}
.modal-options
::-webkit-scrollbar-thumb
{
background
:
var
(
--border-color
);
border-radius
:
3px
;
}
.modal-options
::-webkit-scrollbar-thumb:hover
{
background
:
var
(
--text-muted
);
}
.glassy-option
{
.glassy-option
{
display
:
flex
;
display
:
flex
;
align-items
:
center
;
align-items
:
center
;
...
@@ -518,3 +539,83 @@
...
@@ -518,3 +539,83 @@
0
%
{
opacity
:
0
;
transform
:
scale
(
0.95
)
translateY
(
10px
);
}
0
%
{
opacity
:
0
;
transform
:
scale
(
0.95
)
translateY
(
10px
);
}
100
%
{
opacity
:
1
;
transform
:
scale
(
1
)
translateY
(
0
);
}
100
%
{
opacity
:
1
;
transform
:
scale
(
1
)
translateY
(
0
);
}
}
}
/* Confirm Modal Specifics */
.confirm-modal
{
max-width
:
400px
;
text-align
:
center
;
align-items
:
center
;
}
.confirm-modal
.modal-header
{
width
:
100%
;
margin-bottom
:
16px
;
}
.confirm-modal-body
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
gap
:
16px
;
margin-bottom
:
24px
;
}
.confirm-icon-wrapper
{
width
:
64px
;
height
:
64px
;
border-radius
:
50%
;
background
:
var
(
--danger-light
);
color
:
var
(
--danger
);
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
margin-bottom
:
8px
;
}
.confirm-icon-wrapper
.material-symbols-rounded
{
font-size
:
32px
;
}
.confirm-text
{
font-size
:
15px
;
color
:
var
(
--text-secondary
);
line-height
:
1.5
;
margin
:
0
;
}
.confirm-modal-actions
{
display
:
flex
;
gap
:
12px
;
width
:
100%
;
}
.confirm-modal-actions
.btn
{
flex
:
1
;
padding
:
12px
;
border-radius
:
12px
;
font-size
:
14px
;
font-weight
:
600
;
cursor
:
pointer
;
border
:
none
;
transition
:
all
0.2s
ease
;
}
.confirm-modal-actions
.btn-secondary
{
background
:
var
(
--bg-hover
);
color
:
var
(
--text-primary
);
border
:
1px
solid
var
(
--border-color
);
}
.confirm-modal-actions
.btn-secondary
:hover
{
background
:
var
(
--bg-tertiary
);
}
.confirm-modal-actions
.btn-danger
{
background
:
var
(
--danger
);
color
:
#fff
;
}
.confirm-modal-actions
.btn-danger
:hover
{
background
:
var
(
--danger-border
);
box-shadow
:
0
4px
12px
rgba
(
239
,
68
,
68
,
0.2
);
}
Frontend/src/app/components/layout/layout.html
View file @
68e5aeae
...
@@ -103,7 +103,7 @@
...
@@ -103,7 +103,7 @@
</button>
</button>
</div>
</div>
<div
class=
"modal-options"
>
<div
class=
"modal-options"
[class.scrollable-options]=
"authService.getUserRole() === 'admin'"
>
<a
[routerLink]=
"getUsersRoute()"
class=
"glassy-option"
(click)=
"closeManageUsersPopup()"
>
<a
[routerLink]=
"getUsersRoute()"
class=
"glassy-option"
(click)=
"closeManageUsersPopup()"
>
<div
class=
"option-icon-wrapper"
>
<div
class=
"option-icon-wrapper"
>
<span
class=
"material-symbols-rounded block-icon"
>
people
</span>
<span
class=
"material-symbols-rounded block-icon"
>
people
</span>
...
@@ -219,3 +219,29 @@
...
@@ -219,3 +219,29 @@
</div>
</div>
</div>
</div>
}
}
<!-- Logout Confirmation Modal -->
@if (showLogoutConfirm) {
<div
class=
"glassy-overlay"
(click)=
"showLogoutConfirm = false"
>
<div
class=
"glassy-modal confirm-modal animate-fade-in"
(click)=
"$event.stopPropagation()"
>
<div
class=
"modal-header"
>
<h2>
Confirm Logout
</h2>
<button
class=
"close-btn"
(click)=
"showLogoutConfirm = false"
>
<span
class=
"material-symbols-rounded"
>
close
</span>
</button>
</div>
<div
class=
"confirm-modal-body"
>
<div
class=
"confirm-icon-wrapper"
>
<span
class=
"material-symbols-rounded"
>
logout
</span>
</div>
<p
class=
"confirm-text"
>
Are you sure you want to sign out of QuizMaster?
</p>
</div>
<div
class=
"confirm-modal-actions"
>
<button
class=
"btn btn-secondary"
(click)=
"showLogoutConfirm = false"
>
Cancel
</button>
<button
class=
"btn btn-danger"
(click)=
"confirmLogout()"
>
Sign Out
</button>
</div>
</div>
</div>
}
\ No newline at end of file
Frontend/src/app/components/layout/layout.ts
View file @
68e5aeae
...
@@ -26,6 +26,7 @@ export class LayoutComponent {
...
@@ -26,6 +26,7 @@ export class LayoutComponent {
showThemeMenu
=
false
;
showThemeMenu
=
false
;
mobileSidebarOpen
=
false
;
mobileSidebarOpen
=
false
;
showLogoutConfirm
=
false
;
themes
:
{
id
:
ThemeMode
;
label
:
string
;
icon
:
string
}[]
=
[
themes
:
{
id
:
ThemeMode
;
label
:
string
;
icon
:
string
}[]
=
[
{
id
:
'
light
'
,
label
:
'
Light
'
,
icon
:
'
light_mode
'
},
{
id
:
'
light
'
,
label
:
'
Light
'
,
icon
:
'
light_mode
'
},
...
@@ -107,6 +108,11 @@ export class LayoutComponent {
...
@@ -107,6 +108,11 @@ export class LayoutComponent {
}
}
logout
():
void
{
logout
():
void
{
this
.
showLogoutConfirm
=
true
;
}
confirmLogout
():
void
{
this
.
showLogoutConfirm
=
false
;
this
.
authService
.
logout
();
this
.
authService
.
logout
();
}
}
...
...
Frontend/src/app/pages/admin/dashboard/dashboard.css
View file @
68e5aeae
...
@@ -110,24 +110,63 @@
...
@@ -110,24 +110,63 @@
/* Quick Actions */
/* Quick Actions */
.actions-grid
{
.actions-grid
{
display
:
grid
;
display
:
grid
;
grid-template-columns
:
repeat
(
4
,
1
fr
);
grid-template-columns
:
repeat
(
auto-fit
,
minmax
(
240px
,
1
fr
)
);
gap
:
16px
;
gap
:
16px
;
}
}
.action-card
{
.action-card
{
display
:
flex
;
display
:
flex
;
align-items
:
flex-start
;
align-items
:
center
;
gap
:
16px
;
gap
:
16px
;
text-decoration
:
none
;
text-decoration
:
none
;
color
:
inherit
;
color
:
inherit
;
height
:
100%
;
height
:
100%
;
padding
:
18px
!important
;
background
:
var
(
--bg-card
);
border
:
1px
solid
var
(
--border-color
);
border-radius
:
16px
;
transition
:
all
0.3s
cubic-bezier
(
0.4
,
0
,
0.2
,
1
);
position
:
relative
;
overflow
:
hidden
;
}
.action-card
::before
{
content
:
''
;
position
:
absolute
;
top
:
0
;
left
:
0
;
right
:
0
;
bottom
:
0
;
background
:
linear-gradient
(
135deg
,
rgba
(
102
,
126
,
234
,
0.05
)
0%
,
transparent
100%
);
opacity
:
0
;
transition
:
opacity
0.3s
;
}
.action-card
:hover::before
{
opacity
:
1
;
}
.action-card
:hover
{
border-color
:
var
(
--accent-primary
);
box-shadow
:
0
12px
30px
rgba
(
102
,
126
,
234
,
0.12
);
transform
:
translateY
(
-4px
);
}
}
.action-icon
{
.action-icon
{
font-size
:
28px
;
width
:
48px
;
height
:
48px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
background
:
rgba
(
102
,
126
,
234
,
0.1
);
color
:
var
(
--accent-primary
);
color
:
var
(
--accent-primary
);
border-radius
:
12px
;
font-size
:
24px
!important
;
flex-shrink
:
0
;
flex-shrink
:
0
;
margin-top
:
2px
;
transition
:
all
0.3s
;
}
.action-card
:hover
.action-icon
{
background
:
var
(
--accent-primary
);
color
:
#fff
;
transform
:
scale
(
1.1
)
rotate
(
-5deg
);
}
}
.action-info
{
.action-info
{
...
...
Frontend/src/app/pages/admin/group-interview/group-interview.html
View file @
68e5aeae
...
@@ -257,8 +257,11 @@
...
@@ -257,8 +257,11 @@
<input
class=
"form-input"
[(ngModel)]=
"newGroupInterview.source"
placeholder=
"e.g., LinkedIn, Campus"
>
<input
class=
"form-input"
[(ngModel)]=
"newGroupInterview.source"
placeholder=
"e.g., LinkedIn, Campus"
>
</div>
</div>
<div
class=
"form-group"
>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Date of Interview
</label>
<label
class=
"form-label"
>
Date
&
Time
</label>
<input
class=
"form-input"
type=
"date"
[(ngModel)]=
"newGroupInterview.dateOfInterview"
>
<div
style=
"display: flex; gap: 8px;"
>
<input
class=
"form-input"
type=
"date"
[(ngModel)]=
"newGroupInterview.dateOfInterview"
style=
"flex: 1;"
>
<input
class=
"form-input"
type=
"time"
[(ngModel)]=
"newGroupInterview.timeOfInterview"
style=
"flex: 1;"
>
</div>
</div>
</div>
</div>
</div>
<div
class=
"form-row"
>
<div
class=
"form-row"
>
...
...
Frontend/src/app/pages/admin/group-interview/group-interview.ts
View file @
68e5aeae
...
@@ -145,7 +145,8 @@ export class GroupInterviewComponent implements OnInit {
...
@@ -145,7 +145,8 @@ export class GroupInterviewComponent implements OnInit {
position
:
''
,
position
:
''
,
techStack
:
''
,
techStack
:
''
,
source
:
''
,
source
:
''
,
dateOfInterview
:
new
Date
().
toISOString
().
split
(
'
T
'
)[
0
]
dateOfInterview
:
new
Date
().
toISOString
().
split
(
'
T
'
)[
0
],
timeOfInterview
:
''
};
};
}
}
...
...
Frontend/src/app/pages/admin/hr-users/hr-users.css
View file @
68e5aeae
...
@@ -23,8 +23,8 @@
...
@@ -23,8 +23,8 @@
.btn-primary
:hover:not
(
:disabled
)
{
transform
:
translateY
(
-2px
);
box-shadow
:
0
6px
20px
rgba
(
102
,
126
,
234
,
0.4
);
}
.btn-primary
:hover:not
(
:disabled
)
{
transform
:
translateY
(
-2px
);
box-shadow
:
0
6px
20px
rgba
(
102
,
126
,
234
,
0.4
);
}
.btn-primary
:disabled
{
opacity
:
0.6
;
cursor
:
not-allowed
;
transform
:
none
;
box-shadow
:
none
;
}
.btn-primary
:disabled
{
opacity
:
0.6
;
cursor
:
not-allowed
;
transform
:
none
;
box-shadow
:
none
;
}
.groups-grid
{
display
:
grid
;
grid-template-columns
:
repeat
(
auto-fill
,
minmax
(
320px
,
1
fr
));
gap
:
16px
;
}
.groups-grid
{
column-count
:
2
;
column-
gap
:
16px
;
}
.group-card
{
background
:
var
(
--bg-card
);
border
:
1px
solid
var
(
--border-color
);
border-radius
:
16px
;
padding
:
20px
;
display
:
flex
;
align-items
:
stretch
;
gap
:
16px
;
transition
:
all
0.3s
;
}
.group-card
{
background
:
var
(
--bg-card
);
border
:
1px
solid
var
(
--border-color
);
border-radius
:
16px
;
padding
:
20px
;
display
:
inline-flex
;
width
:
100%
;
align-items
:
center
;
gap
:
16px
;
transition
:
all
0.3s
;
break-inside
:
avoid
;
margin-bottom
:
16px
;
box-sizing
:
border-box
;
}
.group-card
:hover
{
border-color
:
var
(
--accent-primary
);
transform
:
translateY
(
-3px
);
box-shadow
:
var
(
--shadow-md
);
}
.group-card
:hover
{
border-color
:
var
(
--accent-primary
);
transform
:
translateY
(
-3px
);
box-shadow
:
var
(
--shadow-md
);
}
.group-icon-wrapper
{
width
:
50px
;
height
:
50px
;
border-radius
:
12px
;
background
:
linear-gradient
(
135deg
,
rgba
(
102
,
126
,
234
,
0.1
)
0%
,
rgba
(
102
,
126
,
234
,
0.2
)
100%
);
color
:
var
(
--accent-primary
);
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
flex-shrink
:
0
;
}
.group-icon-wrapper
{
width
:
50px
;
height
:
50px
;
border-radius
:
12px
;
background
:
linear-gradient
(
135deg
,
rgba
(
102
,
126
,
234
,
0.1
)
0%
,
rgba
(
102
,
126
,
234
,
0.2
)
100%
);
color
:
var
(
--accent-primary
);
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
flex-shrink
:
0
;
}
.group-icon-wrapper
.material-symbols-rounded
{
font-size
:
26px
;
}
.group-icon-wrapper
.material-symbols-rounded
{
font-size
:
26px
;
}
...
@@ -49,8 +49,10 @@
...
@@ -49,8 +49,10 @@
.empty-state
h3
{
color
:
var
(
--text-primary
);
margin
:
0
0
8px
;
font-size
:
18px
;
}
.empty-state
h3
{
color
:
var
(
--text-primary
);
margin
:
0
0
8px
;
font-size
:
18px
;
}
.empty-state
p
{
color
:
var
(
--text-muted
);
margin
:
0
;
font-size
:
14px
;
}
.empty-state
p
{
color
:
var
(
--text-muted
);
margin
:
0
;
font-size
:
14px
;
}
@media
(
max-width
:
768px
)
{
.groups-grid
{
column-count
:
1
;
}
}
@media
(
max-width
:
600px
)
{
@media
(
max-width
:
600px
)
{
.page-container
{
padding
:
20px
16px
;
}
.page-container
{
padding
:
20px
16px
;
}
.row-align
{
flex-direction
:
column
;
align-items
:
stretch
;
}
.row-align
{
flex-direction
:
column
;
align-items
:
stretch
;
}
.groups-grid
{
grid-template-columns
:
1
fr
;
}
}
}
Frontend/src/app/pages/admin/hr-users/hr-users.html
View file @
68e5aeae
...
@@ -74,7 +74,7 @@
...
@@ -74,7 +74,7 @@
<p
style=
"margin: 4px 0 0; font-size: 13px; color: var(--text-muted);"
>
{{ user.email }}
</p>
<p
style=
"margin: 4px 0 0; font-size: 13px; color: var(--text-muted);"
>
{{ user.email }}
</p>
}
}
</div>
</div>
<div
class=
"group-actions"
style=
"display: flex; gap: 8px;"
>
<div
class=
"group-actions"
style=
"display: flex; gap: 8px;
align-items: center;
"
>
@if (editingId() === user._id) {
@if (editingId() === user._id) {
<button
class=
"action-btn text-success"
(click)=
"saveEdit(user._id)"
title=
"Save"
><span
class=
"material-symbols-rounded"
>
check
</span></button>
<button
class=
"action-btn text-success"
(click)=
"saveEdit(user._id)"
title=
"Save"
><span
class=
"material-symbols-rounded"
>
check
</span></button>
<button
class=
"action-btn text-danger"
(click)=
"cancelEdit()"
title=
"Cancel"
><span
class=
"material-symbols-rounded"
>
close
</span></button>
<button
class=
"action-btn text-danger"
(click)=
"cancelEdit()"
title=
"Cancel"
><span
class=
"material-symbols-rounded"
>
close
</span></button>
...
...
Frontend/src/app/pages/admin/individual-interview/individual-interview.html
View file @
68e5aeae
...
@@ -184,8 +184,11 @@
...
@@ -184,8 +184,11 @@
<input
class=
"form-input"
[(ngModel)]=
"newInterview.source"
placeholder=
"e.g., LinkedIn, Campus"
>
<input
class=
"form-input"
[(ngModel)]=
"newInterview.source"
placeholder=
"e.g., LinkedIn, Campus"
>
</div>
</div>
<div
class=
"form-group"
>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Date of Interview
</label>
<label
class=
"form-label"
>
Date
&
Time
</label>
<input
class=
"form-input"
type=
"date"
[(ngModel)]=
"newInterview.dateOfInterview"
>
<div
style=
"display: flex; gap: 8px;"
>
<input
class=
"form-input"
type=
"date"
[(ngModel)]=
"newInterview.dateOfInterview"
style=
"flex: 1;"
>
<input
class=
"form-input"
type=
"time"
[(ngModel)]=
"newInterview.timeOfInterview"
style=
"flex: 1;"
>
</div>
</div>
</div>
</div>
</div>
...
...
Frontend/src/app/pages/admin/individual-interview/individual-interview.ts
View file @
68e5aeae
...
@@ -34,6 +34,7 @@ export class IndividualInterviewComponent implements OnInit {
...
@@ -34,6 +34,7 @@ export class IndividualInterviewComponent implements OnInit {
techStack
:
''
,
techStack
:
''
,
source
:
''
,
source
:
''
,
dateOfInterview
:
new
Date
().
toISOString
().
split
(
'
T
'
)[
0
],
dateOfInterview
:
new
Date
().
toISOString
().
split
(
'
T
'
)[
0
],
timeOfInterview
:
''
,
quizIds
:
[]
as
string
[]
quizIds
:
[]
as
string
[]
};
};
...
@@ -95,7 +96,7 @@ export class IndividualInterviewComponent implements OnInit {
...
@@ -95,7 +96,7 @@ export class IndividualInterviewComponent implements OnInit {
});
});
this
.
newInterview
=
{
this
.
newInterview
=
{
candidateId
:
''
,
assignedInterviewers
:
[],
assignedHRs
:
[],
assignedPMs
:
[],
position
:
''
,
techStack
:
''
,
candidateId
:
''
,
assignedInterviewers
:
[],
assignedHRs
:
[],
assignedPMs
:
[],
position
:
''
,
techStack
:
''
,
source
:
''
,
dateOfInterview
:
new
Date
().
toISOString
().
split
(
'
T
'
)[
0
],
quizIds
:
[]
source
:
''
,
dateOfInterview
:
new
Date
().
toISOString
().
split
(
'
T
'
)[
0
],
timeOfInterview
:
''
,
quizIds
:
[]
};
};
this
.
showCreateModal
.
set
(
true
);
this
.
showCreateModal
.
set
(
true
);
}
}
...
...
Frontend/src/app/pages/admin/manage-groups/manage-groups.css
View file @
68e5aeae
...
@@ -39,7 +39,7 @@
...
@@ -39,7 +39,7 @@
.group-lane
:hover
{
border-color
:
rgba
(
102
,
126
,
234
,
0.4
);
transform
:
translateY
(
-4px
);
box-shadow
:
0
12px
24px
rgba
(
0
,
0
,
0
,
0.1
);
}
.group-lane
:hover
{
border-color
:
rgba
(
102
,
126
,
234
,
0.4
);
transform
:
translateY
(
-4px
);
box-shadow
:
0
12px
24px
rgba
(
0
,
0
,
0
,
0.1
);
}
.group-lane.cdk-drop-list-receiving
{
border-color
:
var
(
--accent-primary
);
background
:
rgba
(
102
,
126
,
234
,
0.05
);
box-shadow
:
0
0
0
2px
rgba
(
102
,
126
,
234
,
0.2
);
transform
:
scale
(
1.02
);
z-index
:
10
;
}
.group-lane.cdk-drop-list-receiving
{
border-color
:
var
(
--accent-primary
);
background
:
rgba
(
102
,
126
,
234
,
0.05
);
box-shadow
:
0
0
0
2px
rgba
(
102
,
126
,
234
,
0.2
);
transform
:
scale
(
1.02
);
z-index
:
10
;
}
.group-lane-header
{
padding
:
18px
20px
;
background
:
rgba
(
102
,
126
,
234
,
0.05
);
border-bottom
:
1px
solid
transparent
;
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
transition
:
background
0.3s
;
pointer-events
:
none
;
}
.group-lane-header
{
padding
:
18px
20px
;
background
:
rgba
(
102
,
126
,
234
,
0.05
);
border-bottom
:
1px
solid
transparent
;
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
transition
:
background
0.3s
;
pointer-events
:
auto
;
}
.group-lane.expanded
.group-lane-header
{
background
:
rgba
(
102
,
126
,
234
,
0.1
);
border-bottom
:
1px
solid
var
(
--border-color
);
}
.group-lane.expanded
.group-lane-header
{
background
:
rgba
(
102
,
126
,
234
,
0.1
);
border-bottom
:
1px
solid
var
(
--border-color
);
}
.group-actions
{
pointer-events
:
auto
;
}
.group-actions
{
pointer-events
:
auto
;
}
.group-lane-body
{
cursor
:
default
;
overflow
:
hidden
;
}
.group-lane-body
{
cursor
:
default
;
overflow
:
hidden
;
}
...
...
Frontend/src/app/pages/admin/manage-groups/manage-groups.ts
View file @
68e5aeae
...
@@ -75,6 +75,9 @@ export class ManageGroupsComponent implements OnInit {
...
@@ -75,6 +75,9 @@ export class ManageGroupsComponent implements OnInit {
if
(
event
)
{
if
(
event
)
{
event
.
stopPropagation
();
event
.
stopPropagation
();
}
}
if
(
this
.
editingGroup
())
{
return
;
// Do nothing if we are editing a group name
}
if
(
this
.
expandedGroup
()
===
groupName
)
{
if
(
this
.
expandedGroup
()
===
groupName
)
{
this
.
expandedGroup
.
set
(
null
);
this
.
expandedGroup
.
set
(
null
);
}
else
{
}
else
{
...
...
Frontend/src/app/pages/candidate/take-quiz/take-quiz.css
View file @
68e5aeae
...
@@ -88,7 +88,8 @@
...
@@ -88,7 +88,8 @@
display
:
flex
;
display
:
flex
;
align-items
:
center
;
align-items
:
center
;
gap
:
6px
;
gap
:
6px
;
flex
:
0
1
350px
;
width
:
260px
;
flex-shrink
:
0
;
min-width
:
0
;
min-width
:
0
;
overflow
:
hidden
;
overflow
:
hidden
;
}
}
...
@@ -102,7 +103,14 @@
...
@@ -102,7 +103,14 @@
cursor
:
pointer
;
transition
:
all
0.2s
;
cursor
:
pointer
;
transition
:
all
0.2s
;
padding
:
0
;
padding
:
0
;
}
}
.dots-scroll-btn
:hover
{
background
:
var
(
--bg-hover
);
border-color
:
var
(
--border-strong
);
color
:
var
(
--text-primary
);
}
.dots-scroll-btn
:hover:not
(
:disabled
)
{
background
:
var
(
--bg-hover
);
border-color
:
var
(
--border-strong
);
color
:
var
(
--text-primary
);
}
.dots-scroll-btn
:disabled
{
opacity
:
0.35
;
cursor
:
not-allowed
;
background
:
var
(
--bg-card
);
border-color
:
var
(
--border-color
);
color
:
var
(
--text-muted
);
}
.dots-scroll-btn
.material-symbols-rounded
{
font-size
:
20px
;
}
.dots-scroll-btn
.material-symbols-rounded
{
font-size
:
20px
;
}
.question-dots
{
.question-dots
{
display
:
flex
;
gap
:
6px
;
display
:
flex
;
gap
:
6px
;
...
...
Frontend/src/app/pages/candidate/take-quiz/take-quiz.html
View file @
68e5aeae
...
@@ -95,15 +95,15 @@
...
@@ -95,15 +95,15 @@
</button>
</button>
<div
class=
"dots-wrapper"
>
<div
class=
"dots-wrapper"
>
<button
class=
"dots-scroll-btn"
(click)=
"scrollDots(-1)"
aria-label=
"Scroll left"
>
<button
class=
"dots-scroll-btn"
(click)=
"scrollDots(-1)"
[disabled]=
"!canScrollLeft()"
aria-label=
"Scroll left"
>
<span
class=
"material-symbols-rounded"
>
chevron_left
</span>
<span
class=
"material-symbols-rounded"
>
chevron_left
</span>
</button>
</button>
<div
class=
"question-dots"
#dotsContainer
>
<div
class=
"question-dots"
#dotsContainer
(scroll)=
"updateScrollButtons()"
>
@for (q of questions(); track q._id; let i = $index) {
@for (q of questions(); track q._id; let i = $index) {
<button
class=
"dot"
[class.active]=
"i === currentIndex()"
[class.answered]=
"isAnswered(i)"
(click)=
"goTo(i)"
>
{{ i + 1 }}
</button>
<button
class=
"dot"
[class.active]=
"i === currentIndex()"
[class.answered]=
"isAnswered(i)"
(click)=
"goTo(i)"
>
{{ i + 1 }}
</button>
}
}
</div>
</div>
<button
class=
"dots-scroll-btn"
(click)=
"scrollDots(1)"
aria-label=
"Scroll right"
>
<button
class=
"dots-scroll-btn"
(click)=
"scrollDots(1)"
[disabled]=
"!canScrollRight()"
aria-label=
"Scroll right"
>
<span
class=
"material-symbols-rounded"
>
chevron_right
</span>
<span
class=
"material-symbols-rounded"
>
chevron_right
</span>
</button>
</button>
</div>
</div>
...
...
Frontend/src/app/pages/candidate/take-quiz/take-quiz.ts
View file @
68e5aeae
...
@@ -39,6 +39,9 @@ export class TakeQuizComponent implements OnInit, OnDestroy {
...
@@ -39,6 +39,9 @@ export class TakeQuizComponent implements OnInit, OnDestroy {
violationCount
=
signal
(
0
);
violationCount
=
signal
(
0
);
isFullscreen
=
signal
(
false
);
isFullscreen
=
signal
(
false
);
canScrollLeft
=
signal
(
false
);
canScrollRight
=
signal
(
false
);
private
antiCheatSubs
:
Subscription
[]
=
[];
private
antiCheatSubs
:
Subscription
[]
=
[];
// ─────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────
...
@@ -144,6 +147,8 @@ export class TakeQuizComponent implements OnInit, OnDestroy {
...
@@ -144,6 +147,8 @@ export class TakeQuizComponent implements OnInit, OnDestroy {
this
.
startTimer
();
this
.
startTimer
();
// Start anti-cheat after quiz loads
// Start anti-cheat after quiz loads
this
.
startAntiCheat
();
this
.
startAntiCheat
();
// Scroll active dot into view and initialize scroll buttons
this
.
scrollActiveDotIntoView
();
},
},
error
:
(
err
)
=>
{
error
:
(
err
)
=>
{
this
.
error
.
set
(
err
.
error
?.
message
||
'
Failed to load quiz
'
);
this
.
error
.
set
(
err
.
error
?.
message
||
'
Failed to load quiz
'
);
...
@@ -201,7 +206,10 @@ export class TakeQuizComponent implements OnInit, OnDestroy {
...
@@ -201,7 +206,10 @@ export class TakeQuizComponent implements OnInit, OnDestroy {
// ─────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────
goTo
(
index
:
number
):
void
{
goTo
(
index
:
number
):
void
{
if
(
index
>=
0
&&
index
<
this
.
questions
().
length
)
this
.
currentIndex
.
set
(
index
);
if
(
index
>=
0
&&
index
<
this
.
questions
().
length
)
{
this
.
currentIndex
.
set
(
index
);
this
.
scrollActiveDotIntoView
();
}
}
}
prev
():
void
{
this
.
goTo
(
this
.
currentIndex
()
-
1
);
}
prev
():
void
{
this
.
goTo
(
this
.
currentIndex
()
-
1
);
}
...
@@ -209,7 +217,44 @@ export class TakeQuizComponent implements OnInit, OnDestroy {
...
@@ -209,7 +217,44 @@ export class TakeQuizComponent implements OnInit, OnDestroy {
scrollDots
(
direction
:
number
):
void
{
scrollDots
(
direction
:
number
):
void
{
const
el
=
this
.
dotsContainer
?.
nativeElement
;
const
el
=
this
.
dotsContainer
?.
nativeElement
;
if
(
el
)
el
.
scrollBy
({
left
:
direction
*
120
,
behavior
:
'
smooth
'
});
if
(
el
)
{
el
.
scrollBy
({
left
:
direction
*
120
,
behavior
:
'
smooth
'
});
setTimeout
(()
=>
this
.
updateScrollButtons
(),
300
);
}
}
updateScrollButtons
():
void
{
const
el
=
this
.
dotsContainer
?.
nativeElement
;
if
(
el
)
{
// Allow a small tolerance for rounding errors in scroll position
this
.
canScrollLeft
.
set
(
el
.
scrollLeft
>
1
);
this
.
canScrollRight
.
set
(
el
.
scrollLeft
+
el
.
clientWidth
<
el
.
scrollWidth
-
1
);
}
}
scrollActiveDotIntoView
():
void
{
setTimeout
(()
=>
{
const
container
=
this
.
dotsContainer
?.
nativeElement
;
if
(
!
container
)
return
;
const
activeDot
=
container
.
querySelector
(
'
.dot.active
'
)
as
HTMLElement
;
if
(
!
activeDot
)
return
;
const
containerWidth
=
container
.
clientWidth
;
const
dotLeft
=
activeDot
.
offsetLeft
;
const
dotWidth
=
activeDot
.
clientWidth
;
// Center the active dot
const
targetScrollLeft
=
dotLeft
-
(
containerWidth
/
2
)
+
(
dotWidth
/
2
);
container
.
scrollTo
({
left
:
targetScrollLeft
,
behavior
:
'
smooth
'
});
// Update buttons after smooth scroll animation is likely finished
setTimeout
(()
=>
this
.
updateScrollButtons
(),
300
);
},
50
);
}
}
// ─────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────
...
...
Frontend/src/app/pages/hr/dashboard/dashboard.css
View file @
68e5aeae
...
@@ -110,24 +110,63 @@
...
@@ -110,24 +110,63 @@
/* Quick Actions */
/* Quick Actions */
.actions-grid
{
.actions-grid
{
display
:
grid
;
display
:
grid
;
grid-template-columns
:
repeat
(
4
,
1
fr
);
grid-template-columns
:
repeat
(
auto-fit
,
minmax
(
240px
,
1
fr
)
);
gap
:
16px
;
gap
:
16px
;
}
}
.action-card
{
.action-card
{
display
:
flex
;
display
:
flex
;
align-items
:
flex-start
;
align-items
:
center
;
gap
:
16px
;
gap
:
16px
;
text-decoration
:
none
;
text-decoration
:
none
;
color
:
inherit
;
color
:
inherit
;
height
:
100%
;
height
:
100%
;
padding
:
18px
!important
;
background
:
var
(
--bg-card
);
border
:
1px
solid
var
(
--border-color
);
border-radius
:
16px
;
transition
:
all
0.3s
cubic-bezier
(
0.4
,
0
,
0.2
,
1
);
position
:
relative
;
overflow
:
hidden
;
}
.action-card
::before
{
content
:
''
;
position
:
absolute
;
top
:
0
;
left
:
0
;
right
:
0
;
bottom
:
0
;
background
:
linear-gradient
(
135deg
,
rgba
(
102
,
126
,
234
,
0.05
)
0%
,
transparent
100%
);
opacity
:
0
;
transition
:
opacity
0.3s
;
}
.action-card
:hover::before
{
opacity
:
1
;
}
.action-card
:hover
{
border-color
:
var
(
--accent-primary
);
box-shadow
:
0
12px
30px
rgba
(
102
,
126
,
234
,
0.12
);
transform
:
translateY
(
-4px
);
}
}
.action-icon
{
.action-icon
{
font-size
:
28px
;
width
:
48px
;
height
:
48px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
background
:
rgba
(
102
,
126
,
234
,
0.1
);
color
:
var
(
--accent-primary
);
color
:
var
(
--accent-primary
);
border-radius
:
12px
;
font-size
:
24px
!important
;
flex-shrink
:
0
;
flex-shrink
:
0
;
margin-top
:
2px
;
transition
:
all
0.3s
;
}
.action-card
:hover
.action-icon
{
background
:
var
(
--accent-primary
);
color
:
#fff
;
transform
:
scale
(
1.1
)
rotate
(
-5deg
);
}
}
.action-info
{
.action-info
{
...
...
Frontend/src/app/pages/hr/group-interview/group-interview.html
View file @
68e5aeae
...
@@ -258,8 +258,11 @@
...
@@ -258,8 +258,11 @@
<input
class=
"form-input"
[(ngModel)]=
"newGroupInterview.source"
placeholder=
"e.g., LinkedIn, Campus"
>
<input
class=
"form-input"
[(ngModel)]=
"newGroupInterview.source"
placeholder=
"e.g., LinkedIn, Campus"
>
</div>
</div>
<div
class=
"form-group"
>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Date of Interview
</label>
<label
class=
"form-label"
>
Date
&
Time
</label>
<input
class=
"form-input"
type=
"date"
[(ngModel)]=
"newGroupInterview.dateOfInterview"
>
<div
style=
"display: flex; gap: 8px;"
>
<input
class=
"form-input"
type=
"date"
[(ngModel)]=
"newGroupInterview.dateOfInterview"
style=
"flex: 1;"
>
<input
class=
"form-input"
type=
"time"
[(ngModel)]=
"newGroupInterview.timeOfInterview"
style=
"flex: 1;"
>
</div>
</div>
</div>
</div>
</div>
<div
class=
"form-row"
>
<div
class=
"form-row"
>
...
...
Frontend/src/app/pages/hr/group-interview/group-interview.ts
View file @
68e5aeae
...
@@ -145,7 +145,8 @@ export class HRGroupInterviewComponent implements OnInit {
...
@@ -145,7 +145,8 @@ export class HRGroupInterviewComponent implements OnInit {
position
:
''
,
position
:
''
,
techStack
:
''
,
techStack
:
''
,
source
:
''
,
source
:
''
,
dateOfInterview
:
new
Date
().
toISOString
().
split
(
'
T
'
)[
0
]
dateOfInterview
:
new
Date
().
toISOString
().
split
(
'
T
'
)[
0
],
timeOfInterview
:
''
};
};
}
}
...
...
Frontend/src/app/pages/hr/individual-interview/individual-interview.html
View file @
68e5aeae
...
@@ -184,8 +184,11 @@
...
@@ -184,8 +184,11 @@
<input
class=
"form-input"
[(ngModel)]=
"newInterview.source"
placeholder=
"e.g., LinkedIn, Campus"
>
<input
class=
"form-input"
[(ngModel)]=
"newInterview.source"
placeholder=
"e.g., LinkedIn, Campus"
>
</div>
</div>
<div
class=
"form-group"
>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Date of Interview
</label>
<label
class=
"form-label"
>
Date
&
Time
</label>
<input
class=
"form-input"
type=
"date"
[(ngModel)]=
"newInterview.dateOfInterview"
>
<div
style=
"display: flex; gap: 8px;"
>
<input
class=
"form-input"
type=
"date"
[(ngModel)]=
"newInterview.dateOfInterview"
style=
"flex: 1;"
>
<input
class=
"form-input"
type=
"time"
[(ngModel)]=
"newInterview.timeOfInterview"
style=
"flex: 1;"
>
</div>
</div>
</div>
</div>
</div>
...
...
Frontend/src/app/pages/hr/individual-interview/individual-interview.ts
View file @
68e5aeae
...
@@ -34,6 +34,7 @@ export class HRIndividualInterviewComponent implements OnInit {
...
@@ -34,6 +34,7 @@ export class HRIndividualInterviewComponent implements OnInit {
techStack
:
''
,
techStack
:
''
,
source
:
''
,
source
:
''
,
dateOfInterview
:
new
Date
().
toISOString
().
split
(
'
T
'
)[
0
],
dateOfInterview
:
new
Date
().
toISOString
().
split
(
'
T
'
)[
0
],
timeOfInterview
:
''
,
quizIds
:
[]
as
string
[]
quizIds
:
[]
as
string
[]
};
};
...
@@ -95,7 +96,7 @@ export class HRIndividualInterviewComponent implements OnInit {
...
@@ -95,7 +96,7 @@ export class HRIndividualInterviewComponent implements OnInit {
});
});
this
.
newInterview
=
{
this
.
newInterview
=
{
candidateId
:
''
,
assignedInterviewers
:
[],
assignedHRs
:
[],
assignedPMs
:
[],
position
:
''
,
techStack
:
''
,
candidateId
:
''
,
assignedInterviewers
:
[],
assignedHRs
:
[],
assignedPMs
:
[],
position
:
''
,
techStack
:
''
,
source
:
''
,
dateOfInterview
:
new
Date
().
toISOString
().
split
(
'
T
'
)[
0
],
quizIds
:
[]
source
:
''
,
dateOfInterview
:
new
Date
().
toISOString
().
split
(
'
T
'
)[
0
],
timeOfInterview
:
''
,
quizIds
:
[]
};
};
this
.
showCreateModal
.
set
(
true
);
this
.
showCreateModal
.
set
(
true
);
}
}
...
...
Frontend/src/app/pages/hr/manage-groups/manage-groups.css
View file @
68e5aeae
...
@@ -39,7 +39,7 @@
...
@@ -39,7 +39,7 @@
.group-lane
:hover
{
border-color
:
rgba
(
102
,
126
,
234
,
0.4
);
transform
:
translateY
(
-4px
);
box-shadow
:
0
12px
24px
rgba
(
0
,
0
,
0
,
0.1
);
}
.group-lane
:hover
{
border-color
:
rgba
(
102
,
126
,
234
,
0.4
);
transform
:
translateY
(
-4px
);
box-shadow
:
0
12px
24px
rgba
(
0
,
0
,
0
,
0.1
);
}
.group-lane.cdk-drop-list-receiving
{
border-color
:
var
(
--accent-primary
);
background
:
rgba
(
102
,
126
,
234
,
0.05
);
box-shadow
:
0
0
0
2px
rgba
(
102
,
126
,
234
,
0.2
);
transform
:
scale
(
1.02
);
z-index
:
10
;
}
.group-lane.cdk-drop-list-receiving
{
border-color
:
var
(
--accent-primary
);
background
:
rgba
(
102
,
126
,
234
,
0.05
);
box-shadow
:
0
0
0
2px
rgba
(
102
,
126
,
234
,
0.2
);
transform
:
scale
(
1.02
);
z-index
:
10
;
}
.group-lane-header
{
padding
:
18px
20px
;
background
:
rgba
(
102
,
126
,
234
,
0.05
);
border-bottom
:
1px
solid
transparent
;
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
transition
:
background
0.3s
;
pointer-events
:
none
;
}
.group-lane-header
{
padding
:
18px
20px
;
background
:
rgba
(
102
,
126
,
234
,
0.05
);
border-bottom
:
1px
solid
transparent
;
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
transition
:
background
0.3s
;
pointer-events
:
auto
;
}
.group-lane.expanded
.group-lane-header
{
background
:
rgba
(
102
,
126
,
234
,
0.1
);
border-bottom
:
1px
solid
var
(
--border-color
);
}
.group-lane.expanded
.group-lane-header
{
background
:
rgba
(
102
,
126
,
234
,
0.1
);
border-bottom
:
1px
solid
var
(
--border-color
);
}
.group-actions
{
pointer-events
:
auto
;
}
.group-actions
{
pointer-events
:
auto
;
}
.group-lane-body
{
cursor
:
default
;
overflow
:
hidden
;
}
.group-lane-body
{
cursor
:
default
;
overflow
:
hidden
;
}
...
...
Frontend/src/app/pages/hr/manage-groups/manage-groups.ts
View file @
68e5aeae
...
@@ -70,6 +70,9 @@ export class HRManageGroupsComponent {
...
@@ -70,6 +70,9 @@ export class HRManageGroupsComponent {
if
(
event
)
{
if
(
event
)
{
event
.
stopPropagation
();
event
.
stopPropagation
();
}
}
if
(
this
.
editingGroup
())
{
return
;
// Do nothing if we are editing a group name
}
if
(
this
.
expandedGroup
()
===
groupName
)
{
if
(
this
.
expandedGroup
()
===
groupName
)
{
this
.
expandedGroup
.
set
(
null
);
this
.
expandedGroup
.
set
(
null
);
}
else
{
}
else
{
...
...
Frontend/src/styles.css
View file @
68e5aeae
...
@@ -381,6 +381,7 @@ ul, ol {
...
@@ -381,6 +381,7 @@ ul, ol {
.btn-primary
:hover:not
(
:disabled
)
{
.btn-primary
:hover:not
(
:disabled
)
{
transform
:
translateY
(
-1px
);
transform
:
translateY
(
-1px
);
box-shadow
:
0
4px
16px
rgba
(
var
(
--accent-primary-rgb
),
0.35
);
box-shadow
:
0
4px
16px
rgba
(
var
(
--accent-primary-rgb
),
0.35
);
color
:
#fff
;
}
}
.btn-outline
{
.btn-outline
{
...
@@ -403,6 +404,7 @@ ul, ol {
...
@@ -403,6 +404,7 @@ ul, ol {
.btn-danger
:hover:not
(
:disabled
)
{
.btn-danger
:hover:not
(
:disabled
)
{
background
:
#dc2626
;
background
:
#dc2626
;
transform
:
translateY
(
-1px
);
transform
:
translateY
(
-1px
);
color
:
#fff
;
}
}
.btn-ghost
{
.btn-ghost
{
...
...
ITL_Intern_Evaluation_Form.html
deleted
100644 → 0
View file @
99f0042f
This diff is collapsed.
Click to expand it.
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