Commit 9a0f25a3 authored by Aravind RK's avatar Aravind RK

feat : violation policy alert

parent 68e5aeae
......@@ -265,20 +265,20 @@
background: var(--danger);
color: #fff;
border-color: var(--danger);
}
}
/* ---- MAIN CONTENT ---- */
.main-content {
/* ---- MAIN CONTENT ---- */
.main-content {
flex: 1;
margin-left: var(--sidebar-width);
min-height: 100vh;
min-width: 0;
overflow-x: hidden;
transition: margin-left 0.3s ease;
}
}
/* ---- MOBILE HEADER ---- */
.mobile-header {
/* ---- MOBILE HEADER ---- */
.mobile-header {
display: none;
position: sticky;
top: 0;
......@@ -288,10 +288,10 @@
padding: 12px 16px;
align-items: center;
gap: 12px;
}
}
.mobile-menu-btn,
.icon-btn {
.mobile-menu-btn,
.icon-btn {
width: 40px;
height: 40px;
display: flex;
......
......@@ -38,9 +38,9 @@
<!-- Navigation -->
<nav class="sidebar-nav">
@for (item of navItems(); track item.route) {
@if (item.route === '/admin/users' || item.route === '/hr/users') {
@if (item.route === '/admin/users' || item.route === '/hr/users' || item.route === '/admin/interviews' || item.route === '/hr/interviews') {
<a class="nav-item" (click)="handleNavClick(item, $event)" href="javascript:void(0)"
[class.active]="router.url.includes('/admin/users') || router.url.includes('/hr/users') || router.url.includes('manage-groups')">
[class.active]="(item.route === '/admin/users' || item.route === '/hr/users') ? (router.url.includes('/admin/users') || router.url.includes('/hr/users') || router.url.includes('manage-groups')) : (router.url.includes('individual-interview') || router.url.includes('group-interview'))">
<span class="material-symbols-rounded nav-icon">{{ item.icon }}</span>
<span class="nav-label">{{ item.label }}</span>
</a>
......
......@@ -41,6 +41,7 @@ export class LayoutComponent {
return [
{ icon: 'dashboard', label: 'Dashboard', route: '/admin/dashboard' },
{ icon: 'group', label: 'Users', route: '/admin/users' },
{ icon: 'work', label: 'Interviews', route: '/admin/interviews' },
{ icon: 'quiz', label: 'Quizzes', route: '/admin/quizzes' },
{ icon: 'add_circle', label: 'Create Quiz', route: '/admin/create-quiz' },
{ icon: 'person', label: 'Profile', route: '/admin/profile' },
......@@ -51,7 +52,7 @@ export class LayoutComponent {
{ icon: 'quiz', label: 'My Quizzes', route: '/hr/quizzes' },
{ icon: 'add_circle', label: 'Create Quiz', route: '/hr/create-quiz' },
{ icon: 'people', label: 'Candidates', route: '/hr/users' },
{ icon: 'work', label: 'Interviews', route: '/hr/individual-interview' },
{ icon: 'work', label: 'Interviews', route: '/hr/interviews' },
{ icon: 'person', label: 'Profile', route: '/hr/profile' },
];
case 'pm':
......@@ -120,6 +121,9 @@ export class LayoutComponent {
if (item.route === '/admin/users' || item.route === '/hr/users') {
event.preventDefault();
this.uiService.showManageUsersPopup.set(true);
} else if (item.route === '/admin/interviews' || item.route === '/hr/interviews') {
event.preventDefault();
this.uiService.showInterviewPopup.set(true);
} else {
this.mobileSidebarOpen = false;
}
......
......@@ -29,7 +29,7 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 15vw; /* Keeps it contained so it truncation kicks in */
max-width: 15vw;
}
@media (max-width: 768px) {
.quiz-meta .badge { max-width: 40vw; }
......@@ -51,7 +51,340 @@
.quiz-card-footer { display: flex; align-items: center; justify-content: space-between; margin-top: auto; }
/* Start Assessment Button */
.start-assessment-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 24px;
border-radius: 8px;
font-weight: 600;
font-size: 14px;
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.start-assessment-btn:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(79, 110, 247, 0.35);
}
.start-assessment-btn .material-symbols-rounded { font-size: 18px; }
/* ════════════════════════════════════════════════════════
Pre-Quiz Policy Modal
════════════════════════════════════════════════════════ */
.policy-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
padding: 20px;
animation: policyFadeIn 0.25s ease;
}
@keyframes policyFadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes policySlideUp {
from { opacity: 0; transform: translateY(24px) scale(0.97); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.policy-modal {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 20px;
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.18), 0 0 0 1px rgba(255,255,255,0.05);
width: 100%;
max-width: 720px;
max-height: 90vh;
display: flex;
flex-direction: column;
animation: policySlideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
overflow: hidden;
}
/* ── Header ── */
.policy-modal-header {
display: flex;
align-items: center;
gap: 16px;
padding: 24px 28px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
.policy-shield-wrap {
width: 52px; height: 52px; border-radius: 14px; flex-shrink: 0;
background: linear-gradient(135deg, var(--accent-primary-light), rgba(124, 92, 252, 0.12));
display: flex; align-items: center; justify-content: center;
border: 1px solid var(--border-color);
}
.policy-shield-icon {
font-size: 28px !important;
color: var(--accent-primary);
}
.policy-header-text { flex: 1; }
.policy-header-text h2 {
margin: 0 0 4px;
font-size: 20px; font-weight: 700;
color: var(--text-primary);
}
.policy-header-text p {
margin: 0;
font-size: 13px; color: var(--text-muted);
}
.policy-close-btn {
width: 36px; height: 36px; border-radius: 50%;
border: 1px solid var(--border-color);
background: var(--bg-tertiary);
color: var(--text-muted);
display: flex; align-items: center; justify-content: center;
cursor: pointer; transition: all 0.2s; flex-shrink: 0;
}
.policy-close-btn:hover { background: var(--danger-light); color: var(--danger); border-color: var(--danger-border); }
.policy-close-btn .material-symbols-rounded { font-size: 20px; }
/* ── Warning Banner ── */
.policy-warning-banner {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 28px;
background: var(--warning-light);
border-bottom: 1px solid var(--warning-border);
font-size: 13px;
color: var(--warning);
font-weight: 600;
flex-shrink: 0;
}
.policy-warning-banner .material-symbols-rounded { font-size: 18px; flex-shrink: 0; }
/* ── Body ── */
.policy-body {
flex: 1;
overflow-y: auto;
padding: 24px 28px;
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) transparent;
}
.policy-body::-webkit-scrollbar { width: 5px; }
.policy-body::-webkit-scrollbar-track { background: transparent; }
.policy-body::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 3px; }
.policy-section-title {
display: flex; align-items: center; gap: 10px;
font-size: 14px; font-weight: 700; text-transform: uppercase;
letter-spacing: 0.6px; color: var(--text-secondary);
margin: 0 0 16px;
}
.policy-section-title .material-symbols-rounded { font-size: 18px; color: var(--accent-primary); }
/* ── Rule Cards Grid ── */
.policy-rules-grid {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 24px;
}
.policy-rule-card {
display: flex;
align-items: flex-start;
gap: 14px;
padding: 14px 16px;
background: var(--bg-secondary);
border: 1px solid var(--border-subtle);
border-radius: 12px;
transition: border-color 0.2s, box-shadow 0.2s;
position: relative;
}
.policy-rule-card:hover {
border-color: var(--border-strong);
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.policy-rule-card.danger-card {
background: var(--danger-light);
border-color: var(--danger-border);
}
/* Rule icon variants */
.rule-icon-wrap {
width: 40px; height: 40px; border-radius: 10px; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
}
.rule-icon-wrap .material-symbols-rounded { font-size: 20px; }
.rule-icon-wrap.fullscreen { background: rgba(79, 110, 247, 0.12); color: var(--accent-primary); }
.rule-icon-wrap.fullscreen .material-symbols-rounded { color: var(--accent-primary); }
.rule-icon-wrap.tabs { background: rgba(245, 158, 11, 0.12); }
.rule-icon-wrap.tabs .material-symbols-rounded { color: var(--warning); }
.rule-icon-wrap.window { background: rgba(59, 130, 246, 0.12); }
.rule-icon-wrap.window .material-symbols-rounded { color: var(--info); }
.rule-icon-wrap.search { background: rgba(124, 92, 252, 0.12); }
.rule-icon-wrap.search .material-symbols-rounded { color: #7c5cfc; }
.rule-icon-wrap.copy { background: rgba(34, 197, 94, 0.12); }
.rule-icon-wrap.copy .material-symbols-rounded { color: var(--success); }
.rule-icon-wrap.auto-submit { background: rgba(239, 68, 68, 0.12); }
.rule-icon-wrap.auto-submit .material-symbols-rounded { color: var(--danger); }
.rule-content { flex: 1; min-width: 0; }
.rule-content h4 { margin: 0 0 4px; font-size: 14px; font-weight: 600; color: var(--text-primary); }
.rule-content p { margin: 0; font-size: 13px; color: var(--text-secondary); line-height: 1.6; }
.rule-content kbd {
display: inline-block;
padding: 1px 6px; border-radius: 4px;
background: var(--bg-tertiary); border: 1px solid var(--border-strong);
font-size: 11px; font-weight: 600; color: var(--text-primary);
font-family: monospace;
}
.violation-chip {
flex-shrink: 0;
padding: 3px 10px;
border-radius: 99px;
font-size: 11px; font-weight: 700;
background: var(--warning-light);
color: var(--warning);
border: 1px solid var(--warning-border);
white-space: nowrap;
align-self: flex-start;
margin-top: 2px;
}
.violation-chip.danger {
background: var(--danger-light);
color: var(--danger);
border-color: var(--danger-border);
}
/* ── Violation Tracker ── */
.policy-tracker {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 14px;
padding: 18px 20px;
}
.tracker-label {
font-size: 12px; font-weight: 700; text-transform: uppercase;
letter-spacing: 0.6px; color: var(--text-muted);
margin-bottom: 16px;
}
.tracker-steps {
display: flex;
align-items: center;
gap: 0;
}
.tracker-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
text-align: center;
font-size: 12px;
color: var(--text-secondary);
font-weight: 500;
flex: 1;
}
.tracker-step small { display: block; color: var(--text-muted); font-size: 11px; font-weight: 400; }
.tracker-connector {
height: 3px;
flex: 0 0 40px;
background: var(--border-color);
border-radius: 2px;
margin-bottom: 28px;
}
.tracker-dot {
width: 20px; height: 20px; border-radius: 50%;
border: 3px solid var(--border-strong);
background: var(--bg-card);
transition: all 0.3s;
}
.tracker-dot.warn { background: var(--warning); border-color: var(--warning); box-shadow: 0 0 10px rgba(245,158,11,0.4); }
.tracker-dot.danger { background: var(--danger); border-color: var(--danger); box-shadow: 0 0 10px rgba(239,68,68,0.4); }
/* ── Footer ── */
.policy-footer {
padding: 20px 28px;
border-top: 1px solid var(--border-color);
display: flex;
flex-direction: column;
gap: 16px;
flex-shrink: 0;
background: var(--bg-secondary);
}
.policy-checkbox-label {
display: flex;
align-items: flex-start;
gap: 12px;
cursor: pointer;
padding: 14px 16px;
border-radius: 12px;
border: 2px solid var(--border-color);
background: var(--bg-card);
transition: all 0.25s;
font-size: 13px;
color: var(--text-secondary);
line-height: 1.6;
user-select: none;
}
.policy-checkbox-label.checked {
border-color: var(--accent-primary);
background: var(--accent-primary-light);
color: var(--text-primary);
}
.policy-checkbox-label:hover { border-color: var(--border-strong); }
.policy-checkbox {
width: 22px; height: 22px; border-radius: 6px; flex-shrink: 0;
border: 2px solid var(--border-strong);
background: var(--bg-tertiary);
display: flex; align-items: center; justify-content: center;
transition: all 0.2s;
margin-top: 1px;
}
.policy-checkbox.checked {
background: var(--accent-primary);
border-color: var(--accent-primary);
}
.policy-checkbox .material-symbols-rounded { font-size: 15px !important; color: #fff; }
.policy-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.policy-begin-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 11px 28px;
font-size: 14px; font-weight: 700;
border-radius: 10px;
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.policy-begin-btn:not(:disabled):hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(79, 110, 247, 0.4);
}
.policy-begin-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
transform: none;
}
.policy-begin-btn .material-symbols-rounded { font-size: 18px; }
/* ── Responsive ── */
@media (max-width: 768px) {
.page { padding: 20px; }
.quiz-grid { grid-template-columns: 1fr; }
.policy-modal { max-height: 95vh; }
.policy-modal-header { padding: 18px 20px; gap: 12px; }
.policy-body { padding: 18px 20px; }
.policy-footer { padding: 16px 20px; }
.policy-actions { flex-direction: column-reverse; }
.policy-actions .btn { width: 100%; justify-content: center; }
.tracker-steps { gap: 4px; }
.tracker-connector { flex: 0 0 20px; }
}
......@@ -115,9 +115,11 @@
</div>
</div>
} @else if (i === 0 || interview.quizzes[i-1].completed) {
<a [routerLink]="['/candidate/quiz', q.quizId?._id || q.quizId]" class="btn btn-primary" style="padding: 10px 24px; border-radius: 8px; font-weight: 600;">
<button class="btn btn-primary start-assessment-btn"
(click)="openPolicyModal(q.quizId?._id || q.quizId)">
<span class="material-symbols-rounded">play_arrow</span>
Start Assessment
</a>
</button>
}
</div>
</div>
......@@ -181,3 +183,161 @@
</div>
}
</div>
<!-- ══════════════════════════════════════════════════════
Pre-Quiz Violation Policy Modal
══════════════════════════════════════════════════════ -->
@if (showPolicyModal()) {
<div class="policy-overlay" (click)="closePolicyModal()">
<div class="policy-modal animate-scale-in" (click)="$event.stopPropagation()">
<!-- Header -->
<div class="policy-modal-header">
<div class="policy-shield-wrap">
<span class="material-symbols-rounded filled policy-shield-icon">security</span>
</div>
<div class="policy-header-text">
<h2>Assessment Integrity Policy</h2>
<p>Please read all rules carefully before you begin</p>
</div>
<button class="policy-close-btn" (click)="closePolicyModal()">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<!-- Warning Banner -->
<div class="policy-warning-banner">
<span class="material-symbols-rounded filled">warning</span>
<span>This assessment is monitored. Any violations may result in automatic disqualification.</span>
</div>
<!-- Violations List -->
<div class="policy-body">
<h3 class="policy-section-title">
<span class="material-symbols-rounded">gavel</span>
Prohibited Actions During Assessment
</h3>
<div class="policy-rules-grid">
<!-- Rule 1 -->
<div class="policy-rule-card">
<div class="rule-icon-wrap fullscreen">
<span class="material-symbols-rounded filled">fullscreen_exit</span>
</div>
<div class="rule-content">
<h4>Exiting Fullscreen Mode</h4>
<p>The assessment must be taken in fullscreen. Pressing <kbd>Esc</kbd>, using browser controls, or any action that exits fullscreen will be logged as a violation.</p>
</div>
<span class="violation-chip">Violation</span>
</div>
<!-- Rule 2 -->
<div class="policy-rule-card">
<div class="rule-icon-wrap tabs">
<span class="material-symbols-rounded filled">tab</span>
</div>
<div class="rule-content">
<h4>Switching Browser Tabs</h4>
<p>Navigating to any other browser tab or opening a new tab during the assessment is strictly prohibited and will trigger an immediate violation alert.</p>
</div>
<span class="violation-chip">Violation</span>
</div>
<!-- Rule 3 -->
<div class="policy-rule-card">
<div class="rule-icon-wrap window">
<span class="material-symbols-rounded filled">desktop_windows</span>
</div>
<div class="rule-content">
<h4>Switching Applications or Windows</h4>
<p>Alt+Tab, clicking outside the browser, opening any other application, or minimizing the browser window will be detected and counted as a violation.</p>
</div>
<span class="violation-chip">Violation</span>
</div>
<!-- Rule 4 -->
<div class="policy-rule-card">
<div class="rule-icon-wrap search">
<span class="material-symbols-rounded filled">manage_search</span>
</div>
<div class="rule-content">
<h4>Using External Resources</h4>
<p>Searching the internet, consulting notes, textbooks, or communicating with others during the assessment is a violation of academic integrity.</p>
</div>
<span class="violation-chip">Violation</span>
</div>
<!-- Rule 5 -->
<div class="policy-rule-card">
<div class="rule-icon-wrap copy">
<span class="material-symbols-rounded filled">content_copy</span>
</div>
<div class="rule-content">
<h4>Copy-Pasting or Screen Recording</h4>
<p>Copying question content, recording the screen, or taking screenshots during the assessment is strictly forbidden and may lead to disqualification.</p>
</div>
<span class="violation-chip">Violation</span>
</div>
<!-- Rule 6 — Auto Submit -->
<div class="policy-rule-card danger-card">
<div class="rule-icon-wrap auto-submit">
<span class="material-symbols-rounded filled">block</span>
</div>
<div class="rule-content">
<h4>Automatic Submission Threshold</h4>
<p>After <strong>3 violations</strong>, your assessment will be <strong>automatically submitted</strong> regardless of how many questions you have answered. Your current answers will be saved.</p>
</div>
<span class="violation-chip danger">Auto-Submit</span>
</div>
</div>
<!-- Violation Tracker Visual -->
<div class="policy-tracker">
<div class="tracker-label">Violation Tracker</div>
<div class="tracker-steps">
<div class="tracker-step">
<div class="tracker-dot warn"></div>
<span>1st Violation<br><small>Warning shown</small></span>
</div>
<div class="tracker-connector"></div>
<div class="tracker-step">
<div class="tracker-dot warn"></div>
<span>2nd Violation<br><small>Final warning</small></span>
</div>
<div class="tracker-connector"></div>
<div class="tracker-step">
<div class="tracker-dot danger"></div>
<span>3rd Violation<br><small>Auto-submitted</small></span>
</div>
</div>
</div>
</div>
<!-- Terms & Conditions -->
<div class="policy-footer">
<label class="policy-checkbox-label" [class.checked]="policyAccepted()" (click)="policyAccepted.set(!policyAccepted())">
<div class="policy-checkbox" [class.checked]="policyAccepted()">
@if (policyAccepted()) {
<span class="material-symbols-rounded filled">check</span>
}
</div>
<span>I have read and understood all the assessment policies. I agree to follow the integrity guidelines throughout this assessment.</span>
</label>
<div class="policy-actions">
<button class="btn btn-secondary" (click)="closePolicyModal()">Cancel</button>
<button class="btn btn-primary policy-begin-btn"
[disabled]="!policyAccepted()"
(click)="beginAssessment()">
<span class="material-symbols-rounded">rocket_launch</span>
Begin Assessment
</button>
</div>
</div>
</div>
</div>
}
import { Component, OnInit, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
import { Router, RouterLink } from '@angular/router';
import { AuthService } from '../../../services/auth.service';
import { QuizService } from '../../../services/quiz.service';
......@@ -16,7 +16,12 @@ export class CandidateDashboardComponent implements OnInit {
loading = signal(true);
uploadingCodingId = signal<string | null>(null);
constructor(public authService: AuthService, private quizService: QuizService) {}
// ── Pre-quiz Policy Modal ─────────────────────
showPolicyModal = signal(false);
policyAccepted = signal(false);
pendingQuizId = signal<string | null>(null);
constructor(public authService: AuthService, private quizService: QuizService, private router: Router) {}
ngOnInit(): void {
this.loadInterviews();
......@@ -32,6 +37,28 @@ export class CandidateDashboardComponent implements OnInit {
});
}
/** Called when candidate clicks "Start Assessment" — shows policy modal first */
openPolicyModal(quizId: string): void {
this.pendingQuizId.set(quizId);
this.policyAccepted.set(false);
this.showPolicyModal.set(true);
}
/** Dismiss modal without proceeding */
closePolicyModal(): void {
this.showPolicyModal.set(false);
this.pendingQuizId.set(null);
this.policyAccepted.set(false);
}
/** Navigate to quiz only after policy is accepted */
beginAssessment(): void {
const id = this.pendingQuizId();
if (!id || !this.policyAccepted()) return;
this.showPolicyModal.set(false);
this.router.navigate(['/candidate/quiz', id]);
}
onCodingZipSelected(event: any, interviewId: string): void {
const file = event.target.files[0];
if (file) {
......
......@@ -12,7 +12,7 @@
.profile-email { color: var(--text-secondary); font-size: 14px; margin: 0 0 2px; }
.profile-joined { color: var(--text-muted); font-size: 12px; margin: 0; }
.stats-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 32px; }
.stats-row { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; margin-bottom: 32px; }
.stat-card { display: flex; flex-direction: column; align-items: center; gap: 8px; text-align: center; }
.stat-icon { font-size: 28px; }
.stat-icon.blue { color: var(--accent-primary); }
......@@ -21,6 +21,19 @@
.stat-value { font-size: 28px; font-weight: 700; color: var(--text-primary); }
.stat-label { font-size: 13px; color: var(--text-muted); }
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
background: rgba(34, 197, 94, 0.1);
color: #22c55e;
border: 1px solid rgba(34, 197, 94, 0.2);
}
.section { margin-bottom: 32px; }
.section-title { font-size: 16px; font-weight: 600; color: var(--text-primary); margin: 0 0 16px; }
......
......@@ -43,23 +43,6 @@
</div>
</div>
<div class="stats-row">
<div class="stat-card card card-padding">
<span class="material-symbols-rounded stat-icon blue">assignment</span>
<span class="stat-value">{{ testsTaken }}</span>
<span class="stat-label">Tests Taken</span>
</div>
<div class="stat-card card card-padding">
<span class="material-symbols-rounded stat-icon green">trending_up</span>
<span class="stat-value">{{ avgScore }}%</span>
<span class="stat-label">Average Score</span>
</div>
<div class="stat-card card card-padding">
<span class="material-symbols-rounded stat-icon orange">emoji_events</span>
<span class="stat-value">{{ bestScore }}%</span>
<span class="stat-label">Best Score</span>
</div>
</div>
<!-- Topics of Interest Section -->
<div class="section topics-section card card-padding">
......@@ -119,12 +102,12 @@
</div>
<div class="section">
<h2 class="section-title">Test Results</h2>
<h2 class="section-title">Test History</h2>
@if (submissions().length === 0) {
<div class="empty-state">
<span class="material-symbols-rounded">assignment</span>
<h3>No results yet</h3>
<p>Take a quiz to see results here</p>
<h3>No test history yet</h3>
<p>Take a quiz to see your test history here</p>
</div>
} @else {
<div class="results-list">
......@@ -137,15 +120,9 @@
<h4>{{ sub.quizId?.title }}</h4>
<p>{{ sub.submittedAt | date:'medium' }} · {{ sub.timeTaken ? (sub.timeTaken + 's') : '—' }}</p>
</div>
<div class="result-score-wrap">
<span class="score-circle" [ngClass]="{
'good': sub.percentage >= 70,
'avg': sub.percentage >= 40 && sub.percentage < 70,
'poor': sub.percentage < 40
}">{{ sub.percentage }}%</span>
<span class="score-detail">{{ sub.score }}/{{ sub.totalMarks }}</span>
<div class="result-status-wrap">
<span class="status-badge">Submitted</span>
</div>
<a [routerLink]="['/candidate/results', sub._id]" class="btn btn-outline btn-sm">View</a>
</div>
}
</div>
......
.quiz-page { padding: 24px 40px; max-width: 900px; margin: 0 auto; }
.loading-center { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; gap: 16px; }
/* ════════════════════════════════════════════════════════
Take Quiz — Full Exam Layout
════════════════════════════════════════════════════════ */
.quiz-page {
min-height: 100vh;
background: var(--bg-secondary);
display: flex;
flex-direction: column;
}
/* Loading / Error / Success states */
.loading-center { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 80vh; gap: 16px; }
.loading-text { font-size: 14px; color: var(--text-muted); }
.error-state, .success-state { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; gap: 16px; text-align: center; }
.error-state, .success-state { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 80vh; gap: 16px; text-align: center; }
.error-state h2, .success-state h2 { color: var(--text-primary); font-size: 22px; margin: 0; }
.success-state p { color: var(--text-muted); }
.quiz-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; flex-wrap: wrap; gap: 16px; }
.quiz-title-area h1 { color: var(--text-primary); font-size: 20px; font-weight: 700; margin: 0 0 4px; }
.q-counter { color: var(--text-muted); font-size: 13px; }
/* ════════════════════════════════════════════════════════
Top Header Bar
════════════════════════════════════════════════════════ */
.quiz-topbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 28px;
background: var(--bg-card);
border-bottom: 1px solid var(--border-color);
box-shadow: var(--shadow-sm);
position: sticky;
top: 0;
z-index: 50;
flex-shrink: 0;
}
.topbar-left { display: flex; align-items: center; gap: 16px; }
.quiz-brand {
display: flex; align-items: center; gap: 8px;
flex-shrink: 0;
}
.brand-icon {
font-size: 22px !important;
color: var(--accent-primary);
}
.brand-name {
font-size: 16px; font-weight: 800;
background: var(--accent-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: -0.3px;
}
.topbar-divider {
width: 1px; height: 28px;
background: var(--border-color);
}
.quiz-title-block {}
.quiz-title-text {
font-size: 15px; font-weight: 700;
color: var(--text-primary); margin: 0 0 2px;
}
.quiz-subtitle {
font-size: 12px; color: var(--text-muted);
}
/* Right cluster groups violation badge + fullscreen indicator + timer */
.header-right-cluster { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.topbar-right { display: flex; align-items: center; gap: 10px; }
/* Violation Badge */
.violation-badge {
......@@ -19,7 +76,7 @@
color: var(--warning); padding: 7px 14px; border-radius: var(--radius-md);
font-size: 13px; font-weight: 700; transition: all 0.3s;
}
.violation-badge .material-symbols-rounded { font-size: 18px; }
.violation-badge .material-symbols-rounded { font-size: 16px; }
.violation-badge.critical {
background: var(--danger-light); border-color: var(--danger-border);
color: var(--danger); animation: pulse 1s infinite;
......@@ -28,47 +85,180 @@
/* Fullscreen indicator */
.fullscreen-indicator {
display: flex; align-items: center; justify-content: center;
width: 38px; height: 38px; border-radius: var(--radius-md);
border: 1px solid var(--border-color); background: var(--bg-card);
width: 36px; height: 36px; border-radius: var(--radius-md);
border: 1px solid var(--border-color); background: var(--bg-tertiary);
color: var(--text-muted); transition: all 0.3s; cursor: default;
}
.fullscreen-indicator.active {
border-color: var(--success-border); background: var(--success-light);
color: var(--success);
border-color: var(--success-border); background: var(--success-light); color: var(--success);
}
.fullscreen-indicator .material-symbols-rounded { font-size: 20px; }
.fullscreen-indicator .material-symbols-rounded { font-size: 18px; }
/* Timer */
.timer {
display: flex; align-items: center; gap: 8px;
background: var(--bg-card); border: 1px solid var(--border-color);
padding: 10px 20px; border-radius: var(--radius-md); transition: all 0.3s;
padding: 8px 18px; border-radius: var(--radius-md); transition: all 0.3s;
}
.timer .material-symbols-rounded { font-size: 20px; color: var(--text-muted); }
.timer .material-symbols-rounded { font-size: 18px; color: var(--text-muted); }
.timer.warning { border-color: var(--warning-border); background: var(--warning-light); }
.timer.danger { border-color: var(--danger-border); background: var(--danger-light); animation: pulse 1s infinite; }
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.7; } }
.timer-value { color: var(--text-primary); font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; }
.timer-value { color: var(--text-primary); font-size: 18px; font-weight: 700; font-variant-numeric: tabular-nums; }
.timer.warning .timer-value { color: var(--warning); }
.timer.danger .timer-value { color: var(--danger); }
.progress-track { width: 100%; height: 4px; background: var(--bg-tertiary); border-radius: 2px; margin-bottom: 24px; overflow: hidden; }
.progress-fill { height: 100%; background: var(--accent-gradient); border-radius: 2px; transition: width 0.3s ease; }
/* Progress Bar (thin strip under header) */
.quiz-progress-bar {
width: 100%; height: 3px;
background: var(--bg-tertiary); flex-shrink: 0;
}
.quiz-progress-fill {
height: 100%;
background: var(--accent-gradient);
transition: width 0.4s ease;
}
/* ════════════════════════════════════════════════════════
3-Column Exam Layout
════════════════════════════════════════════════════════ */
.exam-layout {
display: grid;
grid-template-columns: 240px 1fr 260px;
gap: 0;
flex: 1;
min-height: 0;
align-items: start;
}
/* ── Shared Panel Card ── */
.left-panel-card,
.right-panel-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 20px;
display: flex;
flex-direction: column;
gap: 14px;
}
.right-panel-card { gap: 12px; }
.panel-section-label {
font-size: 11px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.7px;
color: var(--text-muted);
}
/* ════════════════════════════════════════════════════════
Left Panel
════════════════════════════════════════════════════════ */
.exam-left-panel {
padding: 20px 14px 20px 20px;
display: flex;
flex-direction: column;
gap: 14px;
position: sticky;
top: 52px; /* height of topbar + progress bar */
}
.info-stat {
display: flex; align-items: center; gap: 12px;
}
.info-stat-icon {
width: 38px; height: 38px; border-radius: 10px; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
}
.info-stat-icon .material-symbols-rounded { font-size: 20px; }
.info-stat-icon.blue { background: var(--accent-primary-light); }
.info-stat-icon.blue .material-symbols-rounded { color: var(--accent-primary); }
.info-stat-icon.green { background: var(--success-light); }
.info-stat-icon.green .material-symbols-rounded { color: var(--success); }
.info-stat-icon.orange { background: var(--warning-light); }
.info-stat-icon.orange .material-symbols-rounded { color: var(--warning); }
.info-stat-icon.purple { background: rgba(124, 92, 252, 0.12); }
.info-stat-icon.purple .material-symbols-rounded { color: #7c5cfc; }
.info-stat-value {
font-size: 20px; font-weight: 700; color: var(--text-primary); line-height: 1;
}
.info-stat-label {
font-size: 11px; color: var(--text-muted); margin-top: 2px;
}
.warn-text { color: var(--warning) !important; }
.danger-text { color: var(--danger) !important; }
/* Completion bar */
.progress-card { }
.completion-bar-wrap {
display: flex; align-items: center; gap: 10px;
}
.completion-bar-track {
flex: 1; height: 8px; border-radius: 99px;
background: var(--bg-tertiary); overflow: hidden;
}
.completion-bar-fill {
height: 100%; border-radius: 99px;
background: var(--accent-gradient);
transition: width 0.4s ease;
}
.completion-pct {
font-size: 12px; font-weight: 700; color: var(--text-secondary);
white-space: nowrap;
}
.completion-hint {
font-size: 11px; color: var(--text-muted); margin: 0; line-height: 1.5;
}
/* ════════════════════════════════════════════════════════
Center — Main Question Area
════════════════════════════════════════════════════════ */
.exam-main {
padding: 24px 20px;
display: flex;
flex-direction: column;
gap: 20px;
min-height: calc(100vh - 56px);
}
.question-card { margin-bottom: 24px; }
.q-header { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
.q-badge { background: var(--accent-primary-light); color: var(--accent-primary); padding: 6px 14px; border-radius: var(--radius-sm); font-weight: 700; font-size: 14px; }
.q-text { color: var(--text-primary); font-size: 17px; font-weight: 600; line-height: 1.6; margin: 0 0 24px; }
/* Question Card */
.question-card { margin: 0; }
.q-header {
display: flex; align-items: center; gap: 12px; margin-bottom: 18px;
flex-wrap: wrap;
}
.q-badge {
background: var(--accent-primary-light); color: var(--accent-primary);
padding: 5px 12px; border-radius: var(--radius-sm);
font-weight: 700; font-size: 13px;
}
.q-counter-inline {
margin-left: auto;
font-size: 12px; color: var(--text-muted); font-weight: 500;
}
.q-text {
color: var(--text-primary); font-size: 17px; font-weight: 600;
line-height: 1.65; margin: 0 0 24px;
}
.options-list { display: flex; flex-direction: column; gap: 10px; }
.option-btn {
display: flex; align-items: center; gap: 14px;
width: 100%; padding: 14px 18px; background: var(--bg-input);
border: 1px solid var(--border-color); border-radius: var(--radius-md);
color: var(--text-primary); font-size: 14px; cursor: pointer;
width: 100%; padding: 14px 18px;
background: var(--bg-input); border: 1px solid var(--border-color);
border-radius: var(--radius-md); color: var(--text-primary);
font-size: 14px; cursor: pointer;
transition: all 0.2s; text-align: left; font-family: inherit;
}
.option-btn:hover { background: var(--bg-hover); border-color: var(--border-strong); }
.option-btn.selected { background: var(--accent-primary-light); border-color: var(--accent-primary); }
.option-btn.selected {
background: var(--accent-primary-light);
border-color: var(--accent-primary);
box-shadow: 0 0 0 2px rgba(79, 110, 247, 0.15);
}
.option-letter {
width: 32px; height: 32px; border-radius: var(--radius-sm);
background: var(--bg-tertiary); display: flex; align-items: center;
......@@ -79,152 +269,179 @@
.option-text { flex: 1; }
.check-icon { color: var(--accent-primary); font-size: 20px; }
/* Dots: scrollable strip with arrow buttons */
.quiz-nav { display: flex; justify-content: center; align-items: center; gap: 16px; position: relative; }
.quiz-nav .btn { position: absolute; }
.quiz-nav .btn:first-child { left: 0; }
.quiz-nav .btn:last-child { right: 0; }
.dots-wrapper {
display: flex;
align-items: center;
gap: 6px;
width: 260px;
flex-shrink: 0;
min-width: 0;
overflow: hidden;
/* Navigation */
.quiz-nav {
display: flex; justify-content: space-between; align-items: center;
padding: 8px 0;
}
.dots-scroll-btn {
flex-shrink: 0;
width: 32px; height: 32px; border-radius: 50%;
border: 1px solid var(--border-color);
background: var(--bg-card);
color: var(--text-secondary);
.btn-submit {
display: inline-flex; align-items: center; gap: 8px;
padding: 10px 24px; border-radius: var(--radius-md);
background: var(--success); color: #fff; border: none;
font-size: 14px; font-weight: 600; cursor: pointer;
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
font-family: inherit;
}
.btn-submit:hover:not(:disabled) {
background: color-mix(in srgb, var(--success) 85%, black);
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(34, 197, 94, 0.35);
}
.btn-submit:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
.btn-submit .material-symbols-rounded { font-size: 18px; }
.w-full { width: 100%; justify-content: center; }
/* ════════════════════════════════════════════════════════
Right Panel — Question Navigator
════════════════════════════════════════════════════════ */
.exam-right-panel {
padding: 20px 20px 20px 14px;
position: sticky;
top: 52px;
}
/* Legend */
.nav-legend {
display: flex; flex-direction: column; gap: 8px;
}
.legend-item {
display: flex; align-items: center; gap: 10px;
font-size: 12px; color: var(--text-secondary);
}
.legend-dot {
width: 22px; height: 22px; border-radius: 6px; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
cursor: pointer; transition: all 0.2s;
padding: 0;
font-size: 11px; font-weight: 700;
}
.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);
.legend-dot.answered { background: var(--success-light); border: 1.5px solid var(--success-border); }
.legend-dot.active { background: var(--accent-primary-light); border: 1.5px solid var(--accent-primary); }
.legend-dot.unanswered{ background: var(--bg-tertiary); border: 1.5px solid var(--border-color); }
.nav-divider {
height: 1px; background: var(--border-color); margin: 2px 0;
}
/* Question Grid */
.question-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 6px;
}
.grid-dot {
width: 100%; aspect-ratio: 1;
border-radius: 8px;
border: 1.5px solid var(--border-color);
background: var(--bg-tertiary);
color: var(--text-muted);
font-size: 12px; font-weight: 600;
cursor: pointer;
display: flex; align-items: center; justify-content: center;
font-family: inherit;
transition: all 0.18s ease;
}
.grid-dot:hover {
border-color: var(--accent-primary);
color: var(--accent-primary);
background: var(--accent-primary-light);
transform: scale(1.08);
}
.grid-dot.active {
background: var(--accent-primary);
border-color: var(--accent-primary);
color: #fff;
box-shadow: 0 3px 10px rgba(79, 110, 247, 0.4);
transform: scale(1.05);
}
.grid-dot.answered {
background: var(--success-light);
border-color: var(--success-border);
color: var(--success);
}
.grid-dot.active.answered {
background: var(--accent-primary);
border-color: var(--accent-primary);
color: #fff;
}
.right-submit-wrap {
margin-top: 4px;
}
.dots-scroll-btn .material-symbols-rounded { font-size: 20px; }
.question-dots {
display: flex; gap: 6px;
overflow-x: auto; flex: 1;
scroll-behavior: smooth;
scrollbar-width: none;
-ms-overflow-style: none;
padding: 4px 2px;
}
.question-dots::-webkit-scrollbar { display: none; }
.dot {
width: 32px; height: 32px; border-radius: var(--radius-sm); border: 1px solid var(--border-color);
background: transparent; color: var(--text-muted); font-size: 12px; font-weight: 600;
cursor: pointer; transition: all 0.2s; display: flex; align-items: center; justify-content: center; font-family: inherit;
}
.dot:hover { border-color: var(--border-strong); color: var(--text-primary); }
.dot.active { background: var(--accent-primary-light); border-color: var(--accent-primary); color: var(--accent-primary); }
.dot.answered { background: var(--success-light); border-color: var(--success-border); color: var(--success); }
.dot.active.answered { background: var(--accent-primary-light); border-color: var(--accent-primary); color: var(--accent-primary); }
/* ════════════════════════════════════════════════════
/* ════════════════════════════════════════════════════════
Anti-Cheat Warning Modal
════════════════════════════════════════════════════ */
════════════════════════════════════════════════════════ */
.ac-overlay {
position: fixed; inset: 0;
background: rgba(0, 0, 0, 0.65);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
display: flex; align-items: center; justify-content: center;
z-index: 9999; padding: 24px;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.ac-modal {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-xl);
padding: 40px 36px;
max-width: 460px; width: 100%;
text-align: center;
display: flex; flex-direction: column; align-items: center; gap: 16px;
background: var(--bg-card); border: 1px solid var(--border-color);
border-radius: var(--radius-xl); box-shadow: var(--shadow-xl);
padding: 40px 36px; max-width: 460px; width: 100%;
text-align: center; display: flex; flex-direction: column; align-items: center; gap: 16px;
}
.ac-icon-wrap {
width: 72px; height: 72px; border-radius: 50%;
background: var(--warning-light); border: 2px solid var(--warning-border);
display: flex; align-items: center; justify-content: center;
transition: all 0.3s;
}
.ac-icon-wrap.danger {
background: var(--danger-light); border-color: var(--danger-border);
}
.ac-icon {
font-size: 36px !important;
color: var(--warning);
display: flex; align-items: center; justify-content: center; transition: all 0.3s;
}
.ac-icon-wrap.danger { background: var(--danger-light); border-color: var(--danger-border); }
.ac-icon { font-size: 36px !important; color: var(--warning); }
.ac-icon-wrap.danger .ac-icon { color: var(--danger); }
.ac-title { color: var(--text-primary); font-size: 20px; font-weight: 700; margin: 0; line-height: 1.3; }
.ac-message { color: var(--text-secondary); font-size: 14px; line-height: 1.7; margin: 0; }
.ac-title {
color: var(--text-primary);
font-size: 20px; font-weight: 700;
margin: 0; line-height: 1.3;
}
.ac-message {
color: var(--text-secondary);
font-size: 14px; line-height: 1.7;
margin: 0;
}
/* Violation progress dots */
.ac-violation-dots {
display: flex; gap: 10px; align-items: center; justify-content: center;
}
.ac-violation-dots { display: flex; gap: 10px; align-items: center; justify-content: center; }
.ac-dot {
width: 14px; height: 14px; border-radius: 50%;
background: var(--bg-tertiary); border: 2px solid var(--border-color);
transition: all 0.3s;
}
.ac-dot.filled {
background: var(--danger); border-color: var(--danger);
box-shadow: 0 0 8px rgba(239, 68, 68, 0.4);
}
.ac-dot-label {
color: var(--text-muted); font-size: 12px; margin: -4px 0 0;
background: var(--bg-tertiary); border: 2px solid var(--border-color); transition: all 0.3s;
}
.ac-dot.filled { background: var(--danger); border-color: var(--danger); box-shadow: 0 0 8px rgba(239,68,68,0.4); }
.ac-dot-label { color: var(--text-muted); font-size: 12px; margin: -4px 0 0; }
.ac-dismiss-btn {
width: 100%; justify-content: center; gap: 8px; font-size: 14px;
padding: 12px 24px; border-radius: var(--radius-md);
}
.ac-dismiss-btn { width: 100%; justify-content: center; gap: 8px; font-size: 14px; padding: 12px 24px; border-radius: var(--radius-md); }
.ac-note { color: var(--text-muted); font-size: 12px; margin: 0; line-height: 1.5; }
.ac-submitting { display: flex; flex-direction: column; align-items: center; gap: 12px; color: var(--text-muted); font-size: 14px; }
.ac-note {
color: var(--text-muted); font-size: 12px; margin: 0; line-height: 1.5;
/* ════════════════════════════════════════════════════════
Responsive
════════════════════════════════════════════════════════ */
@media (max-width: 1100px) {
.exam-layout { grid-template-columns: 200px 1fr 220px; }
}
.ac-submitting {
display: flex; flex-direction: column; align-items: center; gap: 12px;
color: var(--text-muted); font-size: 14px;
@media (max-width: 900px) {
.exam-layout {
grid-template-columns: 1fr;
grid-template-rows: auto auto auto;
}
.exam-left-panel,
.exam-right-panel { position: static; padding: 16px; }
.exam-left-panel { order: 2; }
.exam-main { order: 1; padding: 16px; }
.exam-right-panel { order: 3; }
.left-panel-card, .right-panel-card { flex-direction: row; flex-wrap: wrap; gap: 12px; }
.info-stat { flex: 1 1 120px; }
.question-grid { grid-template-columns: repeat(8, 1fr); }
}
/* ════════════════════════════════════════════════════
Responsive
════════════════════════════════════════════════════ */
@media (max-width: 640px) {
.quiz-page { padding: 16px; }
.question-card { padding: 20px; }
.quiz-nav { flex-direction: column; position: static; }
.quiz-nav .btn { position: static; width: 100%; }
.dots-wrapper { order: -1; width: 100%; flex: 1; max-width: none; }
.header-right-cluster { width: 100%; justify-content: flex-end; }
@media (max-width: 600px) {
.quiz-topbar { padding: 10px 16px; gap: 10px; }
.quiz-title-text { font-size: 13px; }
.quiz-subtitle { display: none; }
.q-text { font-size: 15px; }
.exam-main { padding: 12px; }
.quiz-nav { gap: 10px; }
.quiz-nav .btn { font-size: 13px; padding: 8px 16px; }
.question-grid { grid-template-columns: repeat(6, 1fr); }
.ac-modal { padding: 28px 20px; }
}
<!-- Take Quiz -->
<!-- Take Quiz — Full Exam Layout -->
<div class="quiz-page" #quizContainer>
<!-- ── Loading ─────────────────────────────── -->
......@@ -27,15 +27,22 @@
<!-- ── Active Quiz ────────────────────────── -->
} @else if (quiz() && currentQuestion()) {
<!-- Quiz Header -->
<div class="quiz-header">
<div class="quiz-title-area">
<h1>{{ quiz().title }}</h1>
<span class="q-counter">Question {{ currentIndex() + 1 }} of {{ questions().length }}</span>
<!-- ══ Top Header Bar ══ -->
<div class="quiz-topbar">
<div class="topbar-left">
<div class="quiz-brand">
<span class="material-symbols-rounded filled brand-icon">quiz</span>
<span class="brand-name">QuizMaster</span>
</div>
<div class="topbar-divider"></div>
<div class="quiz-title-block">
<h1 class="quiz-title-text">{{ quiz().title }}</h1>
<span class="quiz-subtitle">Online Assessment · {{ questions().length }} Questions</span>
</div>
</div>
<div class="header-right-cluster">
<!-- Violation badge (shows only when there's at least 1 violation) -->
<div class="topbar-right">
<!-- Violation badge -->
@if (violationCount() > 0) {
<div class="violation-badge" [class.critical]="violationCount() >= getMaxViolations() - 1"
title="Violations detected. {{ getMaxViolations() - violationCount() }} warning(s) remaining.">
......@@ -45,7 +52,8 @@
}
<!-- Fullscreen indicator -->
<div class="fullscreen-indicator" [class.active]="isFullscreen()" title="{{ isFullscreen() ? 'Fullscreen active' : 'Not in fullscreen' }}">
<div class="fullscreen-indicator" [class.active]="isFullscreen()"
title="{{ isFullscreen() ? 'Fullscreen active' : 'Not in fullscreen' }}">
<span class="material-symbols-rounded">{{ isFullscreen() ? 'fullscreen' : 'fullscreen_exit' }}</span>
</div>
......@@ -57,20 +65,89 @@
</div>
</div>
<!-- Progress -->
<div class="progress-track"><div class="progress-fill" [style.width.%]="progress()"></div></div>
<!-- Progress Bar -->
<div class="quiz-progress-bar">
<div class="quiz-progress-fill" [style.width.%]="progress()"></div>
</div>
<!-- ══ 3-Column Exam Layout ══ -->
<div class="exam-layout">
<!-- ── Left Panel: Quiz Info ── -->
<aside class="exam-left-panel">
<div class="left-panel-card">
<div class="panel-section-label">Assessment Info</div>
<div class="info-stat">
<div class="info-stat-icon blue">
<span class="material-symbols-rounded filled">quiz</span>
</div>
<div>
<div class="info-stat-value">{{ questions().length }}</div>
<div class="info-stat-label">Total Questions</div>
</div>
</div>
<div class="info-stat">
<div class="info-stat-icon green">
<span class="material-symbols-rounded filled">check_circle</span>
</div>
<div>
<div class="info-stat-value">{{ getAnsweredCount() }}</div>
<div class="info-stat-label">Answered</div>
</div>
</div>
<div class="info-stat">
<div class="info-stat-icon orange">
<span class="material-symbols-rounded filled">pending</span>
</div>
<div>
<div class="info-stat-value">{{ questions().length - getAnsweredCount() }}</div>
<div class="info-stat-label">Remaining</div>
</div>
</div>
<div class="info-stat">
<div class="info-stat-icon purple">
<span class="material-symbols-rounded filled">timer</span>
</div>
<div>
<div class="info-stat-value" [class.warn-text]="timeLeft() < 300" [class.danger-text]="timeLeft() < 60">
{{ formattedTime() }}
</div>
<div class="info-stat-label">Time Remaining</div>
</div>
</div>
</div>
<!-- Progress donut visual -->
<div class="left-panel-card progress-card">
<div class="panel-section-label">Completion</div>
<div class="completion-bar-wrap">
<div class="completion-bar-track">
<div class="completion-bar-fill" [style.width.%]="(getAnsweredCount() / questions().length) * 100"></div>
</div>
<span class="completion-pct">{{ getAnsweredCount() }}/{{ questions().length }}</span>
</div>
<p class="completion-hint">Answer all questions before submitting.</p>
</div>
@if (error()) {
<div class="alert alert-error" style="margin-bottom:16px">
<div class="alert alert-error">
<span class="material-symbols-rounded">error</span>{{ error() }}
</div>
}
</aside>
<!-- ── Center: Question Area ── -->
<main class="exam-main">
<!-- Question Card -->
<div class="question-card card card-padding animate-fade-in">
<div class="q-header">
<span class="q-badge">Q{{ currentIndex() + 1 }}</span>
<span class="q-type badge badge-primary">{{ currentQuestion().type === 'mcq' ? 'Multiple Correct' : 'Single Correct' }}</span>
<span class="q-counter-inline">{{ currentIndex() + 1 }} of {{ questions().length }}</span>
</div>
<p class="q-text">{{ currentQuestion().question }}</p>
<div class="options-list">
......@@ -94,33 +171,75 @@
<span class="material-symbols-rounded">arrow_back</span> Previous
</button>
<div class="dots-wrapper">
<button class="dots-scroll-btn" (click)="scrollDots(-1)" [disabled]="!canScrollLeft()" aria-label="Scroll left">
<span class="material-symbols-rounded">chevron_left</span>
@if (currentIndex() < questions().length - 1) {
<button class="btn btn-primary" (click)="next()">
Next <span class="material-symbols-rounded">arrow_forward</span>
</button>
<div class="question-dots" #dotsContainer (scroll)="updateScrollButtons()">
@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>
} @else {
<button class="btn btn-submit" (click)="submitQuiz()" [disabled]="submitting()">
@if (submitting()) { <div class="spinner"></div> } @else {
<span class="material-symbols-rounded">send</span> Submit Quiz
}
</div>
<button class="dots-scroll-btn" (click)="scrollDots(1)" [disabled]="!canScrollRight()" aria-label="Scroll right">
<span class="material-symbols-rounded">chevron_right</span>
</button>
}
</div>
</main>
@if (currentIndex() < questions().length - 1) {
<button class="btn btn-primary" (click)="next()">Next <span class="material-symbols-rounded">arrow_forward</span></button>
} @else {
<button class="btn btn-primary" style="background:var(--success)" (click)="submitQuiz()" [disabled]="submitting()">
@if (submitting()) { <div class="spinner"></div> } @else { Submit }
<!-- ── Right Panel: Question Navigator ── -->
<aside class="exam-right-panel">
<div class="right-panel-card">
<div class="panel-section-label">Question Navigator</div>
<!-- Legend -->
<div class="nav-legend">
<div class="legend-item">
<div class="legend-dot answered"></div>
<span>Answered ({{ getAnsweredCount() }})</span>
</div>
<div class="legend-item">
<div class="legend-dot active"></div>
<span>Current</span>
</div>
<div class="legend-item">
<div class="legend-dot unanswered"></div>
<span>Not Answered ({{ questions().length - getAnsweredCount() }})</span>
</div>
</div>
<div class="nav-divider"></div>
<!-- Question Grid -->
<div class="panel-section-label" style="margin-top: 4px;">All Questions</div>
<div class="question-grid">
@for (q of questions(); track q._id; let i = $index) {
<button class="grid-dot"
[class.active]="i === currentIndex()"
[class.answered]="isAnswered(i)"
(click)="goTo(i)"
[title]="'Question ' + (i + 1)">
{{ i + 1 }}
</button>
}
</div>
<!-- Submit Button -->
<div class="right-submit-wrap">
<button class="btn btn-submit w-full" (click)="submitQuiz()" [disabled]="submitting()">
@if (submitting()) { <div class="spinner"></div> } @else {
<span class="material-symbols-rounded">send</span>
Submit Quiz
}
</button>
</div>
</div>
</aside>
</div><!-- /exam-layout -->
}
</div>
<!-- ══════════════════════════════════════════════════════════
Anti-Cheat Warning Modal (rendered at page root level)
Anti-Cheat Warning Modal
══════════════════════════════════════════════════════════ -->
@if (showWarningModal() && currentWarning()) {
<div class="ac-overlay" (click)="!currentWarning()!.isAutoSubmit && dismissWarning()">
......
......@@ -294,4 +294,12 @@ export class TakeQuizComponent implements OnInit, OnDestroy {
getMaxViolations(): number {
return this.antiCheat.getMaxViolations();
}
getAnsweredCount(): number {
let count = 0;
for (let i = 0; i < this.questions().length; i++) {
if (this.isAnswered(i)) count++;
}
return count;
}
}
......@@ -43,7 +43,7 @@
.profile-joined { color: rgba(255,255,255,0.3); font-size: 12px; margin: 0; }
/* Stats */
.stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 36px; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; margin-bottom: 36px; }
.stat-card {
background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08);
border-radius: 16px; padding: 24px; text-align: center;
......@@ -53,6 +53,19 @@
.stat-value { color: #fff; font-size: 28px; font-weight: 700; }
.stat-label { color: rgba(255,255,255,0.4); font-size: 13px; font-weight: 500; }
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
background: rgba(34, 197, 94, 0.1);
color: #22c55e;
border: 1px solid rgba(34, 197, 94, 0.2);
}
.section-title { color: #fff; font-size: 18px; font-weight: 600; margin: 0 0 20px; }
.empty-state { text-align: center; padding: 60px 0; }
......
......@@ -33,51 +33,29 @@
</div>
</div>
<!-- Stats Cards -->
<div class="stats-grid">
<div class="stat-card">
<span class="stat-icon">📝</span>
<span class="stat-value">{{ submissions().length }}</span>
<span class="stat-label">Tests Taken</span>
</div>
<div class="stat-card">
<span class="stat-icon">📊</span>
<span class="stat-value">{{ avgScore }}%</span>
<span class="stat-label">Average Score</span>
</div>
<div class="stat-card">
<span class="stat-icon">🏆</span>
<span class="stat-value">{{ bestScore }}%</span>
<span class="stat-label">Best Score</span>
</div>
</div>
<!-- Test Results -->
<h2 class="section-title">Test Results</h2>
<!-- Test History -->
<h2 class="section-title">Test History</h2>
@if (submissions().length === 0) {
<div class="empty-state">
<span class="empty-icon">📋</span>
<h3>No tests taken yet</h3>
<p>Complete a quiz to see your results here</p>
<h3>No test history yet</h3>
<p>Complete a quiz to see your test history here</p>
</div>
} @else {
<div class="results-list">
@for (sub of submissions(); track sub._id) {
<div class="result-card">
<div class="result-icon-wrap">
<span class="result-icon">{{ sub.percentage >= 70 ? '🏆' : sub.percentage >= 40 ? '📋' : '📝' }}</span>
<span class="result-icon">📝</span>
</div>
<div class="result-info">
<h4>{{ sub.quizId?.title || 'Deleted Quiz' }}</h4>
<p>{{ sub.submittedAt | date:'medium' }} · {{ formatTime(sub.timeTaken) }}</p>
</div>
<div class="result-score">
<div class="score-circle" [class.good]="sub.percentage >= 70" [class.avg]="sub.percentage >= 40 && sub.percentage < 70" [class.poor]="sub.percentage < 40">
<span>{{ sub.percentage }}%</span>
</div>
<span class="score-detail">{{ sub.score }}/{{ sub.totalMarks }}</span>
<div class="result-status-wrap">
<span class="status-badge">Submitted</span>
</div>
<a [routerLink]="['/student/results', sub._id]" class="view-btn">View →</a>
</div>
}
</div>
......
......@@ -236,12 +236,14 @@
html {
height: 100%;
overflow-x: hidden;
overflow: hidden;
scroll-behavior: smooth;
}
body {
min-height: 100%;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
background: var(--bg-secondary);
color: var(--text-primary);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment