Learn / index.html
Hectozar's picture
Update index.html
99a3fcc verified
Raw History Blame Contribute Delete
132 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QuizIZhere | Premium Green LMS (Cloud Enabled)</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<style>
/* =========================================
DESIGN SYSTEM & VARIABLES (EMERALD GREEN)
========================================= */
:root {
/* Core Green Palette */
--primary: #10B981;
--primary-hover: #059669;
--primary-light: #D1FAE5;
/* Secondary Teals & Blues */
--secondary: #0F766E;
--secondary-light: #CCFBF1;
/* Feedback Colors */
--danger: #EF4444;
--danger-light: #FEE2E2;
--warning: #F59E0B;
--warning-light: #FEF3C7;
/* Structural Backgrounds */
--bg-base: #F3F4F6;
--bg-surface: #FFFFFF;
--bg-offset: #F9FAFB;
/* Typography Colors */
--text-main: #064E3B;
--text-muted: #4B5563;
--border: #E5E7EB;
/* Layout & Spacing */
--sidebar-width: 280px;
--radius-sm: 8px;
--radius-md: 16px;
--radius-lg: 24px;
/* Premium Shadows */
--shadow-sm: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06);
--shadow-md: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05);
--shadow-lg: 0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04);
--shadow-glow: 0 0 20px rgba(16, 185, 129, 0.4);
/* Fonts and Transitions */
--font-family: 'Plus Jakarta Sans', sans-serif;
--transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
/* =========================================
GLOBAL RESETS
========================================= */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: var(--font-family);
}
body {
background-color: var(--bg-base);
color: var(--text-main);
font-size: 15px;
overflow: hidden;
}
.hidden {
display: none !important;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #A7F3D0;
border-radius: 999px;
}
::-webkit-scrollbar-thumb:hover {
background: #34D399;
}
/* Base Typography */
h1 {
font-size: 2.5rem;
font-weight: 800;
color: var(--text-main);
letter-spacing: -0.02em;
}
h2 {
font-size: 1.75rem;
font-weight: 700;
color: var(--text-main);
}
h3 {
font-size: 1.25rem;
font-weight: 600;
}
p {
color: var(--text-muted);
line-height: 1.6;
}
/* =========================================
LAYOUTS
========================================= */
.app-container {
display: flex;
height: 100vh;
width: 100vw;
background: linear-gradient(135deg, #ECFDF5 0%, #E5E7EB 100%);
}
.sidebar {
width: var(--sidebar-width);
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(12px);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
padding: 2rem 1.5rem;
z-index: 40;
box-shadow: var(--shadow-sm);
}
.brand {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 1.8rem;
font-weight: 800;
color: var(--primary);
margin-bottom: 3rem;
text-shadow: 0 2px 4px rgba(5, 150, 105, 0.2);
}
.brand svg {
width: 36px;
height: 36px;
}
.nav-menu {
display: flex;
flex-direction: column;
gap: 0.5rem;
flex: 1;
}
.nav-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem 1.25rem;
border-radius: var(--radius-md);
color: var(--text-muted);
font-weight: 600;
cursor: pointer;
transition: var(--transition);
border: none;
background: transparent;
text-align: left;
width: 100%;
font-size: 1rem;
}
.nav-item svg {
width: 24px;
height: 24px;
stroke-width: 2;
transition: var(--transition);
}
.nav-item:hover {
background-color: var(--primary-light);
color: var(--primary);
transform: translateX(4px);
}
.nav-item.active {
background-color: var(--primary);
color: white;
box-shadow: var(--shadow-md);
}
.main-content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
position: relative;
padding: 3rem 4rem;
scroll-behavior: smooth;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 3rem;
background: transparent;
}
.container-center {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 2rem;
background: linear-gradient(135deg, var(--primary-light) 0%, #A7F3D0 100%);
}
/* =========================================
COMPONENTS & CARDS
========================================= */
.card {
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(8px);
border-radius: var(--radius-lg);
padding: 2.5rem;
box-shadow: var(--shadow-md);
border: 1px solid rgba(255,255,255,0.8);
margin-bottom: 1.5rem;
transition: var(--transition);
}
.card:hover {
box-shadow: var(--shadow-lg);
}
.card-auth {
width: 100%;
max-width: 500px;
border-top: 6px solid var(--primary);
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.8rem 1.75rem;
font-size: 1rem;
font-weight: 600;
border-radius: 999px;
cursor: pointer;
border: 2px solid transparent;
transition: var(--transition);
outline: none;
}
.btn:active {
transform: scale(0.97);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary) 0%, #065F46 100%);
color: white;
box-shadow: 0 4px 15px rgba(5, 150, 105, 0.4);
border: none;
}
.btn-primary:hover:not(:disabled) {
box-shadow: var(--shadow-glow);
transform: translateY(-2px);
}
.btn-secondary {
background-color: white;
color: var(--primary);
border-color: var(--primary-light);
box-shadow: var(--shadow-sm);
}
.btn-secondary:hover:not(:disabled) {
background-color: var(--primary-light);
border-color: var(--primary);
}
.btn-danger {
background-color: white;
color: var(--danger);
border-color: var(--danger-light);
}
.btn-danger:hover:not(:disabled) {
background-color: var(--danger);
color: white;
}
.btn-success {
background: linear-gradient(135deg, var(--secondary) 0%, #1D4ED8 100%);
color: white;
border: none;
box-shadow: 0 4px 15px rgba(37, 99, 235, 0.4);
}
.btn-block {
width: 100%;
padding: 1rem;
font-size: 1.1rem;
}
.btn-sm {
padding: 0.5rem 1rem;
font-size: 0.85rem;
}
/* Forms */
.input-group {
margin-bottom: 1.5rem;
width: 100%;
}
.input-group label {
display: block;
font-weight: 700;
margin-bottom: 0.5rem;
font-size: 0.9rem;
color: var(--text-main);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.form-control {
width: 100%;
padding: 1rem 1.25rem;
border: 2px solid var(--border);
border-radius: var(--radius-md);
font-size: 1rem;
transition: var(--transition);
background-color: var(--bg-surface);
color: var(--text-main);
}
.form-control:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 4px var(--primary-light);
background-color: white;
}
textarea.form-control {
min-height: 120px;
resize: vertical;
}
/* Layout Helpers */
.flex-row {
display: flex;
gap: 1rem;
align-items: center;
}
.flex-between {
display: flex;
justify-content: space-between;
align-items: center;
}
.grid-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
/* Tabs */
.tabs {
display: flex;
gap: 1rem;
border-bottom: 2px solid var(--border);
margin-bottom: 2.5rem;
flex-wrap: wrap;
}
.tab {
padding: 1rem 1.5rem;
font-weight: 700;
color: var(--text-muted);
cursor: pointer;
border-bottom: 3px solid transparent;
margin-bottom: -2px;
transition: var(--transition);
border-radius: var(--radius-md) var(--radius-md) 0 0;
}
.tab:hover {
color: var(--primary);
background: var(--bg-offset);
}
.tab.active {
color: var(--primary);
border-bottom-color: var(--primary);
background: var(--bg-surface);
}
/* Badges */
.badge {
display: inline-flex;
align-items: center;
padding: 0.4rem 1rem;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.badge-primary {
background-color: var(--primary-light);
color: var(--primary-hover);
}
.badge-success {
background-color: var(--secondary-light);
color: var(--secondary);
}
.badge-warning {
background-color: var(--warning-light);
color: #B45309;
}
.badge-danger {
background-color: var(--danger-light);
color: var(--danger);
}
.badge-code {
background: #1F2937;
color: #10B981;
font-family: monospace;
font-size: 1.1rem;
letter-spacing: 0.1em;
}
/* List Elements */
.list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
background: var(--bg-surface);
border: 2px solid var(--border);
border-radius: var(--radius-md);
transition: var(--transition);
margin-bottom: 1rem;
}
.list-item:hover {
border-color: var(--primary);
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.question-card {
background: var(--bg-surface);
border: 2px solid var(--border);
border-radius: var(--radius-md);
padding: 2rem;
margin-bottom: 1.5rem;
transition: var(--transition);
}
.question-card:focus-within {
border-color: var(--primary);
box-shadow: 0 0 0 4px var(--primary-light);
}
.mc-option {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem 1.5rem;
border: 2px solid var(--border);
border-radius: var(--radius-md);
cursor: pointer;
transition: var(--transition);
margin-bottom: 0.75rem;
background: var(--bg-offset);
}
.mc-option:hover {
background-color: white;
border-color: var(--primary-light);
}
.mc-option input[type="radio"],
.mc-option input[type="checkbox"] {
width: 1.25rem;
height: 1.25rem;
accent-color: var(--primary);
cursor: pointer;
}
.mc-option.selected {
background-color: var(--primary-light);
border-color: var(--primary);
box-shadow: inset 0 0 0 1px var(--primary);
}
.mc-option.wrong {
background-color: var(--danger-light);
border-color: var(--danger);
}
/* Class Banner Decorator */
.class-banner {
background: linear-gradient(135deg, var(--primary) 0%, #064E3B 100%);
color: white;
border-radius: var(--radius-lg);
padding: 3rem;
margin-bottom: 2rem;
box-shadow: var(--shadow-lg);
position: relative;
overflow: hidden;
}
.class-banner::after {
content: '';
position: absolute;
top: 0;
right: 0;
width: 300px;
height: 300px;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
border-radius: 50%;
transform: translate(30%, -30%);
}
/* =========================================
TOASTS & MODALS
========================================= */
#toast-container {
position: fixed;
bottom: 2rem;
right: 2rem;
display: flex;
flex-direction: column;
gap: 1rem;
z-index: 9999;
}
.toast {
display: flex;
align-items: center;
gap: 1rem;
background: var(--text-main);
color: white;
padding: 1.25rem 2rem;
border-radius: var(--radius-md);
font-weight: 600;
box-shadow: var(--shadow-lg);
transform: translateX(120%);
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.toast.show {
transform: translateX(0);
}
.toast.success {
border-left: 8px solid var(--primary);
}
.toast.error {
border-left: 8px solid var(--danger);
}
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(6, 78, 59, 0.7);
backdrop-filter: blur(6px);
z-index: 9000;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
pointer-events: none;
transition: var(--transition);
}
.modal-backdrop.active {
opacity: 1;
pointer-events: auto;
}
.modal {
background: var(--bg-surface);
width: 100%;
max-width: 550px;
max-height: 90vh;
overflow-y: auto;
border-radius: var(--radius-lg);
padding: 3rem;
transform: scale(0.95) translateY(20px);
transition: var(--transition);
box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25);
}
.modal.modal-lg {
max-width: 850px;
}
.modal-backdrop.active .modal {
transform: scale(1) translateY(0);
}
/* CONFETTI CANVAS */
#confetti-canvas {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 9999;
}
</style>
</head>
<body>
<canvas id="confetti-canvas"></canvas>
<div id="toast-container"></div>
<div id="modal-container" class="modal-backdrop">
<div class="modal">
<h2 id="modal-title" style="margin-bottom: 1rem;">Confirm</h2>
<p id="modal-desc" style="margin-bottom: 2rem;">Proceed with action?</p>
<div id="modal-input-wrapper" class="input-group hidden">
<input type="password" id="modal-input" class="form-control" placeholder="Enter value...">
</div>
<div class="flex-row" style="justify-content: flex-end; margin-top: 1rem;">
<button class="btn btn-secondary" onclick="UI.closeModal()">Cancel</button>
<button id="modal-confirm-btn" class="btn btn-primary">Confirm</button>
</div>
</div>
</div>
<div id="grade-modal" class="modal-backdrop">
<div class="modal modal-lg">
<h2 id="grade-modal-title">Grade Submission</h2>
<p id="grade-modal-student" style="font-weight: 600; color: var(--primary);">Student Name</p>
<div id="grade-questions-container" style="margin-top: 2rem; border-top: 2px solid var(--border); padding-top: 2rem;"></div>
<div class="flex-row" style="justify-content: flex-end; margin-top: 2rem;">
<button class="btn btn-secondary" onclick="document.getElementById('grade-modal').classList.remove('active')">Cancel</button>
<button class="btn btn-primary" id="grade-save-btn">Save Grades & Feedback</button>
</div>
</div>
</div>
<div id="view-auth" class="container-center">
<div class="card card-auth">
<div class="brand" style="justify-content: center; margin-bottom: 2rem; font-size: 2.5rem;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 14l9-5-9-5-9 5 9 5z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z"/>
</svg>
QuizIZhere LMS
</div>
<div class="tabs" style="justify-content: center;">
<div class="tab active" id="tab-login" onclick="Auth.switchTab('login')">Secure Login</div>
<div class="tab" id="tab-register" onclick="Auth.switchTab('register')">Register</div>
</div>
<div id="form-login">
<div class="input-group">
<label>Username</label>
<input type="text" id="login-user" class="form-control" placeholder="Enter username">
</div>
<div class="input-group">
<label>Password</label>
<input type="password" id="login-pass" class="form-control" placeholder="Enter password">
</div>
<button class="btn btn-primary btn-block" onclick="Auth.login()" style="margin-top: 1rem;">Access Portal</button>
</div>
<div id="form-register" class="hidden">
<div class="input-group">
<label>Account Type</label>
<select id="reg-role" class="form-control" onchange="Auth.toggleTeacherCode(this.value)">
<option value="student">Student / Learner</option>
<option value="teacher">Instructor / Administrator</option>
</select>
</div>
<div class="input-group hidden" id="reg-code-group">
<label style="color: var(--warning);">Administrator Access Code</label>
<input type="password" id="reg-code" class="form-control" placeholder="Enter code (42)">
</div>
<div class="input-group">
<label>Full Name</label>
<input type="text" id="reg-name" class="form-control" placeholder="e.g. Jane Doe">
</div>
<div class="input-group">
<label>Username</label>
<input type="text" id="reg-user" class="form-control" placeholder="Choose a username">
</div>
<div class="input-group">
<label>Password</label>
<input type="password" id="reg-pass" class="form-control" placeholder="Create a strong password">
</div>
<button class="btn btn-primary btn-block" onclick="Auth.register()" style="margin-top: 1rem;">Create Account</button>
</div>
</div>
</div>
<div id="view-app" class="app-container hidden">
<aside class="sidebar">
<div class="brand">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 14l9-5-9-5-9 5 9 5z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z"/>
</svg>
QuizIZhere
</div>
<div id="nav-student" class="nav-menu hidden">
<button class="nav-item active" onclick="Router.navigate('student-dash', this)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/></svg> My Classes
</button>
<button class="nav-item" onclick="Router.navigate('student-history', this)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg> Grades & Reports
</button>
<button class="nav-item" onclick="Router.navigate('settings', this)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065zM15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg> Settings
</button>
</div>
<div id="nav-teacher" class="nav-menu hidden">
<button class="nav-item active" onclick="Router.navigate('teacher-classes', this)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg> Classes
</button>
<button class="nav-item" onclick="Router.navigate('teacher-bank', this)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 002-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg> Assessment Bank
</button>
<button class="nav-item" onclick="Router.navigate('teacher-grading', this)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/></svg> Grading Center
</button>
<button class="nav-item" onclick="Router.navigate('settings', this)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065zM15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg> Settings
</button>
</div>
<div style="flex: 1;"></div>
<div style="border-top: 2px solid var(--border); padding-top: 1.5rem; margin-top: 1.5rem;">
<p id="nav-user-name" style="font-weight: 800; color: var(--text-main); margin-bottom: 0.25rem;">User Name</p>
<p id="nav-user-role" style="font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.1em; font-weight:700; margin-bottom: 1rem; color: var(--primary);">Role</p>
<button class="nav-item" onclick="Auth.logout()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg> Sign Out
</button>
</div>
</aside>
<main class="main-content">
<div id="page-settings" class="page hidden" style="max-width: 800px; margin: 0 auto;">
<div class="page-header">
<div>
<h1>Account Settings</h1>
<p>Update your personal information and security credentials.</p>
</div>
</div>
<div class="card">
<h3 style="margin-bottom: 1.5rem; border-bottom: 2px solid var(--border); padding-bottom: 1rem; color: var(--primary);">Profile Details</h3>
<div class="input-group">
<label>Full Name</label>
<input type="text" id="set-name" class="form-control">
</div>
<div class="input-group">
<label>Username</label>
<input type="text" id="set-user" class="form-control">
</div>
<h3 style="margin-top: 2.5rem; margin-bottom: 1.5rem; border-bottom: 2px solid var(--border); padding-bottom: 1rem; color: var(--primary);">Security</h3>
<div class="input-group">
<label>New Password (leave blank to keep current)</label>
<input type="password" id="set-pass" class="form-control" placeholder="Enter new password">
</div>
<button class="btn btn-primary" style="margin-top: 1rem; width: 100%; font-size: 1.1rem; padding: 1rem;" onclick="SettingsCtrl.save()">Save All Changes</button>
</div>
</div>
<div id="page-teacher-classes" class="page hidden" style="max-width: 1200px; margin: 0 auto;">
<div class="page-header">
<div>
<h1>My Classes</h1>
<p>Manage your classroom rosters and assignments.</p>
</div>
<button class="btn btn-primary" onclick="ClassCtrl.showCreateModal()">+ Create New Class</button>
</div>
<div id="teacher-class-list" class="grid-cards"></div>
</div>
<div id="page-teacher-class-detail" class="page hidden" style="max-width: 1200px; margin: 0 auto;">
<div class="page-header" style="margin-bottom: 1.5rem;">
<button class="btn btn-secondary btn-sm" onclick="Router.navigate('teacher-classes', document.querySelector('#nav-teacher .nav-item:nth-child(1)'))">← Back to Classes</button>
<div class="flex-row">
<button class="btn btn-secondary btn-sm" onclick="ClassCtrl.editClassName()">✏️ Edit Name</button>
<button class="btn btn-danger btn-sm" onclick="ClassCtrl.deleteClass()">🗑️ Delete Class</button>
</div>
</div>
<div class="class-banner">
<h1 id="tc-detail-name" style="color:white; margin-bottom: 0.5rem; font-size: 3rem;">Class Name</h1>
<p style="color: var(--primary-light); font-size: 1.2rem; margin-bottom: 1.5rem;">Class Code: <span id="tc-detail-code" class="badge badge-code" style="background: white; color: var(--primary);">CODE</span></p>
</div>
<div class="tabs">
<div class="tab active" id="tab-tcd-assignments" onclick="ClassCtrl.switchDetailTab('assignments')">Assigned Quizzes</div>
<div class="tab" id="tab-tcd-roster" onclick="ClassCtrl.switchDetailTab('roster')">Student Roster</div>
</div>
<div id="tcd-view-assignments">
<div class="flex-between" style="margin-bottom: 1.5rem;">
<h3>Active Assessments</h3>
<button class="btn btn-success" onclick="ClassCtrl.showAssignModal()">+ Assign Quiz to Class</button>
</div>
<div id="tcd-assignments-list" class="data-list"></div>
</div>
<div id="tcd-view-roster" class="hidden">
<h3 style="margin-bottom: 1.5rem;">Enrolled Students</h3>
<div id="tcd-roster-list" class="grid-cards"></div>
</div>
</div>
<div id="page-teacher-bank" class="page hidden" style="max-width: 1200px; margin: 0 auto;">
<div class="page-header">
<div>
<h1>Assessment Bank</h1>
<p>Build and store quizzes here. Assign them to classes later.</p>
</div>
<button class="btn btn-primary" onclick="BuilderCtrl.init(null)">+ Create New Quiz</button>
</div>
<div id="teacher-quiz-list" class="grid-cards"></div>
</div>
<div id="page-teacher-builder" class="page hidden" style="max-width: 1000px; margin: 0 auto;">
<div class="page-header">
<div><h1 id="builder-header-title">Quiz Builder</h1></div>
<div class="flex-row">
<button class="btn btn-secondary" onclick="BuilderCtrl.save()">Save to Bank</button>
</div>
</div>
<div class="card" style="margin-bottom: 2rem;">
<div class="input-group" style="margin: 0;">
<label>Assessment Title</label>
<input type="text" id="build-title" class="form-control" placeholder="e.g. Advanced Biology Midterm" style="font-size: 1.25rem; font-weight: 700;">
</div>
</div>
<div class="tabs">
<div class="tab active" id="tab-tb-editor" onclick="BuilderCtrl.switchTab('editor')">Questions Editor</div>
<div class="tab" id="tab-tb-bulk" onclick="BuilderCtrl.switchTab('bulk')">✨ Bulk Import Text</div>
<div class="tab" id="tab-tb-settings" onclick="BuilderCtrl.switchTab('settings')">Settings</div>
</div>
<div id="build-view-editor">
<div id="build-questions-list"></div>
<button class="btn btn-secondary btn-block" style="border-style: dashed; padding: 2rem; margin-top: 1rem; border-width: 3px;" onclick="BuilderCtrl.addQuestion()">+ Add New Question</button>
</div>
<div id="build-view-bulk" class="hidden card">
<h3 style="margin-bottom: 1rem; color: var(--primary);">Bulk Import Questions</h3>
<p style="margin-bottom: 1.5rem; font-size: 0.95rem; line-height: 1.6; color: var(--text-muted);">
Paste text to instantly generate questions.<br>
<strong>Format 1 (Paragraphs):</strong> Put an asterisk (*) next to the correct answer. Separate questions with a blank line.<br>
<strong>Format 2 (Colons):</strong> <code>Question : Correct : Wrong : Wrong</code> on each line.
</p>
<textarea id="bulk-import-text" class="form-control" style="min-height: 350px; font-family: monospace; font-size: 0.95rem; margin-bottom: 1.5rem; line-height: 1.5;" placeholder="Format 1 Example:&#10;What is the capital of France?&#10;London&#10;*Paris&#10;Berlin&#10;&#10;Format 2 Example:&#10;What is 2+2? : 4 : 3 : 5 : 6"></textarea>
<button class="btn btn-primary btn-block" onclick="BuilderCtrl.processBulk()">Process & Generate Questions</button>
</div>
<div id="build-view-settings" class="hidden card">
<div class="flex-row" style="margin-bottom: 1.5rem;">
<div class="input-group" style="flex: 1; margin: 0;"><label>Time Limit (Mins, 0 = Unlimited)</label><input type="number" id="build-timer" class="form-control" value="0" min="0"></div>
<div class="input-group" style="flex: 1; margin: 0;"><label>Access Password (Optional)</label><input type="text" id="build-password" class="form-control" placeholder="Leave blank for open access"></div>
</div>
<label class="mc-option" style="justify-content: flex-start; max-width: 400px;"><input type="checkbox" id="build-rand-q"><span style="font-weight: 700;">Randomize Question Order</span></label>
<label class="mc-option" style="justify-content: flex-start; max-width: 400px;"><input type="checkbox" id="build-rand-o" checked><span style="font-weight: 700;">Randomize Options</span></label>
</div>
</div>
<div id="page-teacher-grading" class="page hidden" style="max-width: 1200px; margin: 0 auto;">
<div class="page-header">
<div>
<h1>Grading Center</h1>
<p>Review student submissions across all classes.</p>
</div>
</div>
<div id="teacher-subs-list" class="data-list"></div>
</div>
<div id="page-student-dash" class="page hidden" style="max-width: 1200px; margin: 0 auto;">
<div class="page-header">
<div>
<h1>My Classes</h1>
<p>Access your enrolled classrooms and assignments.</p>
</div>
<button class="btn btn-primary" onclick="StudentCtrl.showJoinModal()">+ Join a Class</button>
</div>
<div id="student-class-list" class="grid-cards"></div>
</div>
<div id="page-student-class-detail" class="page hidden" style="max-width: 1000px; margin: 0 auto;">
<div class="page-header" style="margin-bottom: 1.5rem;">
<button class="btn btn-secondary btn-sm" onclick="Router.navigate('student-dash', document.querySelector('#nav-student .nav-item:nth-child(1)'))">← Back to My Classes</button>
</div>
<div class="class-banner">
<h1 id="sc-detail-name" style="color:white; margin-bottom: 0.5rem; font-size: 3rem;">Class Name</h1>
<p style="color: var(--primary-light); font-size: 1.2rem;">Instructor: <span id="sc-detail-teacher" style="font-weight: 700;">Teacher Name</span></p>
</div>
<h3 style="margin-bottom: 1.5rem;">Assessments</h3>
<div id="sc-assignments-list" class="data-list"></div>
</div>
<div id="page-pre-test" class="page hidden" style="max-width: 700px; margin: 0 auto;">
<div class="card" style="text-align: center; border-top: 8px solid var(--primary); margin-top: 10vh; padding: 4rem 2rem;">
<h1 style="margin-bottom: 1.5rem; font-size: 2.5rem;" id="pre-title">Assessment Title</h1>
<div style="background: var(--bg-offset); padding: 1.5rem; border-radius: var(--radius-md); margin-bottom: 2.5rem; border: 1px solid var(--border); display: inline-block; text-align: left;">
<p style="font-size: 1.1rem; margin-bottom: 0.5rem;"><strong>Class:</strong> <span id="pre-class"></span></p>
<p style="font-size: 1.1rem; margin-bottom: 0;"><strong>Time Limit:</strong> <span id="pre-time" class="badge badge-warning"></span></p>
</div>
<div class="flex-row" style="justify-content: center; gap: 1rem;">
<button class="btn btn-secondary" onclick="Router.navigate('student-dash')">Cancel</button>
<button class="btn btn-primary" style="font-size: 1.2rem; padding: 1rem 2.5rem;" onclick="TestCtrl.startSession()">Begin Assessment</button>
</div>
</div>
</div>
<div id="page-take-test" class="page hidden" style="max-width: 900px; margin: 0 auto; padding-bottom: 5rem;">
<div class="card" style="position: sticky; top: -1rem; z-index: 10; margin-bottom: 2rem; padding: 1.5rem 2.5rem; display: flex; justify-content: space-between; align-items: center; box-shadow: var(--shadow-md); background: rgba(255,255,255,0.9); backdrop-filter: blur(10px);">
<div>
<h2 id="live-test-title" style="margin:0; color:var(--primary);">Assessment</h2>
<span id="live-test-progress" class="badge badge-primary" style="margin-top: 0.5rem;">Progress</span>
</div>
<div class="flex-row">
<div id="live-timer" class="badge badge-danger hidden" style="font-size: 1.2rem; padding: 0.8rem 1.2rem;"><span>00:00</span></div>
<button class="btn btn-secondary" onclick="TestCtrl.pause()">⏸ Save & Pause</button>
<button class="btn btn-success" onclick="TestCtrl.submit()">Submit</button>
</div>
</div>
<div id="live-test-questions"></div>
</div>
<div id="page-student-history" class="page hidden" style="max-width: 1000px; margin: 0 auto;">
<div class="page-header">
<div>
<h1>Grades & Reports</h1>
<p>Review your performance across all classes.</p>
</div>
</div>
<div id="student-history-list" class="data-list"></div>
</div>
<div id="page-review-test" class="page hidden" style="max-width: 900px; margin: 0 auto;">
<div class="page-header">
<div><h1 id="review-title">Review Mode</h1></div>
<button class="btn btn-secondary" onclick="Router.navigate('student-history', null)">Back to Grades</button>
</div>
<div id="review-header-card" class="card" style="text-align: center; border-top: 8px solid var(--primary); padding: 3rem;"></div>
<div id="review-questions"></div>
</div>
</main>
</div>
<script>
/**
* ============================================================================
* SYSTEM LOGGER
* ============================================================================
* Description:
* A centralized logging system to ensure professional, readable outputs
* in the developer console. Essential for tracking cloud sync statuses.
* ============================================================================
*/
const SystemLogger = {
info: (msg) => console.log(`%c[INFO] ${msg}`, 'color: #2563EB; font-weight: bold;'),
success: (msg) => console.log(`%c[SUCCESS] ${msg}`, 'color: #10B981; font-weight: bold;'),
warn: (msg) => console.warn(`%c[WARN] ${msg}`, 'color: #F59E0B; font-weight: bold;'),
error: (msg, err = '') => console.error(`%c[ERROR] ${msg}`, 'color: #EF4444; font-weight: bold;', err)
};
/**
* ============================================================================
* CORE UTILITIES
* ============================================================================
* Description:
* Reusable helper functions utilized globally across all controllers.
* Includes shuffling algorithms, percentage calculations, and deep cloning.
* ============================================================================
*/
const Utils = {
id: () => Math.random().toString(36).substr(2, 9),
classCode: () => Math.random().toString(36).substr(2, 6).toUpperCase(),
shuffle: (array) => {
let cur = array.length, rand;
while (cur !== 0) {
rand = Math.floor(Math.random() * cur);
cur--;
[array[cur], array[rand]] = [array[rand], array[cur]];
}
return array;
},
getPctColor: (score, total) => {
if (total === 0) return 'badge-primary';
const p = score / total;
if (p >= 0.8) return 'badge-success';
if (p >= 0.6) return 'badge-warning';
return 'badge-danger';
},
deepClone: (obj) => JSON.parse(JSON.stringify(obj))
};
/**
* ============================================================================
* CLOUD-CONNECTED DATABASE MANAGER (v5)
* ============================================================================
* Description:
* Manages the synchronization between the local client and the Google
* Apps Script backend. Uses caching for speed and uploads in the background.
* ============================================================================
*/
const DB = {
data: {
users: [],
classes: [],
quizzes: [],
sessions: [],
submissions: []
},
/**
* Initializes the cloud connection.
* Attempts to load the global database via google.script.run
* and falls back to local storage if running in an offline or local environment.
*/
initCloud: (callback) => {
SystemLogger.info("Initializing Cloud Database Connection...");
if (typeof google !== 'undefined' && google.script) {
google.script.run
.withSuccessHandler((cloudData) => {
if (cloudData) {
DB.data = JSON.parse(cloudData);
localStorage.setItem('quiziz_db_v5', cloudData); // Buffer for next instant load
SystemLogger.success("Cloud data loaded successfully.");
} else {
SystemLogger.warn("Cloud data is empty. Checking local fallback...");
const local = localStorage.getItem('quiziz_db_v5');
if (local) DB.data = JSON.parse(local);
}
callback(); // Tell the app it's ready to boot
})
.loadDataFromCloud();
} else {
SystemLogger.warn("Local Environment Detected. Bypassing Google Script Run.");
const local = localStorage.getItem('quiziz_db_v5');
if (local) {
try {
DB.data = JSON.parse(local);
SystemLogger.success("Local fallback loaded.");
} catch(e) {
SystemLogger.error("Failed to parse local fallback.");
}
}
callback();
}
},
/**
* Loads data synchronously from the local cache to ensure
* instant UI updates without waiting for network latency.
*/
load: () => {
const s = localStorage.getItem('quiziz_db_v5');
if (s) {
try {
DB.data = JSON.parse(s);
} catch(e) {
SystemLogger.error("DB Local Load Error", e);
}
}
},
/**
* Saves data synchronously to the local cache, then asynchronously
* triggers a payload upload to the Google Apps Script backend.
*/
save: () => {
try {
const jsonString = JSON.stringify(DB.data);
// 1. Save locally for instant UI speed
localStorage.setItem('quiziz_db_v5', jsonString);
SystemLogger.info("Data buffered locally.");
// 2. Upload to Google Drive Cloud in the background
if (typeof google !== 'undefined' && google.script) {
google.script.run
.withSuccessHandler(() => SystemLogger.success("Data synced to Cloud successfully!"))
.saveDataToCloud(jsonString);
}
} catch(e) {
SystemLogger.error("DB Save Error: ", e);
}
}
};
/**
* ============================================================================
* UI CONTROLLER
* ============================================================================
* Description:
* Manages Toast notifications and Modal dialog interactions dynamically.
* ============================================================================
*/
const UI = {
toast: (msg, type = 'success') => {
const container = document.getElementById('toast-container');
if (!container) return SystemLogger.warn("Toast container not found.");
const toastEl = document.createElement('div');
toastEl.className = `toast ${type}`;
toastEl.innerHTML = msg;
container.appendChild(toastEl);
// Animate in
setTimeout(() => toastEl.classList.add('show'), 10);
// Animate out and remove
setTimeout(() => {
toastEl.classList.remove('show');
setTimeout(() => toastEl.remove(), 400);
}, 4000);
},
openModal: (title, desc, onConfirm, inputPlaceholder = null) => {
// Determine if password-type input is needed based on the placeholder string
const isPasswordType = inputPlaceholder && inputPlaceholder.toLowerCase().includes("password");
document.getElementById('modal-title').innerText = title;
document.getElementById('modal-desc').innerText = desc;
const inputWrap = document.getElementById('modal-input-wrapper');
const inputEl = document.getElementById('modal-input');
inputEl.value = '';
inputEl.type = isPasswordType ? 'password' : 'text'; // Dynamically set input type
if (inputPlaceholder) {
inputWrap.classList.remove('hidden');
inputEl.placeholder = inputPlaceholder;
} else {
inputWrap.classList.add('hidden');
}
const btn = document.getElementById('modal-confirm-btn');
const newBtn = btn.cloneNode(true);
btn.parentNode.replaceChild(newBtn, btn);
newBtn.onclick = () => {
if (inputPlaceholder && !inputEl.value.trim()) {
return UI.toast("Input is required to proceed.", "error");
}
onConfirm(inputEl.value.trim());
UI.closeModal();
};
document.getElementById('modal-container').classList.add('active');
},
closeModal: () => {
document.getElementById('modal-container').classList.remove('active');
}
};
/**
* ============================================================================
* CONFETTI ENGINE
* ============================================================================
* Description:
* Renders a full-screen canvas confetti animation upon successful test completion.
* Utilizes requestAnimationFrame for smooth 60fps rendering.
* ============================================================================
*/
const ConfettiEngine = {
particles: [], canvas: null, ctx: null, w: 0, h: 0, active: false,
colors: ['#10B981', '#0F766E', '#F59E0B', '#EF4444', '#34D399'],
init: () => {
ConfettiEngine.canvas = document.getElementById('confetti-canvas');
if (!ConfettiEngine.canvas) return;
ConfettiEngine.ctx = ConfettiEngine.canvas.getContext('2d');
window.addEventListener('resize', ConfettiEngine.resize);
ConfettiEngine.resize();
},
resize: () => {
ConfettiEngine.w = window.innerWidth;
ConfettiEngine.h = window.innerHeight;
ConfettiEngine.canvas.width = ConfettiEngine.w;
ConfettiEngine.canvas.height = ConfettiEngine.h;
},
fire: () => {
if (!ConfettiEngine.canvas) ConfettiEngine.init();
ConfettiEngine.active = true;
ConfettiEngine.particles = [];
for(let i=0; i<200; i++) {
ConfettiEngine.particles.push({
x: ConfettiEngine.w / 2,
y: ConfettiEngine.h + 20,
r: Math.random() * 8 + 4,
dx: Math.random() * 30 - 15,
dy: Math.random() * -25 - 15,
c: ConfettiEngine.colors[Math.floor(Math.random() * ConfettiEngine.colors.length)],
tilt: Math.floor(Math.random() * 10) - 10,
tiltAngleInc: (Math.random() * 0.07) + 0.05,
tiltAngle: 0
});
}
requestAnimationFrame(ConfettiEngine.loop);
setTimeout(() => { ConfettiEngine.active = false; }, 3500);
},
loop: () => {
ConfettiEngine.ctx.clearRect(0, 0, ConfettiEngine.w, ConfettiEngine.h);
let remaining = false;
ConfettiEngine.particles.forEach((p) => {
p.tiltAngle += p.tiltAngleInc;
p.y += (Math.cos(p.tiltAngle) + 1 + p.r / 2) / 2;
p.x += Math.sin(p.tiltAngle) * 2 + p.dx;
p.dy += 0.25;
p.y += p.dy;
if (p.y <= ConfettiEngine.h + 20) remaining = true;
ConfettiEngine.ctx.beginPath();
ConfettiEngine.ctx.lineWidth = p.r;
ConfettiEngine.ctx.strokeStyle = p.c;
ConfettiEngine.ctx.moveTo(p.x + p.tilt + p.r, p.y);
ConfettiEngine.ctx.lineTo(p.x + p.tilt, p.y + p.tilt + p.r);
ConfettiEngine.ctx.stroke();
});
if (remaining || ConfettiEngine.active) {
requestAnimationFrame(ConfettiEngine.loop);
} else {
ConfettiEngine.ctx.clearRect(0, 0, ConfettiEngine.w, ConfettiEngine.h);
}
}
};
/**
* ============================================================================
* ROUTING & APPLICATION STATE (Cloud Upgraded)
* ============================================================================
* Description:
* Manages the single-page application views, state, and user sessions.
* Implements a blocking initialization sequence to ensure cloud data loads first.
* ============================================================================
*/
const AppState = { user: null };
const Router = {
init: () => {
SystemLogger.info("Application Initialization Started.");
if (typeof UI !== 'undefined' && UI.toast) {
UI.toast("Connecting to Global Database...", "success");
}
// Synchronize with the cloud BEFORE booting the UI
DB.initCloud(() => {
const storedUser = localStorage.getItem('quiziz_user_v5');
if (storedUser) {
try {
AppState.user = JSON.parse(storedUser);
Router.bootApp();
} catch (e) {
SystemLogger.error("Failed to parse stored user data.");
document.getElementById('view-auth').classList.remove('hidden');
}
} else {
document.getElementById('view-auth').classList.remove('hidden');
}
ConfettiEngine.init();
});
},
bootApp: () => {
document.getElementById('view-auth').classList.add('hidden');
document.getElementById('view-app').classList.remove('hidden');
document.getElementById('nav-user-name').innerText = AppState.user.name;
document.getElementById('nav-user-role').innerText = AppState.user.role === 'teacher' ? 'Instructor' : 'Student';
document.getElementById('nav-student').classList.add('hidden');
document.getElementById('nav-teacher').classList.add('hidden');
if (AppState.user.role === 'student') {
document.getElementById('nav-student').classList.remove('hidden');
Router.navigate('student-dash', document.querySelector('#nav-student .nav-item'));
} else {
document.getElementById('nav-teacher').classList.remove('hidden');
Router.navigate('teacher-classes', document.querySelector('#nav-teacher .nav-item'));
}
},
navigate: (pageId, navElement) => {
SystemLogger.info(`Navigating to View: ${pageId}`);
if (TestCtrl.liveSession && pageId !== 'take-test') {
return UI.toast("You must pause or submit your assessment first.", "warning");
}
document.querySelectorAll('.page').forEach(p => p.classList.add('hidden'));
const targetPage = document.getElementById('page-' + pageId);
if (targetPage) targetPage.classList.remove('hidden');
if (navElement) {
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
navElement.classList.add('active');
}
// Call contextual render functions upon navigation
if (pageId === 'teacher-classes') ClassCtrl.renderTeacherClasses();
else if (pageId === 'teacher-bank') BuilderCtrl.renderBank();
else if (pageId === 'teacher-grading') TeacherCtrl.renderGrading();
else if (pageId === 'student-dash') ClassCtrl.renderStudentClasses();
else if (pageId === 'student-history') StudentCtrl.renderHistory();
else if (pageId === 'settings') SettingsCtrl.init();
}
};
/**
* ============================================================================
* AUTHENTICATION LOGIC
* ============================================================================
* Description:
* Manages user registration, verification against the cloud DB, and session tokens.
* ============================================================================
*/
const Auth = {
switchTab: (tab) => {
document.querySelectorAll('.card-auth .tab').forEach(t => t.classList.remove('active'));
document.getElementById('tab-'+tab).classList.add('active');
document.getElementById('form-login').classList.add('hidden');
document.getElementById('form-register').classList.add('hidden');
document.getElementById('form-'+tab).classList.remove('hidden');
},
toggleTeacherCode: (val) => {
const group = document.getElementById('reg-code-group');
if (val === 'teacher') {
group.classList.remove('hidden');
} else {
group.classList.add('hidden');
}
},
register: () => {
const role = document.getElementById('reg-role').value;
const name = document.getElementById('reg-name').value.trim();
const user = document.getElementById('reg-user').value.trim();
const pass = document.getElementById('reg-pass').value;
if (!name || !user || !pass) {
return UI.toast("All fields are strictly required.", "error");
}
if (DB.data.users.find(u => u.username.toLowerCase() === user.toLowerCase())) {
return UI.toast("This Username is already taken.", "error");
}
if (role === 'teacher' && document.getElementById('reg-code').value.trim() !== '42') {
return UI.toast("Invalid Administrator Access Code.", "error");
}
const newUser = {
id: Utils.id(),
role: role,
name: name,
username: user,
password: pass
};
DB.data.users.push(newUser);
DB.save(); // Sync new user to cloud immediately
AppState.user = newUser;
localStorage.setItem('quiziz_user_v5', JSON.stringify(newUser));
UI.toast("Account securely created.", "success");
Router.bootApp();
},
login: () => {
const user = document.getElementById('login-user').value.trim();
const pass = document.getElementById('login-pass').value;
const foundUser = DB.data.users.find(u => u.username.toLowerCase() === user.toLowerCase() && u.password === pass);
if (!foundUser) {
return UI.toast("Invalid username or password.", "error");
}
AppState.user = foundUser;
localStorage.setItem('quiziz_user_v5', JSON.stringify(foundUser));
UI.toast("Welcome back.", "success");
Router.bootApp();
},
logout: () => {
AppState.user = null;
localStorage.removeItem('quiziz_user_v5');
document.getElementById('view-app').classList.add('hidden');
document.getElementById('view-auth').classList.remove('hidden');
document.querySelectorAll('input').forEach(i => i.value = '');
Auth.switchTab('login');
}
};
/**
* ============================================================================
* SETTINGS CONTROLLER
* ============================================================================
* Description:
* Manages user profile updates, strictly verifying username availability globally.
* ============================================================================
*/
const SettingsCtrl = {
init: () => {
DB.load();
const nInput = document.getElementById('set-name');
const uInput = document.getElementById('set-user');
const pInput = document.getElementById('set-pass');
if(nInput) nInput.value = AppState.user.name;
if(uInput) uInput.value = AppState.user.username;
if(pInput) pInput.value = '';
},
save: () => {
const newName = document.getElementById('set-name').value.trim();
const newUser = document.getElementById('set-user').value.trim();
const newPass = document.getElementById('set-pass').value;
if (!newName || !newUser) {
return UI.toast("Full Name and Username cannot be empty.", "error");
}
DB.load();
const existingUser = DB.data.users.find(u => u.username.toLowerCase() === newUser.toLowerCase() && u.id !== AppState.user.id);
if (existingUser) {
return UI.toast("That Username is already assigned to someone else.", "error");
}
const userIndex = DB.data.users.findIndex(u => u.id === AppState.user.id);
if (userIndex >= 0) {
DB.data.users[userIndex].name = newName;
DB.data.users[userIndex].username = newUser;
if (newPass && newPass.trim() !== '') {
DB.data.users[userIndex].password = newPass;
}
DB.save();
AppState.user = DB.data.users[userIndex];
localStorage.setItem('quiziz_user_v5', JSON.stringify(AppState.user));
const nameEl = document.getElementById('nav-user-name');
if (nameEl) nameEl.innerText = AppState.user.name;
UI.toast("Account settings have been saved successfully!", "success");
document.getElementById('set-pass').value = '';
} else {
UI.toast("Critical Error: Active user not found in database.", "error");
}
}
};
/**
* ============================================================================
* CLASS MANAGEMENT CONTROLLER
* ============================================================================
* Description:
* Comprehensive management for Instructors and Students regarding classrooms,
* enrollments, assignment delegation, and class editing features.
* ============================================================================
*/
const ClassCtrl = {
currentClassId: null,
/**
* Renders the grid of classes owned by the active teacher.
*/
renderTeacherClasses: () => {
DB.load();
const list = document.getElementById('teacher-class-list');
list.innerHTML = '';
const myClasses = DB.data.classes.filter(c => c.teacherId === AppState.user.id);
if (myClasses.length === 0) {
list.innerHTML = `<div class="card" style="text-align:center; grid-column: 1/-1;"><h3>No classes yet.</h3><p>Create a class to generate a join code for students.</p></div>`;
return;
}
myClasses.forEach(cls => {
const card = document.createElement('div');
card.className = 'card';
card.style.cursor = 'pointer';
card.onclick = () => ClassCtrl.openTeacherClassDetail(cls.id);
card.innerHTML = `
<div style="background: var(--primary-light); padding: 1.5rem; margin: -2.5rem -2.5rem 1.5rem -2.5rem; border-radius: var(--radius-lg) var(--radius-lg) 0 0; border-bottom: 2px solid var(--primary);">
<h2 style="color: var(--primary); margin: 0;">${cls.name}</h2>
</div>
<p style="font-size: 1.1rem; font-weight: 700;">Code: <span class="badge badge-code">${cls.code}</span></p>
<div class="flex-row" style="margin-top: 1.5rem; color: var(--text-muted);">
<span>🧑‍🎓 ${cls.students.length} Students</span>
<span>📝 ${cls.assignments.length} Quizzes</span>
</div>
`;
list.appendChild(card);
});
},
/**
* Generates a new classroom object and synchronizes it globally.
*/
showCreateModal: () => {
UI.openModal(
"Create New Class",
"Enter a distinct name for your class.",
(name) => {
const newClass = {
id: Utils.id(),
name: name,
code: Utils.classCode(),
teacherId: AppState.user.id,
students: [],
assignments: []
};
DB.data.classes.push(newClass);
DB.save();
ClassCtrl.renderTeacherClasses();
UI.toast(`Class created! Your code is: ${newClass.code}`, "success");
},
"e.g. Biology 101 Period 2"
);
},
/**
* Routes the instructor to the detail management view of a specific class.
* @param {string} classId - The unique identifier of the target class.
*/
openTeacherClassDetail: (classId) => {
ClassCtrl.currentClassId = classId;
DB.load();
const cls = DB.data.classes.find(c => c.id === classId);
const nameEl = document.getElementById('tc-detail-name');
const codeEl = document.getElementById('tc-detail-code');
if(nameEl) nameEl.innerText = cls.name;
if(codeEl) codeEl.innerText = cls.code;
Router.navigate('teacher-class-detail');
ClassCtrl.switchDetailTab('assignments');
},
/**
* Prompts the teacher to edit the name of their class.
*/
editClassName: () => {
DB.load();
const cls = DB.data.classes.find(c => c.id === ClassCtrl.currentClassId);
if (!cls) return UI.toast("Error: Class not found.", "error");
UI.openModal(
"Edit Class Name",
"Enter a new name for this class:",
(newName) => {
const trimmedName = newName.trim();
if (!trimmedName) {
return UI.toast("Class name cannot be empty.", "error");
}
cls.name = trimmedName;
DB.save();
const headerEl = document.getElementById('tc-detail-name');
if(headerEl) headerEl.innerText = trimmedName;
UI.toast("Class name updated successfully.", "success");
},
cls.name
);
},
/**
* Initiates the permanent deletion protocol for a classroom.
* Requires exact string matching for safety.
*/
deleteClass: () => {
UI.openModal(
"Delete Class",
"Are you absolutely sure? This permanently removes the roster and active assignments. Type 'DELETE' below to confirm.",
(confirmation) => {
if (confirmation.trim() !== 'DELETE') {
return UI.toast("Action cancelled. You must type 'DELETE' exactly.", "error");
}
DB.load();
DB.data.classes = DB.data.classes.filter(c => c.id !== ClassCtrl.currentClassId);
DB.save();
UI.toast("Class successfully deleted.", "success");
Router.navigate('teacher-classes', document.querySelector('#nav-teacher .nav-item:nth-child(1)'));
},
"Type DELETE"
);
},
/**
* Handles switching between inner tabs in the class detail view.
* @param {string} tab - The target tab identifier (assignments or roster).
*/
switchDetailTab: (tab) => {
['assignments', 'roster'].forEach(t => {
const tEl = document.getElementById('tab-tcd-'+t);
const vEl = document.getElementById('tcd-view-'+t);
if(tEl) tEl.classList.remove('active');
if(vEl) vEl.classList.add('hidden');
});
const atEl = document.getElementById('tab-tcd-'+tab);
const avEl = document.getElementById('tcd-view-'+tab);
if(atEl) atEl.classList.add('active');
if(avEl) avEl.classList.remove('hidden');
DB.load();
const cls = DB.data.classes.find(c => c.id === ClassCtrl.currentClassId);
if (tab === 'assignments') {
const list = document.getElementById('tcd-assignments-list');
list.innerHTML = '';
if (cls.assignments.length === 0) {
list.innerHTML = `<div class="card" style="text-align:center;">No assessments assigned to this class.</div>`;
}
cls.assignments.forEach(qId => {
const qz = DB.data.quizzes.find(q => q.id === qId);
if (!qz) return;
const el = document.createElement('div');
el.className = 'list-item';
el.innerHTML = `
<div>
<h3 style="margin:0;">${qz.title}</h3>
<p style="margin:0; font-size:0.9rem;" class="mt-2">${qz.questions.length} Questions | ${qz.timeLimit ? qz.timeLimit + ' Mins' : 'Unlimited'}</p>
</div>
<button class="btn btn-danger btn-sm" onclick="ClassCtrl.removeAssignment('${qId}')">Remove from Class</button>
`;
list.appendChild(el);
});
} else if (tab === 'roster') {
const list = document.getElementById('tcd-roster-list');
list.innerHTML = '';
if (cls.students.length === 0) {
list.innerHTML = `<div class="card" style="text-align:center; grid-column:1/-1;">No students enrolled yet. Give them the Class Code!</div>`;
}
cls.students.forEach(sId => {
const stu = DB.data.users.find(u => u.id === sId);
if (!stu) return;
const el = document.createElement('div');
el.className = 'card';
el.style.marginBottom = '0';
el.innerHTML = `<h3 style="margin:0; color:var(--primary);">${stu.name}</h3><p style="margin:0; font-size:0.9rem;">@${stu.username}</p>`;
list.appendChild(el);
});
}
},
showAssignModal: () => {
DB.load();
const myQuizzes = DB.data.quizzes.filter(q => q.author === AppState.user.id);
if (myQuizzes.length === 0) {
return UI.toast("You have no quizzes in your bank to assign.", "error");
}
const cls = DB.data.classes.find(c => c.id === ClassCtrl.currentClassId);
const unassigned = myQuizzes.filter(q => !cls.assignments.includes(q.id));
if (unassigned.length === 0) {
return UI.toast("All your quizzes are already assigned here.", "error");
}
let html = `<div style="display:flex; flex-direction:column; gap:1rem;">`;
unassigned.forEach(q => {
html += `<button class="btn btn-secondary" style="justify-content:flex-start;" onclick="ClassCtrl.assignQuiz('${q.id}')">${q.title}</button>`;
});
html += `</div>`;
document.getElementById('modal-title').innerText = "Assign Assessment";
document.getElementById('modal-desc').innerHTML = html;
const wrap = document.getElementById('modal-input-wrapper');
const cBtn = document.getElementById('modal-confirm-btn');
if(wrap) wrap.classList.add('hidden');
if(cBtn) cBtn.classList.add('hidden');
document.getElementById('modal-container').classList.add('active');
},
assignQuiz: (quizId) => {
DB.load();
const cls = DB.data.classes.find(c => c.id === ClassCtrl.currentClassId);
if (!cls.assignments.includes(quizId)) {
cls.assignments.push(quizId);
}
DB.save();
UI.closeModal();
const cBtn = document.getElementById('modal-confirm-btn');
if(cBtn) cBtn.classList.remove('hidden');
ClassCtrl.switchDetailTab('assignments');
UI.toast("Assessment assigned successfully.", "success");
},
removeAssignment: (quizId) => {
UI.openModal(
"Remove Assessment",
"Remove this quiz from the class? (Student grades will remain intact).",
() => {
DB.load();
const cls = DB.data.classes.find(c => c.id === ClassCtrl.currentClassId);
cls.assignments = cls.assignments.filter(id => id !== quizId);
DB.save();
ClassCtrl.switchDetailTab('assignments');
UI.toast("Assessment removed from class.", "success");
}
);
},
// --- STUDENT FACING LOGIC ---
renderStudentClasses: () => {
DB.load();
const list = document.getElementById('student-class-list');
list.innerHTML = '';
const myClasses = DB.data.classes.filter(c => c.students.includes(AppState.user.id));
if (myClasses.length === 0) {
list.innerHTML = `<div class="card" style="text-align:center; grid-column: 1/-1;"><h3>No classes yet.</h3><p>Click "+ Join a Class" and enter your instructor's code.</p></div>`;
return;
}
myClasses.forEach(cls => {
const teacher = DB.data.users.find(u => u.id === cls.teacherId);
const card = document.createElement('div');
card.className = 'card';
card.style.cursor = 'pointer';
card.onclick = () => ClassCtrl.openStudentClassDetail(cls.id);
card.innerHTML = `
<div style="background: var(--primary); padding: 1.5rem; margin: -2.5rem -2.5rem 1.5rem -2.5rem; border-radius: var(--radius-lg) var(--radius-lg) 0 0;">
<h2 style="color: white; margin: 0;">${cls.name}</h2>
</div>
<p style="font-weight: 700; color: var(--text-main);">Instructor: ${teacher ? teacher.name : 'Unknown'}</p>
<div class="flex-row" style="margin-top: 1.5rem;">
<span class="badge badge-primary">${cls.assignments.length} Assessments</span>
</div>
`;
list.appendChild(card);
});
},
openStudentClassDetail: (classId) => {
ClassCtrl.currentClassId = classId;
DB.load();
const cls = DB.data.classes.find(c => c.id === classId);
const teacher = DB.data.users.find(u => u.id === cls.teacherId);
const nEl = document.getElementById('sc-detail-name');
const tEl = document.getElementById('sc-detail-teacher');
if(nEl) nEl.innerText = cls.name;
if(tEl) tEl.innerText = teacher ? teacher.name : 'Unknown';
Router.navigate('student-class-detail');
const list = document.getElementById('sc-assignments-list');
list.innerHTML = '';
if (cls.assignments.length === 0) {
list.innerHTML = `<div class="card" style="text-align:center;">No active assessments assigned by instructor.</div>`;
return;
}
cls.assignments.forEach(qId => {
const qz = DB.data.quizzes.find(q => q.id === qId);
if (!qz) return;
const submission = DB.data.submissions.find(s => s.quizId === qId && s.studentId === AppState.user.id);
const session = DB.data.sessions.find(s => s.quizId === qId && s.studentId === AppState.user.id);
let statusHtml = `<button class="btn btn-primary" onclick="TestCtrl.initPreTest('${qId}', '${cls.name}')">Start Assessment</button>`;
if (submission) {
statusHtml = `<span class="badge badge-success">Completed</span>`;
} else if (session) {
statusHtml = `<button class="btn btn-warning" style="background:var(--warning); color:white; border:none;" onclick="TestCtrl.initPreTest('${qId}', '${cls.name}')">Resume Paused</button>`;
}
const el = document.createElement('div');
el.className = 'list-item';
el.innerHTML = `
<div>
<h3 style="margin:0;">${qz.title}</h3>
<p style="margin:0; font-size:0.9rem;" class="mt-2">${qz.questions.length} Qs | ${qz.timeLimit ? qz.timeLimit + ' Mins' : 'Unlimited Time'}</p>
</div>
<div class="flex-row">${statusHtml}</div>
`;
list.appendChild(el);
});
}
};
/**
* ============================================================================
* STUDENT CONTROLLER
* ============================================================================
* Description:
* Distinct functions mapped to specific student features, mainly class enrollment
* and historical performance metrics.
* ============================================================================
*/
const StudentCtrl = {
/**
* Resolves class connection using global tracking code.
*/
showJoinModal: () => {
UI.openModal(
"Join a Class",
"Enter the 6-character Class Code provided by your instructor.",
(code) => {
if (!code || code.trim() === '') {
return UI.toast("You must provide a code to join.", "error");
}
const cleanCode = code.trim().toUpperCase();
DB.load();
const targetClass = DB.data.classes.find(c => c.code === cleanCode);
if (!targetClass) {
return UI.toast("Invalid class code. Please check and try again.", "error");
}
if (!targetClass.students) {
targetClass.students = [];
}
if (targetClass.students.includes(AppState.user.id)) {
return UI.toast("You are already enrolled in this class.", "error");
}
targetClass.students.push(AppState.user.id);
DB.save();
UI.toast(`Successfully enrolled in ${targetClass.name}!`, "success");
ClassCtrl.renderStudentClasses();
},
"Class Code (e.g. A1B2C3)"
);
},
renderHistory: () => {
DB.load();
const list = document.getElementById('student-history-list');
list.innerHTML = '';
const subs = DB.data.submissions.filter(s => s.studentId === AppState.user.id).sort((a,b) => new Date(b.timestamp) - new Date(a.timestamp));
if (subs.length === 0) {
return list.innerHTML = '<div class="card" style="text-align:center;">No completed assessments yet.</div>';
}
subs.forEach(sub => {
const qz = DB.data.quizzes.find(q => q.id === sub.quizId);
if (!qz) return;
const el = document.createElement('div');
el.className = 'list-item';
el.innerHTML = `
<div>
<h3 style="margin:0; color:var(--primary);">${qz.title}</h3>
<p style="margin:0; font-size:0.9rem;" class="mt-2">${new Date(sub.timestamp).toLocaleDateString()} at ${new Date(sub.timestamp).toLocaleTimeString()}</p>
</div>
<div class="flex-row">
${sub.status === 'pending' ? `<span class="badge badge-warning">Pending Review</span>` : `<span class="badge ${Utils.getPctColor(sub.earnedPts, sub.totalPts)}">Score: ${sub.earnedPts}/${sub.totalPts}</span>`}
<button class="btn btn-secondary" onclick="StudentCtrl.openReview('${sub.id}')">View Report</button>
</div>`;
list.appendChild(el);
});
},
openReview: (subId) => {
const sub = DB.data.submissions.find(s => s.id === subId);
const qz = DB.data.quizzes.find(q => q.id === sub.quizId);
document.querySelectorAll('.page').forEach(p => p.classList.add('hidden'));
document.getElementById('page-review-test').classList.remove('hidden');
const hdr = document.getElementById('review-header-card');
if (sub.status === 'pending') {
hdr.innerHTML = `<h2 style="color:var(--warning); font-size: 2.5rem;">Review Pending</h2><p style="font-size:1.2rem;">Waiting for manual grading by your instructor.</p>`;
} else {
hdr.innerHTML = `<h2 style="color:var(--primary); font-size:4rem; margin-bottom:0.5rem;">${Math.round((sub.earnedPts/sub.totalPts)*100)}%</h2><p style="font-size:1.2rem; font-weight:700;">${sub.earnedPts} of ${sub.totalPts} Points Earned</p>`;
}
const c = document.getElementById('review-questions');
c.innerHTML = '';
if (!sub.feedback) sub.feedback = {};
qz.questions.forEach((q, idx) => {
const card = document.createElement('div');
card.className = 'question-card';
const sAns = sub.answers[q.id];
const pts = sub.scores[q.id];
const sFeed = sub.feedback[q.id];
let html = `<div class="flex-between" style="margin-bottom:1.5rem;"><h3 style="margin:0; font-size:1.2rem;">${idx+1}. ${q.text}</h3><span class="badge ${pts === q.pts ? 'badge-success' : 'badge-danger'}">${pts} / ${q.pts} Pts</span></div>`;
if (q.type === 'mc') {
q.options.forEach((opt, oIdx) => {
let cls = 'mc-option';
if (opt.correct) cls += ' selected';
else if (sAns === oIdx && !opt.correct) cls += ' wrong';
html += `<div class="${cls}" style="cursor:default; pointer-events:none;"><input type="radio" disabled ${sAns===oIdx ? 'checked' : ''}><span>${opt.text}</span></div>`;
});
} else {
html += `<div style="background:var(--bg-offset); padding:1.5rem; border-radius:var(--radius-md); border:1px solid var(--border); font-size:1.1rem;"><strong>Your Response:</strong><p style="margin-top:0.5rem;">${sAns || '<em>None</em>'}</p></div>`;
}
if (sFeed) {
html += `<div style="background-color: var(--warning-light); border-left: 6px solid var(--warning); padding: 1.5rem; margin-top: 1.5rem; border-radius: var(--radius-sm); font-size: 1.1rem;"><strong>Instructor Feedback:</strong><br><span style="color:#B45309; font-style:italic;">"${sFeed}"</span></div>`;
}
card.innerHTML = html;
c.appendChild(card);
});
}
};
/**
* ============================================================================
* TEACHER QUIZ BUILDER & BANK
* ============================================================================
* Description:
* Manages Quiz creation, saving, editing, previewing, and bulk importing.
* ============================================================================
*/
const BuilderCtrl = {
id: null,
questions: [],
existingIdx: -1,
renderBank: () => {
DB.load();
const list = document.getElementById('teacher-quiz-list');
list.innerHTML = '';
const qs = DB.data.quizzes.filter(q => q.author === AppState.user.id);
if (qs.length === 0) {
return list.innerHTML = `<div class="card" style="text-align:center; grid-column:1/-1;">No assessments in bank. Create one!</div>`;
}
qs.forEach(q => {
const el = document.createElement('div');
el.className = 'card';
el.style.marginBottom = '0';
el.innerHTML = `
<h3 style="margin-bottom: 0.5rem; color: var(--primary);">${q.title}</h3>
<p style="margin-bottom: 1.5rem;">${q.questions.length} Questions</p>
<div class="flex-row">
<button class="btn btn-secondary btn-sm" onclick="BuilderCtrl.init('${q.id}')">Edit</button>
<button class="btn btn-success btn-sm" onclick="TestCtrl.previewAssessment('${q.id}')">Preview</button>
<button class="btn btn-danger btn-sm" onclick="BuilderCtrl.deleteQuiz('${q.id}')">Delete</button>
</div>
`;
list.appendChild(el);
});
},
deleteQuiz: (id) => {
UI.openModal(
"Delete Assessment",
"Permanently delete from bank? (Will be removed from all assigned classes).",
() => {
DB.load();
DB.data.quizzes = DB.data.quizzes.filter(q => q.id !== id);
DB.data.classes.forEach(c => {
if(c.assignments) {
c.assignments = c.assignments.filter(aid => aid !== id);
}
});
DB.save();
BuilderCtrl.renderBank();
UI.toast("Assessment deleted.", "success");
}
);
},
init: (editId = null) => {
DB.load();
BuilderCtrl.id = editId || Utils.id();
BuilderCtrl.questions = [];
BuilderCtrl.existingIdx = DB.data.quizzes.findIndex(q => q.id === editId);
document.getElementById('builder-header-title').innerText = editId ? "Edit Assessment" : "Create Assessment";
document.getElementById('build-title').value = '';
document.getElementById('build-timer').value = '0';
// Clear password field initially
const pwdInput = document.getElementById('build-password');
if (pwdInput) pwdInput.value = '';
if (BuilderCtrl.existingIdx >= 0) {
const qz = DB.data.quizzes[BuilderCtrl.existingIdx];
document.getElementById('build-title').value = qz.title;
document.getElementById('build-timer').value = qz.timeLimit || 0;
document.getElementById('build-rand-q').checked = qz.randQ;
document.getElementById('build-rand-o').checked = qz.randO;
// ADDED: Load existing password into settings
if (pwdInput && qz.password) pwdInput.value = qz.password;
BuilderCtrl.questions = Utils.deepClone(qz.questions);
} else {
BuilderCtrl.addQuestion();
}
Router.navigate('teacher-builder', null);
BuilderCtrl.switchTab('editor');
},
switchTab: (tab) => {
['editor', 'settings', 'bulk'].forEach(t => {
const tEl = document.getElementById('tab-tb-' + t);
const vEl = document.getElementById('build-view-' + t);
if (tEl) tEl.classList.remove('active');
if (vEl) vEl.classList.add('hidden');
});
const aTab = document.getElementById('tab-tb-' + tab);
const aView = document.getElementById('build-view-' + tab);
if (aTab) aTab.classList.add('active');
if (aView) aView.classList.remove('hidden');
if (tab === 'editor') BuilderCtrl.renderQuestions();
},
addQuestion: () => {
BuilderCtrl.questions.push({
id: Utils.id(),
type: 'mc',
text: '',
pts: 1,
options: [{text:'', correct:true}, {text:'', correct:false}]
});
BuilderCtrl.renderQuestions();
},
removeQuestion: (idx) => {
BuilderCtrl.questions.splice(idx, 1);
BuilderCtrl.renderQuestions();
},
renderQuestions: () => {
const c = document.getElementById('build-questions-list');
c.innerHTML = '';
BuilderCtrl.questions.forEach((q, idx) => {
const card = document.createElement('div');
card.className = 'question-card';
let optsHtml = '';
if (q.type === 'mc') {
optsHtml = `<div style="margin-top: 1rem;"><label style="font-weight:700; display:block; margin-bottom:0.5rem;">Options (Check correct)</label>`;
q.options.forEach((opt, oIdx) => {
optsHtml += `
<div class="flex-row" style="margin-top: 0.75rem;">
<input type="radio" name="corr_${q.id}" ${opt.correct ? 'checked' : ''} onchange="BuilderCtrl.setCorrect(${idx}, ${oIdx})" style="width:1.5rem; height:1.5rem; accent-color:var(--primary);">
<input type="text" class="form-control" value="${opt.text}" placeholder="Option text" oninput="BuilderCtrl.updOpt(${idx}, ${oIdx}, this.value)">
<button class="btn btn-danger btn-sm" onclick="BuilderCtrl.rmOpt(${idx}, ${oIdx})">Del</button>
</div>`;
});
optsHtml += `</div><button class="btn btn-secondary btn-sm" style="margin-top:1rem;" onclick="BuilderCtrl.addOpt(${idx})">+ Add Option</button>`;
} else {
optsHtml = `<div class="input-group" style="margin-top: 1.5rem; background: var(--warning-light); padding: 1rem; border-radius: var(--radius-sm);"><label style="color: #B45309;">Instructor Note</label><p style="margin:0; color: #B45309;">Short Answer questions require manual grading via the Grading Center.</p></div>`;
}
card.innerHTML = `
<div class="flex-between" style="margin-bottom: 1.5rem;">
<h3 style="margin:0; color: var(--primary);">Question ${idx+1}</h3>
<div class="flex-row" style="gap: 0.5rem;">
<select class="form-control" style="width:auto; padding:0.5rem;" onchange="BuilderCtrl.changeType(${idx}, this.value)">
<option value="mc" ${q.type==='mc' ? 'selected' : ''}>Multiple Choice</option>
<option value="short" ${q.type==='short' ? 'selected' : ''}>Short Answer</option>
</select>
<input type="number" class="form-control" value="${q.pts}" min="1" style="width:80px; padding:0.5rem;" oninput="BuilderCtrl.questions[${idx}].pts = parseInt(this.value)||1">
<button class="btn btn-danger btn-sm" onclick="BuilderCtrl.removeQuestion(${idx})">Remove</button>
</div>
</div>
<textarea class="form-control" placeholder="Enter prompt..." oninput="BuilderCtrl.questions[${idx}].text = this.value" style="font-size: 1.1rem;">${q.text}</textarea>
${optsHtml}
`;
c.appendChild(card);
});
},
changeType: (qIdx, type) => {
BuilderCtrl.questions[qIdx].type = type;
BuilderCtrl.renderQuestions();
},
updOpt: (qIdx, oIdx, val) => {
BuilderCtrl.questions[qIdx].options[oIdx].text = val;
},
setCorrect: (qIdx, oIdx) => {
BuilderCtrl.questions[qIdx].options.forEach((o, i) => o.correct = (i === oIdx));
},
addOpt: (qIdx) => {
BuilderCtrl.questions[qIdx].options.push({text:'', correct:false});
BuilderCtrl.renderQuestions();
},
rmOpt: (qIdx, oIdx) => {
if (BuilderCtrl.questions[qIdx].options.length <= 2) {
return UI.toast("A multiple choice question needs at least 2 options.", "error");
}
BuilderCtrl.questions[qIdx].options.splice(oIdx, 1);
if (!BuilderCtrl.questions[qIdx].options.some(o => o.correct)) {
BuilderCtrl.questions[qIdx].options[0].correct = true;
}
BuilderCtrl.renderQuestions();
},
save: () => {
const title = document.getElementById('build-title').value.trim();
if (!title) {
return UI.toast("Assessment Title is required.", "error");
}
if (BuilderCtrl.questions.length === 0) {
return UI.toast("At least 1 question is required.", "error");
}
// ADDED: Capture the optional password
const pwdInput = document.getElementById('build-password');
const parsedPassword = pwdInput ? pwdInput.value.trim() : '';
const qz = {
id: BuilderCtrl.id,
author: AppState.user.id,
title: title,
timeLimit: parseInt(document.getElementById('build-timer').value) || 0,
password: parsedPassword, // Store the password in DB
randQ: document.getElementById('build-rand-q').checked,
randO: document.getElementById('build-rand-o').checked,
questions: BuilderCtrl.questions
};
if (BuilderCtrl.existingIdx >= 0) {
DB.data.quizzes[BuilderCtrl.existingIdx] = qz;
} else {
DB.data.quizzes.push(qz);
}
DB.save();
UI.toast(`Assessment securely saved to bank.`, "success");
Router.navigate('teacher-bank', document.querySelector('#nav-teacher .nav-item:nth-child(2)'));
},
processBulk: () => {
SystemLogger.info("Initiating Bulk Data Processing...");
const textArea = document.getElementById('bulk-import-text');
if (!textArea) return;
const text = textArea.value.trim();
if (!text) {
return typeof UI !== 'undefined' && UI.toast ? UI.toast("Please enter text to import.", "error") : alert("No text entered.");
}
let addedCount = 0;
if (text.includes(':') && !text.includes('\n\n')) {
SystemLogger.info("Detected COLON format parsing mode.");
const lines = text.split('\n').map(l => l.trim()).filter(l => l);
lines.forEach(line => {
if (line.includes(':')) {
const parts = line.split(':').map(p => p.trim()).filter(p => p);
if (parts.length >= 2) {
const questionText = parts[0];
const options = [];
for (let i = 1; i < parts.length; i++) {
options.push({ text: parts[i], correct: (i === 1) });
}
if (!BuilderCtrl.questions) BuilderCtrl.questions = [];
BuilderCtrl.questions.push({
id: Utils.id(), type: 'mc', pts: 1, text: questionText, options: options
});
addedCount++;
}
}
});
} else {
SystemLogger.info("Detected PARAGRAPH format parsing mode.");
const blocks = text.split(/\n\s*\n/);
blocks.forEach(block => {
const lines = block.split('\n').map(l => l.trim()).filter(l => l);
if (lines.length < 2) return;
const questionText = lines[0];
const options = [];
for (let i = 1; i < lines.length; i++) {
const isCorrect = lines[i].startsWith('*');
const optionText = isCorrect ? lines[i].substring(1).trim() : lines[i];
options.push({ text: optionText, correct: isCorrect });
}
if (!options.some(o => o.correct) && options.length > 0) {
options[0].correct = true;
}
if (!BuilderCtrl.questions) BuilderCtrl.questions = [];
BuilderCtrl.questions.push({
id: Utils.id(), type: 'mc', pts: 1, text: questionText, options: options
});
addedCount++;
});
}
if (addedCount > 0) {
UI.toast(`Successfully processed and imported ${addedCount} questions!`, "success");
textArea.value = '';
BuilderCtrl.switchTab('editor');
} else {
UI.toast("Could not detect any valid questions. Please verify your formatting.", "error");
}
}
};
/**
* ============================================================================
* TEACHER GRADING CONTROLLER
* ============================================================================
* Description:
* Exposes grading mechanisms and logic to teachers reviewing assessments.
* ============================================================================
*/
const TeacherCtrl = {
renderGrading: () => {
DB.load();
const list = document.getElementById('teacher-subs-list');
list.innerHTML = '';
const tQs = DB.data.quizzes.filter(q => q.author === AppState.user.id).map(q => q.id);
const subs = DB.data.submissions.filter(s => tQs.includes(s.quizId)).sort((a,b) => new Date(b.timestamp) - new Date(a.timestamp));
if (subs.length === 0) {
return list.innerHTML = '<div class="card" style="text-align:center;">No student submissions available to review.</div>';
}
subs.forEach(sub => {
const qz = DB.data.quizzes.find(q => q.id === sub.quizId);
if (!qz) return;
const el = document.createElement('div');
el.className = 'list-item';
el.innerHTML = `
<div>
<h3 style="margin:0; color:var(--primary);">${qz.title}</h3>
<p style="margin:0; font-size:0.95rem; font-weight: 600;" class="mt-2">Student: ${sub.studentName}</p>
<p style="margin:0; font-size:0.85rem; color:var(--text-muted);">${new Date(sub.timestamp).toLocaleString()}</p>
</div>
<div class="flex-row">
${sub.status === 'pending' ? `<span class="badge badge-warning">Needs Review</span>` : `<span class="badge ${Utils.getPctColor(sub.earnedPts, sub.totalPts)}">${sub.earnedPts}/${sub.totalPts} Pts</span>`}
<button class="btn btn-primary" onclick="TeacherCtrl.openGrader('${sub.id}')">Review Submission</button>
</div>`;
list.appendChild(el);
});
},
openGrader: (subId) => {
const sub = DB.data.submissions.find(s => s.id === subId);
const qz = DB.data.quizzes.find(q => q.id === sub.quizId);
document.getElementById('grade-modal-student').innerText = `${sub.studentName} — ${qz.title}`;
const cont = document.getElementById('grade-questions-container');
cont.innerHTML = '';
if (!sub.feedback) sub.feedback = {};
qz.questions.forEach((q, idx) => {
const sAns = sub.answers[q.id] || 'No Response';
const sPts = sub.scores[q.id] || 0;
const sFeed = sub.feedback[q.id] || '';
let ansHtml = '';
if (q.type === 'mc') {
const isCorr = (sAns === q.options.findIndex(o => o.correct));
ansHtml = `
<p style="font-size:1.1rem;"><strong>Selection:</strong> <span style="color:${isCorr ? 'var(--secondary)' : 'var(--danger)'}; font-weight:600;">${q.options[sAns] ? q.options[sAns].text : 'N/A'}</span></p>
<p style="font-size:0.85rem; color:var(--text-muted);">Auto-graded: ${sPts}/${q.pts} Pts</p>`;
} else {
ansHtml = `
<div style="background:var(--bg-offset); padding:1rem; border-radius:var(--radius-md); border:1px solid var(--border); margin-bottom:1rem;">
<strong>Response:</strong><p style="margin-top:0.5rem; white-space:pre-wrap; font-size:1.1rem;">${sAns}</p>
</div>`;
}
const card = document.createElement('div');
card.style.cssText = "margin-bottom: 2rem; padding-bottom: 2rem; border-bottom: 1px dashed var(--border);";
card.innerHTML = `
<h4 style="margin-bottom: 1rem; font-size:1.15rem;">Q${idx+1}: ${q.text} <span class="badge badge-primary" style="float:right;">Max: ${q.pts}</span></h4>
${ansHtml}
<div class="flex-row" style="margin-top: 1rem; align-items:flex-start;">
<div class="input-group" style="flex: 1; margin:0;">
<label>Awarded Points</label>
<input type="number" id="grade-pts-${q.id}" class="form-control" value="${sPts}" min="0" max="${q.pts}" ${q.type==='mc' ? 'disabled' : ''}>
</div>
<div class="input-group" style="flex: 3; margin:0;">
<label>Instructor Feedback (Optional)</label>
<textarea id="grade-feed-${q.id}" class="form-control" style="min-height:60px;" placeholder="Great job! or Needs more detail...">${sFeed}</textarea>
</div>
</div>
`;
cont.appendChild(card);
});
const saveBtn = document.getElementById('grade-save-btn');
const newSaveBtn = saveBtn.cloneNode(true);
saveBtn.parentNode.replaceChild(newSaveBtn, saveBtn);
newSaveBtn.onclick = () => TeacherCtrl.saveGrades(sub.id, qz);
document.getElementById('grade-modal').classList.add('active');
},
saveGrades: (subId, qz) => {
const sub = DB.data.submissions.find(s => s.id === subId);
let newEarned = 0;
qz.questions.forEach(q => {
const ptInput = document.getElementById(`grade-pts-${q.id}`);
const feedInput = document.getElementById(`grade-feed-${q.id}`);
if (ptInput) {
const pts = Math.min(Math.max(parseInt(ptInput.value) || 0, 0), q.pts);
sub.scores[q.id] = pts;
newEarned += pts;
}
if (feedInput) {
sub.feedback[q.id] = feedInput.value.trim();
}
});
sub.earnedPts = newEarned;
sub.status = 'graded';
DB.save();
document.getElementById('grade-modal').classList.remove('active');
TeacherCtrl.renderGrading();
UI.toast("Grades and feedback permanently saved.", "success");
}
};
/**
* ============================================================================
* LIVE ASSESSMENT ENGINE
* ============================================================================
* Description:
* Resolves taking, pausing, previewing, saving, and verifying constraints
* (like passwords and time limits) for an active session.
* ============================================================================
*/
const TestCtrl = {
liveSession: null,
timerInterval: null,
pendingQuizId: null,
pendingClassName: null,
/**
* ADDED: Teacher Preview Assessment Logic
* Bypasses DB writes and loads a mocked session directly into the viewer.
*/
previewAssessment: (quizId) => {
SystemLogger.info(`Initiating Teacher Preview for Assessment: ${quizId}`);
DB.load();
const quiz = DB.data.quizzes.find(q => q.id === quizId);
if (!quiz) return UI.toast("Assessment not found in local database.", "error");
document.querySelectorAll('.page').forEach(p => p.classList.add('hidden'));
document.getElementById('page-take-test').classList.remove('hidden');
document.getElementById('live-test-title').innerText = "PREVIEW: " + quiz.title;
let renderQs = Utils.deepClone(quiz.questions);
// Mirror actual randomized logic to ensure accuracy during preview
if (quiz.randQ) renderQs = Utils.shuffle(renderQs);
renderQs.forEach(q => {
if (q.type === 'mc') {
q.options.forEach((o, i) => o._oIdx = i);
if (quiz.randO) q.options = Utils.shuffle(q.options);
}
});
TestCtrl.liveSession = {
id: 'preview_' + Utils.id(),
quizId: quiz.id,
studentId: AppState.user.id,
renderMap: renderQs,
answers: {},
timeLimit: quiz.timeLimit,
timeRemaining: quiz.timeLimit * 60,
isPreview: true // Marks session as ghost
};
TestCtrl.render();
const tBadge = document.getElementById('live-timer');
if (TestCtrl.liveSession.timeLimit > 0) {
tBadge.classList.remove('hidden');
TestCtrl.updateTimerUI();
TestCtrl.timerInterval = setInterval(() => {
TestCtrl.liveSession.timeRemaining--;
TestCtrl.updateTimerUI();
if (TestCtrl.liveSession.timeRemaining <= 0) {
clearInterval(TestCtrl.timerInterval);
UI.toast("Preview Time Expired!", "warning");
TestCtrl.submit(true);
}
}, 1000);
} else {
tBadge.classList.add('hidden');
}
},
/**
* ADDED: Pre-test sequence now natively enforces Passwords.
*/
initPreTest: (quizId, className) => {
TestCtrl.pendingQuizId = quizId;
TestCtrl.pendingClassName = className;
DB.load();
const qz = DB.data.quizzes.find(q => q.id === quizId);
// Helper to initialize view
const openPreTestView = () => {
document.querySelectorAll('.page').forEach(p => p.classList.add('hidden'));
document.getElementById('page-pre-test').classList.remove('hidden');
document.getElementById('pre-title').innerText = qz.title;
document.getElementById('pre-class').innerText = className;
document.getElementById('pre-time').innerText = qz.timeLimit > 0 ? `${qz.timeLimit} Minutes` : "Unlimited";
};
// Enforce Password check
if (qz.password && qz.password.trim() !== "") {
UI.openModal(
"Assessment Password Required",
"Your instructor has secured this assessment. Enter the password below:",
(pwd) => {
if (pwd !== qz.password) {
return UI.toast("Incorrect password. Access denied.", "error");
}
openPreTestView();
},
"Enter Password..."
);
} else {
openPreTestView();
}
},
startSession: () => {
DB.load();
const quiz = DB.data.quizzes.find(q => q.id === TestCtrl.pendingQuizId);
const existing = DB.data.sessions.find(s => s.quizId === quiz.id && s.studentId === AppState.user.id);
document.querySelectorAll('.page').forEach(p => p.classList.add('hidden'));
document.getElementById('page-take-test').classList.remove('hidden');
document.getElementById('live-test-title').innerText = quiz.title;
if (!existing) {
let renderQs = Utils.deepClone(quiz.questions);
if (quiz.randQ) {
renderQs = Utils.shuffle(renderQs);
}
renderQs.forEach(q => {
if (q.type === 'mc') {
q.options.forEach((o, i) => o._oIdx = i);
if (quiz.randO) q.options = Utils.shuffle(q.options);
}
});
TestCtrl.liveSession = {
id: Utils.id(),
quizId: quiz.id,
studentId: AppState.user.id,
renderMap: renderQs,
answers: {},
timeLimit: quiz.timeLimit,
timeRemaining: quiz.timeLimit * 60
};
} else {
TestCtrl.liveSession = existing;
}
TestCtrl.render();
const tBadge = document.getElementById('live-timer');
if (TestCtrl.liveSession.timeLimit > 0) {
tBadge.classList.remove('hidden');
TestCtrl.updateTimerUI();
TestCtrl.timerInterval = setInterval(() => {
TestCtrl.liveSession.timeRemaining--;
TestCtrl.updateTimerUI();
if (TestCtrl.liveSession.timeRemaining <= 0) {
clearInterval(TestCtrl.timerInterval);
UI.toast("Time expired! Submitting assessment.", "error");
TestCtrl.submit(true);
}
}, 1000);
} else {
tBadge.classList.add('hidden');
}
},
updateTimerUI: () => {
const tr = TestCtrl.liveSession.timeRemaining;
const m = Math.floor(tr / 60).toString().padStart(2, '0');
const s = (tr % 60).toString().padStart(2, '0');
document.querySelector('#live-timer span').innerText = `⏳ ${m}:${s}`;
},
render: () => {
const c = document.getElementById('live-test-questions');
c.innerHTML = '';
const qs = TestCtrl.liveSession.renderMap;
document.getElementById('live-test-progress').innerText = `${qs.length} Questions`;
qs.forEach((q, idx) => {
const card = document.createElement('div');
card.className = 'question-card';
let html = `
<div class="flex-between" style="margin-bottom:1.5rem;">
<h3 style="margin:0; font-size:1.2rem;">${idx+1}. ${q.text}</h3>
<span class="badge badge-primary">${q.pts} Pts</span>
</div>
`;
const savedAns = TestCtrl.liveSession.answers[q.id];
if (q.type === 'mc') {
q.options.forEach((opt) => {
const isCh = savedAns === opt._oIdx;
html += `
<label class="mc-option ${isCh ? 'selected' : ''}">
<input type="radio" name="ans_${q.id}" value="${opt._oIdx}" ${isCh ? 'checked' : ''} onchange="TestCtrl.record('${q.id}', ${opt._oIdx}, this)">
<span style="font-weight:600; font-size:1.05rem;">${opt.text}</span>
</label>`;
});
} else {
html += `<textarea class="form-control" style="font-size:1.1rem;" placeholder="Write your response here..." oninput="TestCtrl.record('${q.id}', this.value, null)">${savedAns || ''}</textarea>`;
}
card.innerHTML = html;
c.appendChild(card);
});
},
record: (qId, val, radioEl) => {
TestCtrl.liveSession.answers[qId] = val;
if (radioEl) {
const p = radioEl.closest('.question-card');
p.querySelectorAll('.mc-option').forEach(el => el.classList.remove('selected'));
radioEl.closest('.mc-option').classList.add('selected');
}
// Do not persist previews to DB
if (TestCtrl.liveSession.isPreview) return;
const exist = DB.data.sessions.findIndex(s => s.id === TestCtrl.liveSession.id);
if (exist >= 0) {
DB.data.sessions[exist] = TestCtrl.liveSession;
} else {
DB.data.sessions.push(TestCtrl.liveSession);
}
DB.save();
},
pause: () => {
clearInterval(TestCtrl.timerInterval);
if (TestCtrl.liveSession.isPreview) {
UI.toast("Preview closed.", "warning");
TestCtrl.liveSession = null;
Router.navigate('teacher-bank', document.querySelector('#nav-teacher .nav-item:nth-child(2)'));
return;
}
UI.toast("Progress securely saved.", "success");
TestCtrl.liveSession = null;
Router.navigate('student-dash', document.querySelector('#nav-student .nav-item'));
},
submit: (auto = false) => {
const finalize = () => {
clearInterval(TestCtrl.timerInterval);
const qz = DB.data.quizzes.find(q => q.id === TestCtrl.liveSession.quizId);
let totalPts = 0;
let earnedPts = 0;
let needsManual = false;
const scores = {};
const feedback = {};
qz.questions.forEach(q => {
totalPts += q.pts;
const ans = TestCtrl.liveSession.answers[q.id];
let pts = 0;
if (q.type === 'mc') {
const corrOrigIdx = q.options.findIndex(o => o.correct);
if (ans === corrOrigIdx) {
pts = q.pts;
}
} else {
needsManual = true;
}
scores[q.id] = pts;
earnedPts += pts;
});
// Intercept and branch logic if this was a teacher preview
if (TestCtrl.liveSession.isPreview) {
TestCtrl.liveSession = null;
UI.toast(`Preview Complete! Theoretical Score: ${earnedPts}/${totalPts}`, "success");
Router.navigate('teacher-bank', document.querySelector('#nav-teacher .nav-item:nth-child(2)'));
return;
}
// Standard Student Submission
DB.data.submissions.push({
id: Utils.id(),
quizId: qz.id,
studentId: AppState.user.id,
studentName: AppState.user.name,
answers: TestCtrl.liveSession.answers,
scores: scores,
feedback: feedback,
totalPts: totalPts,
earnedPts: earnedPts,
status: needsManual ? 'pending' : 'graded',
timestamp: new Date().toISOString()
});
DB.data.sessions = DB.data.sessions.filter(s => s.id !== TestCtrl.liveSession.id);
DB.save();
TestCtrl.liveSession = null;
ConfettiEngine.fire();
UI.toast(needsManual ? "Submitted! Waiting for instructor grading." : `Complete! Initial Score: ${earnedPts}/${totalPts}`, "success");
Router.navigate('student-history', document.querySelector('#nav-student .nav-item:nth-child(2)'));
};
if (auto) {
finalize();
} else {
UI.openModal("Submit Assessment", "Are you sure you want to finalize your answers?", finalize);
}
}
};
/**
* ============================================================================
* APPLICATION BOOTSTRAP
* ============================================================================
* Initiates the core routing protocol.
* Wait until DOM is fully loaded before executing.
* ============================================================================
*/
window.onload = Router.init;
</script>
</body>
</html>