1036 lines
37 KiB
HTML
1036 lines
37 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>SSCTopper | 100,000+ SSC CGL Practice Questions & Mock Tests</title>
|
|
<meta name="description" content="Master the SSC CGL exam with SSCTopper. Access over 100,000 practice questions, detailed explanations, and performance analytics for Math, Reasoning, English, and GK.">
|
|
<meta name="keywords" content="SSC CGL, SSC Practice Questions, CGL Mock Test, SSC Exam Preparation, Quantitative Aptitude, Reasoning, English Language, General Awareness, SSCTopper">
|
|
<meta name="author" content="SSCTopper">
|
|
<link rel="canonical" href="https://ssctopper.com">
|
|
|
|
<!-- Open Graph / Facebook -->
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:url" content="https://ssctopper.com/">
|
|
<meta property="og:title" content="SSCTopper | 100,000+ SSC CGL Practice Questions">
|
|
<meta property="og:description" content="Master the SSC CGL with elite practice tools and thousands of template-generated questions.">
|
|
<meta property="og:image" content="https://ssctopper.com/og_image.png">
|
|
|
|
<!-- Twitter -->
|
|
<meta property="twitter:card" content="summary_large_image">
|
|
<meta property="twitter:url" content="https://ssctopper.com/">
|
|
<meta property="twitter:title" content="SSCTopper | SSC CGL Question Bank">
|
|
<meta property="twitter:description" content="100,000+ Practice Questions for SSC CGL. Free tests and performance tracking.">
|
|
<meta property="twitter:image" content="https://ssctopper.com/og_image.png">
|
|
|
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap" rel="stylesheet">
|
|
<script src="https://accounts.google.com/gsi/client" async defer></script>
|
|
|
|
<!-- Google tag (gtag.js) -->
|
|
<script async src="https://www.googletagmanager.com/gtag/js?id=G-2TK5WJ3GFR"></script>
|
|
<script>
|
|
window.dataLayer = window.dataLayer || [];
|
|
function gtag(){dataLayer.push(arguments);}
|
|
gtag('js', new Date());
|
|
gtag('config', 'G-2TK5WJ3GFR');
|
|
</script>
|
|
|
|
<!-- Microsoft Clarity -->
|
|
<script type="text/javascript">
|
|
(function(c,l,a,r,i,t,y){
|
|
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
|
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
|
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
|
})(window, document, "clarity", "script", "w35209edfr");
|
|
</script>
|
|
|
|
<!-- Structured Data -->
|
|
<script type="application/ld+json">
|
|
{
|
|
"@context": "https://schema.org",
|
|
"@type": "EducationalOrganization",
|
|
"name": "SSCTopper",
|
|
"url": "https://ssctopper.com",
|
|
"logo": "https://ssctopper.com/logo.png",
|
|
"description": "Premium SSC CGL practice platform with 100,000+ questions.",
|
|
"offers": {
|
|
"@type": "Service",
|
|
"name": "SSC CGL Question Bank",
|
|
"description": "Interactive question bank for SSC CGL exam preparation."
|
|
}
|
|
}
|
|
</script>
|
|
<style>
|
|
:root {
|
|
--primary: #6366f1;
|
|
--primary-dark: #4f46e5;
|
|
--bg: #0f172a;
|
|
--card-bg: rgba(30, 41, 59, 0.7);
|
|
--text-main: #f8fafc;
|
|
--text-dim: #94a3b8;
|
|
--glass-border: rgba(255, 255, 255, 0.1);
|
|
--gradient: linear-gradient(135deg, #6366f1 0%, #a855f7 100%);
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
font-family: 'Outfit', sans-serif;
|
|
}
|
|
|
|
body {
|
|
background-color: var(--bg);
|
|
color: var(--text-main);
|
|
min-height: 100vh;
|
|
overflow-x: hidden;
|
|
background-image:
|
|
radial-gradient(circle at 0% 0%, rgba(99, 102, 241, 0.15) 0%, transparent 50%),
|
|
radial-gradient(circle at 100% 100%, rgba(168, 85, 247, 0.15) 0%, transparent 50%);
|
|
}
|
|
|
|
header {
|
|
padding: 2rem;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
backdrop-filter: blur(10px);
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 100;
|
|
border-bottom: 1px solid var(--glass-border);
|
|
}
|
|
|
|
.logo {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
background: var(--gradient);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.stats-bar {
|
|
display: flex;
|
|
gap: 2rem;
|
|
}
|
|
|
|
.stat-item {
|
|
text-align: right;
|
|
}
|
|
|
|
.stat-value {
|
|
font-weight: 700;
|
|
color: var(--primary);
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.75rem;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
main {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 2rem;
|
|
}
|
|
|
|
.hero {
|
|
text-align: center;
|
|
margin-bottom: 4rem;
|
|
padding: 4rem 2rem;
|
|
}
|
|
|
|
.hero h1 {
|
|
font-size: 3.5rem;
|
|
margin-bottom: 1rem;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
.hero p {
|
|
color: var(--text-dim);
|
|
font-size: 1.25rem;
|
|
max-width: 600px;
|
|
margin: 0 auto 2rem;
|
|
}
|
|
|
|
.btn {
|
|
padding: 0.75rem 2rem;
|
|
border-radius: 9999px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
border: none;
|
|
outline: none;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: var(--gradient);
|
|
color: white;
|
|
box-shadow: 0 10px 15px -3px rgba(99, 102, 241, 0.3);
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 20px 25px -5px rgba(99, 102, 241, 0.4);
|
|
}
|
|
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: 1.5rem;
|
|
margin-top: 2rem;
|
|
}
|
|
|
|
.card {
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--glass-border);
|
|
border-radius: 1.5rem;
|
|
padding: 2rem;
|
|
backdrop-filter: blur(12px);
|
|
transition: all 0.3s ease;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.card:hover {
|
|
border-color: var(--primary);
|
|
transform: translateY(-5px);
|
|
}
|
|
|
|
.card h3 {
|
|
margin-bottom: 0.5rem;
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
.card .q-count {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
margin: 1rem 0;
|
|
}
|
|
|
|
.view-section {
|
|
display: none;
|
|
animation: fadeIn 0.5s ease-out;
|
|
}
|
|
|
|
.view-section.active {
|
|
display: block;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
}
|
|
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
/* Practice Area */
|
|
.practice-container {
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.question-card {
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--glass-border);
|
|
border-radius: 2rem;
|
|
padding: 3rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.q-meta {
|
|
display: flex;
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
font-size: 0.875rem;
|
|
color: var(--primary);
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.q-text {
|
|
font-size: 1.5rem;
|
|
line-height: 1.4;
|
|
margin-bottom: 2.5rem;
|
|
}
|
|
|
|
.options-grid {
|
|
display: grid;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.option {
|
|
background: rgba(255, 255, 255, 0.03);
|
|
border: 1px solid var(--glass-border);
|
|
padding: 1.25rem 1.5rem;
|
|
border-radius: 1rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.option:hover {
|
|
background: rgba(255, 255, 255, 0.08);
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
.option.correct {
|
|
background: rgba(34, 197, 94, 0.2);
|
|
border-color: #22c55e;
|
|
}
|
|
|
|
.option.wrong {
|
|
background: rgba(239, 68, 68, 0.2);
|
|
border-color: #ef4444;
|
|
}
|
|
|
|
.opt-letter {
|
|
width: 32px;
|
|
height: 32px;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.explanation {
|
|
margin-top: 2rem;
|
|
padding: 1.5rem;
|
|
background: rgba(99, 102, 241, 0.1);
|
|
border-left: 4px solid var(--primary);
|
|
border-radius: 0 1rem 1rem 0;
|
|
display: none;
|
|
}
|
|
|
|
/* Navigation */
|
|
.nav-breadcrumb {
|
|
margin-bottom: 2rem;
|
|
color: var(--text-dim);
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.nav-link {
|
|
cursor: pointer;
|
|
color: var(--primary);
|
|
}
|
|
|
|
.nav-link:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
/* Syllabus Accordion */
|
|
.syllabus-item {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.syllabus-header {
|
|
padding: 1rem 1.5rem;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 1rem;
|
|
cursor: pointer;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.syllabus-content {
|
|
padding: 1rem 2rem;
|
|
display: none;
|
|
}
|
|
|
|
.subject-card {
|
|
border-left: 6px solid var(--primary);
|
|
}
|
|
|
|
.back-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
color: var(--text-dim);
|
|
cursor: pointer;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.back-btn:hover {
|
|
color: var(--text-main);
|
|
}
|
|
|
|
/* Loading */
|
|
#loader {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 4px;
|
|
background: rgba(99, 102, 241, 0.2);
|
|
z-index: 1000;
|
|
display: none;
|
|
}
|
|
|
|
#loader-inner {
|
|
height: 100%;
|
|
background: var(--gradient);
|
|
width: 30%;
|
|
animation: loading 2s infinite linear;
|
|
}
|
|
|
|
@keyframes loading {
|
|
0% {
|
|
left: -30%;
|
|
}
|
|
|
|
100% {
|
|
left: 100%;
|
|
}
|
|
}
|
|
|
|
.topics-list {
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.topic-badge {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
padding: 0.25rem 0.75rem;
|
|
border-radius: 99px;
|
|
font-size: 0.75rem;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
/* Modal */
|
|
.modal {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(15, 23, 42, 0.8);
|
|
backdrop-filter: blur(8px);
|
|
z-index: 1000;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.modal-content {
|
|
background: var(--bg);
|
|
border: 1px solid var(--glass-border);
|
|
border-radius: 2rem;
|
|
padding: 3rem;
|
|
width: 100%;
|
|
max-width: 400px;
|
|
position: relative;
|
|
}
|
|
|
|
.modal-close {
|
|
position: absolute;
|
|
top: 1.5rem;
|
|
right: 1.5rem;
|
|
cursor: pointer;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
margin-bottom: 0.5rem;
|
|
font-size: 0.875rem;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.form-input {
|
|
width: 100%;
|
|
padding: 0.75rem 1rem;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border: 1px solid var(--glass-border);
|
|
border-radius: 0.75rem;
|
|
color: white;
|
|
outline: none;
|
|
}
|
|
|
|
.form-input:focus {
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
/* Profile Styles */
|
|
.profile-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 2rem;
|
|
margin-bottom: 3rem;
|
|
padding: 2rem;
|
|
background: var(--card-bg);
|
|
border-radius: 2rem;
|
|
border: 1px solid var(--glass-border);
|
|
}
|
|
|
|
.profile-avatar {
|
|
width: 80px;
|
|
height: 80px;
|
|
background: var(--gradient);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.progress-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-top: 2rem;
|
|
}
|
|
|
|
.progress-table th,
|
|
.progress-table td {
|
|
padding: 1rem;
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--glass-border);
|
|
}
|
|
|
|
.progress-bar-bg {
|
|
width: 100%;
|
|
height: 8px;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-bar-fill {
|
|
height: 100%;
|
|
background: var(--gradient);
|
|
transition: width 0.5s ease;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div id="loader">
|
|
<div id="loader-inner"></div>
|
|
</div>
|
|
|
|
<header>
|
|
<div class="logo">
|
|
<span>🚀</span> SSCTopper
|
|
</div>
|
|
<div class="stats-bar" id="stats-header">
|
|
<!-- Loaded via JS -->
|
|
</div>
|
|
<div id="auth-controls">
|
|
<button class="btn" style="background: rgba(255,255,255,0.05); color: white;"
|
|
onclick="showAuthModal('login')">Login</button>
|
|
<button class="btn btn-primary" onclick="showAuthModal('signup')">Sign Up</button>
|
|
</div>
|
|
<div id="user-controls" style="display: none;">
|
|
<span id="user-name" style="margin-right: 1rem; font-weight: 500; font-size: 0.875rem;"></span>
|
|
<button class="btn" style="background: rgba(255,255,255,0.05); color: white;"
|
|
onclick="showProfile()">Profile</button>
|
|
<button class="btn" style="background: rgba(255,255,255,0.05); color: white; margin-left: 0.5rem;" onclick="logout()">Logout</button>
|
|
</div>
|
|
</header>
|
|
|
|
<main>
|
|
<!-- Home View -->
|
|
<section id="view-home" class="view-section active">
|
|
<div class="hero">
|
|
<h1>Master the SSC CGL</h1>
|
|
<p>Access 100,000+ template-generated questions across Quantitative Aptitude, Reasoning, English, and
|
|
General Awareness.</p>
|
|
<button class="btn btn-primary" onclick="showSyllabus()">Browse Syllabus</button>
|
|
</div>
|
|
|
|
<div class="grid" id="subject-grid">
|
|
<!-- Subject cards via JS -->
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Syllabus View -->
|
|
<section id="view-syllabus" class="view-section">
|
|
<div class="back-btn" onclick="showHome()">← Back to Home</div>
|
|
<h1>Full Syllabus Browser</h1>
|
|
<p style="color: var(--text-dim); margin-bottom: 2rem;">Explore topics and start practicing specifically for
|
|
each type.</p>
|
|
<div id="syllabus-container">
|
|
<!-- Syllabus tree via JS -->
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Practice View -->
|
|
<section id="view-practice" class="view-section">
|
|
<div class="back-btn" onclick="showHome()">← Exit Practice</div>
|
|
<div class="practice-container">
|
|
<div class="nav-breadcrumb" id="practice-breadcrumb"></div>
|
|
<div id="question-area">
|
|
<!-- Questions via JS -->
|
|
</div>
|
|
<div style="text-align: center; margin-top: 2rem;">
|
|
<button class="btn btn-primary" id="btn-next" onclick="loadNextQuestion()">Next Question</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Profile View -->
|
|
<section id="view-profile" class="view-section">
|
|
<div class="back-btn" onclick="showHome()">← Back to Home</div>
|
|
<div id="profile-container">
|
|
<!-- Profile data via JS -->
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<!-- Auth Modal -->
|
|
<div id="auth-modal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-close" onclick="closeAuthModal()">✕</div>
|
|
<h2 id="auth-title" style="margin-bottom: 2rem;">Login</h2>
|
|
<div id="auth-error" style="color: #ef4444; margin-bottom: 1rem; font-size: 0.875rem; display: none;"></div>
|
|
|
|
<div id="signup-fields" style="display: none;">
|
|
<div class="form-group">
|
|
<label>Email Address</label>
|
|
<input type="email" id="auth-email" class="form-input" placeholder="you@example.com">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Username</label>
|
|
<input type="text" id="auth-username" class="form-input" placeholder="Your username">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Password</label>
|
|
<input type="password" id="auth-password" class="form-input" placeholder="••••••••">
|
|
</div>
|
|
|
|
<button class="btn btn-primary" style="width: 100%;" id="auth-submit-btn"
|
|
onclick="handleAuth()">Continue</button>
|
|
|
|
<div style="margin: 1.5rem 0; display: flex; align-items: center; gap: 1rem;">
|
|
<div style="flex: 1; height: 1px; background: var(--glass-border);"></div>
|
|
<span style="font-size: 0.75rem; color: var(--text-dim);">OR</span>
|
|
<div style="flex: 1; height: 1px; background: var(--glass-border);"></div>
|
|
</div>
|
|
|
|
<div id="google-signin-btn" style="display: flex; justify-content: center;"></div>
|
|
|
|
<p style="text-align: center; font-size: 0.875rem; color: var(--text-dim); margin-top: 1.5rem;">
|
|
<span id="auth-toggle-text">Don't have an account?</span>
|
|
<span style="color: var(--primary); cursor: pointer;" onclick="toggleAuthMode()"
|
|
id="auth-toggle-link">Sign Up</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
const API_BASE = '';
|
|
let currentSubject = null;
|
|
let currentTopic = null;
|
|
let currentQType = null;
|
|
let currentQuestions = [];
|
|
let currentQIdx = 0;
|
|
let currentUser = null;
|
|
let authMode = 'login';
|
|
let questionStartTime = null;
|
|
|
|
async function api(path, method = 'GET', body = null) {
|
|
document.getElementById('loader').style.display = 'block';
|
|
try {
|
|
const options = { method };
|
|
if (body) {
|
|
options.body = JSON.stringify(body);
|
|
options.headers = { 'Content-Type': 'application/json' };
|
|
}
|
|
const res = await fetch(`${API_BASE}${path}`, options);
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || 'Request failed');
|
|
return data;
|
|
} finally {
|
|
document.getElementById('loader').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
async function init() {
|
|
const stats = await api('/api/stats');
|
|
updateStatsHeader(stats);
|
|
|
|
const syllabus = await api('/api/syllabus');
|
|
renderSubjects(syllabus);
|
|
renderSyllabus(syllabus);
|
|
|
|
// Check if already logged in (try to get profile)
|
|
try {
|
|
const profile = await api('/api/user/profile');
|
|
onLoginSuccess(profile);
|
|
} catch (e) {
|
|
// Not logged in
|
|
}
|
|
}
|
|
|
|
function updateStatsHeader(stats) {
|
|
const container = document.getElementById('stats-header');
|
|
container.innerHTML = `
|
|
<div class="stat-item">
|
|
<div class="stat-value">${(stats.total || 0).toLocaleString()}</div>
|
|
<div class="stat-label">Total Questions</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value">${stats.topic_count || 0}</div>
|
|
<div class="stat-label">Topics</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderSubjects(syllabus) {
|
|
const grid = document.getElementById('subject-grid');
|
|
grid.innerHTML = syllabus.map(s => `
|
|
<div class="card subject-card" onclick="startPractice({subject_id: ${s.id}, name: '${s.name}'})">
|
|
<div class="stat-label">${s.tier}</div>
|
|
<h3>${s.name}</h3>
|
|
<div class="q-count">${(s.question_count || 0).toLocaleString()}</div>
|
|
<div class="stat-label">Practice Questions</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function renderSyllabus(syllabus) {
|
|
const container = document.getElementById('syllabus-container');
|
|
container.innerHTML = syllabus.map(s => `
|
|
<div class="syllabus-item">
|
|
<div class="syllabus-header" onclick="toggleSyllabus(this)">
|
|
<div>
|
|
<strong>${s.name}</strong>
|
|
<span class="topic-badge">${s.question_count.toLocaleString()} Qs</span>
|
|
</div>
|
|
<span>▼</span>
|
|
</div>
|
|
<div class="syllabus-content">
|
|
${s.subtopics.map(st => `
|
|
<div style="margin-bottom: 1.5rem;">
|
|
<h4 style="color: var(--primary); margin-bottom: 0.5rem;">${st.name}</h4>
|
|
<div style="display: grid; gap: 0.5rem;">
|
|
${st.topics.map(t => `
|
|
<div style="background: rgba(255,255,255,0.03); padding: 0.75rem; border-radius: 0.5rem; display: flex; justify-content: space-between; align-items: center;">
|
|
<div>${t.name} <span class="topic-badge">${t.question_count} Qs</span></div>
|
|
<button class="btn btn-primary" style="padding: 0.4rem 1rem; font-size: 0.8rem;"
|
|
onclick="startPractice({topic_id: ${t.id}, name: '${t.name}'})">
|
|
Practice
|
|
</button>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function toggleSyllabus(el) {
|
|
const content = el.nextElementSibling;
|
|
const isVisible = content.style.display === 'block';
|
|
content.style.display = isVisible ? 'none' : 'block';
|
|
el.querySelector('span').innerText = isVisible ? '▼' : '▲';
|
|
}
|
|
|
|
function showView(viewId) {
|
|
document.querySelectorAll('.view-section').forEach(v => v.classList.remove('active'));
|
|
document.getElementById(viewId).classList.add('active');
|
|
window.scrollTo(0, 0);
|
|
}
|
|
|
|
function showHome() { showView('view-home'); }
|
|
function showSyllabus() { showView('view-syllabus'); }
|
|
|
|
async function startPractice(params) {
|
|
document.getElementById('practice-breadcrumb').innerText = params.name;
|
|
currentQuestions = [];
|
|
currentQIdx = 0;
|
|
|
|
let query = '';
|
|
if (params.subject_id) query = `?subject_id=${params.subject_id}`;
|
|
else if (params.topic_id) query = `?topic_id=${params.topic_id}`;
|
|
|
|
const data = await api(`/api/questions${query}&limit=50`);
|
|
currentQuestions = data.questions;
|
|
|
|
showView('view-practice');
|
|
renderQuestion();
|
|
}
|
|
|
|
function renderQuestion() {
|
|
if (currentQIdx >= currentQuestions.length) {
|
|
document.getElementById('question-area').innerHTML = `
|
|
<div class="question-card" style="text-align: center;">
|
|
<h2>Session Complete!</h2>
|
|
<p style="margin: 1rem 0; color: var(--text-dim);">You've practiced all loaded questions.</p>
|
|
<button class="btn btn-primary" onclick="showHome()">Back to Home</button>
|
|
</div>
|
|
`;
|
|
document.getElementById('btn-next').style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
const q = currentQuestions[currentQIdx];
|
|
const area = document.getElementById('question-area');
|
|
|
|
area.innerHTML = `
|
|
<div class="question-card">
|
|
<div class="q-meta">${q.subject} • ${q.topic} • ${q.qtype}</div>
|
|
<div class="q-text">${q.question_text}</div>
|
|
<div class="options-grid">
|
|
${Object.entries(q.options).map(([key, val]) => `
|
|
<div class="option" onclick="checkAnswer(this, '${key}')">
|
|
<span class="opt-letter">${key}</span>
|
|
<span class="opt-val">${val}</span>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
<div class="explanation" id="ex-box">
|
|
<strong style="color: var(--primary)">Explanation:</strong><br>
|
|
${q.explanation || 'No explanation available.'}
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.getElementById('btn-next').style.display = 'none';
|
|
questionStartTime = Date.now();
|
|
}
|
|
|
|
function checkAnswer(el, choice) {
|
|
const q = currentQuestions[currentQIdx];
|
|
const options = el.parentElement.querySelectorAll('.option');
|
|
|
|
// Disable further clicks
|
|
options.forEach(opt => opt.onclick = null);
|
|
|
|
if (choice === q.correct_option) {
|
|
el.classList.add('correct');
|
|
} else {
|
|
el.classList.add('wrong');
|
|
// Find and highlight correct answer
|
|
options.forEach(opt => {
|
|
if (opt.querySelector('.opt-letter').innerText === q.correct_option) {
|
|
opt.classList.add('correct');
|
|
}
|
|
});
|
|
}
|
|
|
|
document.getElementById('ex-box').style.display = 'block';
|
|
document.getElementById('btn-next').style.display = 'inline-block';
|
|
|
|
// Report progress if logged in
|
|
const timeTaken = (Date.now() - questionStartTime) / 1000;
|
|
reportProgress(q.id, choice === q.correct_option, timeTaken);
|
|
}
|
|
|
|
function loadNextQuestion() {
|
|
currentQIdx++;
|
|
renderQuestion();
|
|
}
|
|
|
|
// Auth Logic
|
|
function showAuthModal(mode) {
|
|
authMode = mode;
|
|
document.getElementById('auth-modal').style.display = 'flex';
|
|
updateAuthUI();
|
|
initGoogleBtn();
|
|
}
|
|
|
|
function initGoogleBtn() {
|
|
if (typeof google === 'undefined') return;
|
|
|
|
google.accounts.id.initialize({
|
|
client_id: "273072123939-dd82h4o1rt3k7811sri6qgsig73b3916.apps.googleusercontent.com",
|
|
callback: handleGoogleCallback
|
|
});
|
|
|
|
google.accounts.id.renderButton(
|
|
document.getElementById("google-signin-btn"),
|
|
{ theme: "dark", size: "large", width: 340, shape: "pill" }
|
|
);
|
|
}
|
|
|
|
async function handleGoogleCallback(response) {
|
|
try {
|
|
const data = await api('/api/auth/google', 'POST', { id_token: response.credential });
|
|
onLoginSuccess(data);
|
|
closeAuthModal();
|
|
} catch (err) {
|
|
const errorEl = document.getElementById('auth-error');
|
|
errorEl.innerText = err.message;
|
|
errorEl.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
function closeAuthModal() {
|
|
document.getElementById('auth-modal').style.display = 'none';
|
|
document.getElementById('auth-error').style.display = 'none';
|
|
}
|
|
|
|
function toggleAuthMode() {
|
|
authMode = authMode === 'login' ? 'signup' : 'login';
|
|
updateAuthUI();
|
|
}
|
|
|
|
function updateAuthUI() {
|
|
const isLogin = authMode === 'login';
|
|
document.getElementById('auth-title').innerText = isLogin ? 'Login' : 'Sign Up';
|
|
document.getElementById('signup-fields').style.display = isLogin ? 'none' : 'block';
|
|
document.getElementById('auth-toggle-text').innerText = isLogin ? "Don't have an account?" : "Already have an account?";
|
|
document.getElementById('auth-toggle-link').innerText = isLogin ? 'Sign Up' : 'Login';
|
|
}
|
|
|
|
async function handleAuth() {
|
|
const username = document.getElementById('auth-username').value;
|
|
const password = document.getElementById('auth-password').value;
|
|
const email = document.getElementById('auth-email').value;
|
|
const errorEl = document.getElementById('auth-error');
|
|
|
|
errorEl.style.display = 'none';
|
|
|
|
try {
|
|
if (authMode === 'signup') {
|
|
await api('/api/auth/signup', 'POST', { username, email, password });
|
|
// After signup, auto-login
|
|
authMode = 'login';
|
|
}
|
|
|
|
const data = await api('/api/auth/login', 'POST', { username, password });
|
|
onLoginSuccess(data);
|
|
closeAuthModal();
|
|
} catch (err) {
|
|
errorEl.innerText = err.message;
|
|
errorEl.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
function onLoginSuccess(user) {
|
|
currentUser = user;
|
|
document.getElementById('auth-controls').style.display = 'none';
|
|
document.getElementById('user-controls').style.display = 'flex';
|
|
document.getElementById('user-name').innerText = user.username;
|
|
closeAuthModal();
|
|
}
|
|
|
|
function logout() {
|
|
currentUser = null;
|
|
document.cookie = "session_id=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
|
location.reload();
|
|
}
|
|
|
|
async function showProfile(timeframe = 'overall') {
|
|
try {
|
|
const profile = await api(`/api/user/profile?timeframe=${timeframe}`);
|
|
renderProfile(profile, timeframe);
|
|
showView('view-profile');
|
|
} catch (err) {
|
|
showAuthModal('login');
|
|
}
|
|
}
|
|
|
|
function renderProfile(profile, currentTimeframe = 'overall') {
|
|
const container = document.getElementById('profile-container');
|
|
container.innerHTML = `
|
|
<div class="profile-header">
|
|
<div class="profile-avatar">${profile.username[0].toUpperCase()}</div>
|
|
<div style="flex: 1">
|
|
<h1>${profile.username}</h1>
|
|
<p style="color: var(--text-dim)">${profile.email} • Joined ${new Date(profile.joined).toLocaleDateString()}</p>
|
|
</div>
|
|
<div>
|
|
<select class="form-input" style="width: auto; background: rgba(255,255,255,0.1);" onchange="showProfile(this.value)">
|
|
<option value="overall" ${currentTimeframe === 'overall' ? 'selected' : ''}>Overall Stats</option>
|
|
<option value="daily" ${currentTimeframe === 'daily' ? 'selected' : ''}>Last 24 Hours</option>
|
|
<option value="weekly" ${currentTimeframe === 'weekly' ? 'selected' : ''}>Last 7 Days</option>
|
|
<option value="monthly" ${currentTimeframe === 'monthly' ? 'selected' : ''}>This Month</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid" style="margin-bottom: 3rem;">
|
|
<div class="card">
|
|
<div class="stat-label">Total Attempts</div>
|
|
<div class="q-count">${profile.stats.total_attempts}</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="stat-label">Correct Answers</div>
|
|
<div class="q-count">${profile.stats.correct_attempts}</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="stat-label">Accuracy</div>
|
|
<div class="q-count">${profile.stats.accuracy}%</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="stat-label">Avg Pace (sec)</div>
|
|
<div class="q-count">${profile.stats.avg_time}s</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h2 style="margin-bottom: 2rem;">Topic-wise Analytics</h2>
|
|
<div class="card" style="padding: 0; overflow: hidden; background: rgba(255,255,255,0.02);">
|
|
<table class="progress-table">
|
|
<thead>
|
|
<tr>
|
|
<th style="text-align: left">Subject > Topic</th>
|
|
<th>Mastered</th>
|
|
<th>Pace (Avg)</th>
|
|
<th style="text-align: right">Progress</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${profile.topic_progress.filter(t => t.total > 0).map(t => `
|
|
<tr>
|
|
<td>
|
|
<div style="font-weight: 600">${t.topic}</div>
|
|
<div style="font-size: 0.75rem; color: var(--text-dim)">${t.subject} > ${t.subtopic}</div>
|
|
</td>
|
|
<td style="text-align: center">${t.answered} / ${t.total}</td>
|
|
<td style="text-align: center">${t.avg_time}s</td>
|
|
<td style="text-align: right">
|
|
<div style="display: flex; align-items: center; gap: 0.8rem; justify-content: flex-end;">
|
|
<div class="progress-bar-bg" style="width: 120px; height: 10px; margin: 0; background: rgba(255,255,255,0.1); border-radius: 5px; overflow: hidden;">
|
|
<div class="progress-bar-fill" style="width: ${t.percent}%; height: 100%; background: var(--gradient);"></div>
|
|
</div>
|
|
<span style="font-size: 0.875rem; font-weight: 500; min-width: 45px;">${t.percent}%</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async function reportProgress(questionId, isCorrect, timeTaken) {
|
|
if (!currentUser) return;
|
|
try {
|
|
await api('/api/user/progress', 'POST', { question_id: questionId, is_correct: isCorrect, time_taken: timeTaken });
|
|
} catch (e) {
|
|
console.error("Failed to report progress", e);
|
|
}
|
|
}
|
|
|
|
init();
|
|
</script>
|
|
</body>
|
|
|
|
</html> |