954 lines
37 KiB
HTML
954 lines
37 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="kk">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>ҰБТ Тарих — Дайындық</title>
|
||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📚</text></svg>">
|
||
<style>
|
||
:root{--ink:#0F1218;--cyan:#00E5FF;--cyan-50:#E8FCFF;--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7}
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{font:17px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;color:var(--ink);background:var(--white)}
|
||
.container{max-width:1140px;margin:0 auto;padding:80px 24px}
|
||
.hero{background:var(--ink);color:var(--white);padding:100px 24px}
|
||
.hero h1{font-size:56px;font-weight:800;line-height:1.05;margin-bottom:24px}
|
||
.hero p{font-size:20px;color:#9aa3b2;max-width:600px;margin-bottom:32px}
|
||
.btn{display:inline-block;background:var(--cyan);color:var(--ink);padding:14px 28px;border-radius:8px;font-weight:700;text-decoration:none;cursor:pointer;border:none}
|
||
.btn:hover{opacity:0.9}
|
||
.section{padding:80px 24px}
|
||
.section h2{font-size:36px;font-weight:700;margin-bottom:24px}
|
||
.card{background:var(--gray-100);border-radius:16px;padding:32px;margin-bottom:16px}
|
||
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:20px}
|
||
.stat-card{background:var(--cyan-50);border-radius:12px;padding:24px;text-align:center}
|
||
.stat-num{font-size:48px;font-weight:800;color:var(--ink)}
|
||
.stat-label{color:var(--gray-500);margin-top:8px}
|
||
input,select,textarea{width:100%;padding:12px;border:1px solid var(--gray-100);border-radius:8px;margin-bottom:16px;font-size:17px}
|
||
input:focus,select:focus,textarea:focus{outline:2px solid var(--cyan);border-color:transparent}
|
||
.nav{background:var(--ink);padding:12px 16px;position:sticky;top:0;z-index:100}
|
||
.nav-content{max-width:1140px;margin:0 auto;display:flex;justify-content:space-between;align-items:center;position:relative}
|
||
.nav-logo{color:var(--white);font-weight:800;font-size:20px;text-decoration:none;z-index:101}
|
||
.nav-links{display:flex;gap:16px}
|
||
.nav-links a{color:var(--white);text-decoration:none;font-weight:600;font-size:14px;white-space:nowrap}
|
||
.nav-links a:hover{color:var(--cyan)}
|
||
.nav-links a.admin-only{display:none}
|
||
.mobile-menu-btn{display:none;background:none;border:none;color:var(--white);font-size:28px;cursor:pointer;padding:4px}
|
||
.mobile-menu{display:none;position:fixed;top:52px;left:0;right:0;background:var(--ink);padding:16px;z-index:99;box-shadow:0 4px 12px rgba(0,0,0,0.3);max-height:80vh;overflow-y:auto}
|
||
.mobile-menu.show{display:block}
|
||
.mobile-menu a{display:block;color:var(--white);padding:14px 0;text-decoration:none;font-size:17px;border-bottom:1px solid rgba(255,255,255,0.08)}
|
||
.mobile-menu a:last-child{border-bottom:none}
|
||
.mobile-menu a:hover{color:var(--cyan)}
|
||
.mobile-menu a.admin-only{display:none}
|
||
.mobile-menu-btn{display:none;background:none;border:none;color:var(--white);font-size:24px;cursor:pointer}
|
||
.mobile-menu{display:none;position:fixed;top:60px;left:0;right:0;background:var(--ink);padding:20px;z-index:99;box-shadow:0 4px 12px rgba(0,0,0,0.3)}
|
||
.mobile-menu a{display:block;color:var(--white);padding:16px 0;text-decoration:none;font-size:18px;border-bottom:1px solid rgba(255,255,255,0.1)}
|
||
.mobile-menu a:hover{color:var(--cyan)}
|
||
.page{display:none}
|
||
.page.active{display:block}
|
||
.topic-card{background:linear-gradient(135deg,var(--ink),#1a1f29);color:var(--white);border-radius:16px;padding:24px;cursor:pointer;transition:transform 0.2s}
|
||
.topic-card:hover{transform:translateY(-4px)}
|
||
.topic-card h3{font-size:20px;margin-bottom:12px}
|
||
.topic-card p{color:#9aa3b2;font-size:14px}
|
||
.video-card{border-radius:16px;overflow:hidden;background:var(--gray-100}
|
||
.video-placeholder{width:100%;height:200px;background:linear-gradient(135deg,var(--cyan-50),var(--gray-100));display:flex;align-items:center;justify-content:center;font-size:48px}
|
||
.video-info{padding:16px}
|
||
.video-info h4{font-size:18px;margin-bottom:8px}
|
||
.video-info p{color:var(--gray-500);font-size:14px}
|
||
.test-question{background:var(--white);border:1px solid var(--gray-100);border-radius:12px;padding:20px;margin-bottom:16px}
|
||
.test-question h4{font-size:18px;margin-bottom:16px}
|
||
.test-option{display:block;padding:12px;border:1px solid var(--gray-100);border-radius:8px;margin-bottom:8px;cursor:pointer}
|
||
.test-option:hover{background:var(--cyan-50)}
|
||
.test-option.selected{background:var(--cyan);border-color:var(--cyan)}
|
||
.test-option.correct{background:#22c55e;border-color:#22c55e;color:white}
|
||
.test-option.wrong{background:#ef4444;border-color:#ef4444;color:white}
|
||
.upload-area{border:2px dashed var(--gray-100);border-radius:16px;padding:40px;text-align:center;background:var(--gray-100;cursor:pointer}
|
||
.upload-area:hover{border-color:var(--cyan)}
|
||
.badge{display:inline-block;padding:4px 12px;border-radius:20px;font-size:12px;font-weight:600;background:var(--cyan-50);color:var(--ink)}
|
||
.progress-bar{height:8px;background:var(--gray-100);border-radius:4px;overflow:hidden}
|
||
.progress-fill{height:100%;background:var(--cyan);transition:width 0.3s}
|
||
@media(max-width:640px){
|
||
.hero h1{font-size:28px}.hero p{font-size:15px}.section h2{font-size:22px}
|
||
.nav-links{display:none}.mobile-menu-btn{display:block}
|
||
.container{padding:32px 14px}.card{padding:16px;border-radius:12px}.topic-card{padding:14px;border-radius:12px}
|
||
.stat-num{font-size:32px}.stat-label{font-size:13px}.btn{padding:12px 18px;font-size:15px;border-radius:8px}
|
||
input,select,textarea{font-size:16px;padding:12px;border-radius:8px;margin-bottom:12px}
|
||
.test-option{padding:12px;font-size:15px;border-radius:6px;margin-bottom:6px}
|
||
.video-placeholder{height:180px}.nav{padding:10px 14px}.nav-logo{font-size:17px}
|
||
.grid{grid-template-columns:1fr;gap:12px}.upload-area{padding:24px 16px}
|
||
.topic-card h3{font-size:17px}.topic-card p{font-size:13px}
|
||
.badge{font-size:11px;padding:3px 10px;border-radius:16px}
|
||
.test-question{padding:16px;border-radius:10px;margin-bottom:12px}
|
||
#prevBtn,#nextBtn,#finishBtn{flex:1;padding:14px}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<nav class="nav">
|
||
<div class="nav-content">
|
||
<a href="#" class="nav-logo" onclick="showPage('home');return false">📚 ҰБТ Тарих</a>
|
||
<button class="mobile-menu-btn" onclick="toggleMobileMenu()">☰</button>
|
||
<div class="nav-links">
|
||
<a href="#" onclick="showPage('home');return false">Тесттер</a>
|
||
<a href="#" onclick="showPage('videos');return false">Видео</a>
|
||
<a href="#" onclick="showPage('dashboard');return false">Дашборд</a>
|
||
<a href="#" onclick="showPage('admin');return false" class="admin-only" id="navAdminLink">Админ</a>
|
||
<a href="#" onclick="showPage('login');return false" id="loginLink">Кіру</a>
|
||
</div>
|
||
</div>
|
||
<div class="mobile-menu" id="mobileMenu">
|
||
<a href="#" onclick="showPage('home');toggleMobileMenu()">Тесттер</a>
|
||
<a href="#" onclick="showPage('videos');toggleMobileMenu()">Видео</a>
|
||
<a href="#" onclick="showPage('dashboard');toggleMobileMenu()">Дашборд</a>
|
||
<a href="#" onclick="showPage('admin');toggleMobileMenu()" class="admin-only" id="mobileNavAdminLink">Админ</a>
|
||
<a href="#" onclick="showPage('login');toggleMobileMenu()" id="mobileLoginLink">Кіру</a>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- LOGIN PAGE -->
|
||
<div id="login" class="page">
|
||
<div class="container" style="max-width:400px;margin-top:60px">
|
||
<div class="card">
|
||
<h2 style="text-align:center;margin-bottom:24px">Кіру</h2>
|
||
<form id="loginForm">
|
||
<input type="tel" id="loginPhone" placeholder="+7 (___) ___-__-__" required>
|
||
<input type="password" id="loginPassword" placeholder="Құпиясөз" required>
|
||
<button type="submit" class="btn" style="width:100%">Кіру</button>
|
||
</form>
|
||
<p style="text-align:center;margin-top:16px;color:var(--gray-500)">
|
||
Әлі тіркелмегенсіз бе? <a href="#" onclick="showPage('register');return false" style="color:var(--cyan)">Тіркеу</a>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- REGISTER PAGE -->
|
||
<div id="register" class="page">
|
||
<div class="container" style="max-width:400px;margin-top:60px">
|
||
<div class="card">
|
||
<h2 style="text-align:center;margin-bottom:24px">Тіркеу</h2>
|
||
<form id="registerForm">
|
||
<input type="tel" id="regPhone" placeholder="+7 (___) ___-__-__" required>
|
||
<input type="text" id="regName" placeholder="Аты-жөніңіз" required>
|
||
<input type="password" id="regPassword" placeholder="Құпиясөз" required>
|
||
<button type="submit" class="btn" style="width:100%">Тіркеу</button>
|
||
</form>
|
||
<p style="text-align:center;margin-top:16px;color:var(--gray-500)">
|
||
Аккаунт бар ма? <a href="#" onclick="showPage('login');return false" style="color:var(--cyan)">Кіру</a>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- HOME PAGE -->
|
||
<div id="home" class="page active">
|
||
<section class="hero">
|
||
<div class="container" style="padding:40px 24px">
|
||
<h1>ҰБТ Тарихқа Дайындық</h1>
|
||
<p>Тест тапсырмалары, видео-сабақтар және жеке прогресс бақылау жүйесі</p>
|
||
<a href="#topics" class="btn">Бастау</a>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="topics" class="section">
|
||
<div class="container">
|
||
<h2>Тақырыптар</h2>
|
||
<div class="grid" id="topicsGrid"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section" style="background:var(--cyan-50)">
|
||
<div class="container">
|
||
<h2>Жаңа Тесттер</h2>
|
||
<div class="grid" id="recentTests"></div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<!-- VIDEOS PAGE -->
|
||
<div id="videos" class="page">
|
||
<div class="container">
|
||
<h2 style="margin-bottom:24px">Видео Сабақтар</h2>
|
||
<div class="grid" id="videosGrid"></div>
|
||
<div id="noVideos" style="display:none;text-align:center;padding:40px">
|
||
<div style="font-size:48px;margin-bottom:16px">🎥</div>
|
||
<h3 style="margin-bottom:8px">Видео жоқ</h3>
|
||
<p style="color:var(--gray-500)">Админ панельден видео файл жүктеңіз</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TEST PAGE -->
|
||
<div id="test" class="page">
|
||
<div class="container">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:24px">
|
||
<h2 id="testTitle">Тест</h2>
|
||
<div>
|
||
<span class="badge" id="questionNum">1/20</span>
|
||
</div>
|
||
</div>
|
||
<div class="progress-bar" style="margin-bottom:24px">
|
||
<div class="progress-fill" id="testProgress" style="width:0%"></div>
|
||
</div>
|
||
<div id="testContent"></div>
|
||
<div style="margin-top:24px;display:flex;gap:12px">
|
||
<button class="btn" id="prevBtn" onclick="prevQuestion()" style="background:var(--gray-100)">Артқа</button>
|
||
<button class="btn" id="nextBtn" onclick="nextQuestion()">Келесі</button>
|
||
<button class="btn" id="finishBtn" onclick="finishTest()" style="display:none;background:#22c55e">Аяқтау</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- DASHBOARD PAGE -->
|
||
<div id="dashboard" class="page">
|
||
<div class="container">
|
||
<h2 style="margin-bottom:24px">Менің Прогрессім</h2>
|
||
<div class="grid" style="margin-bottom:40px">
|
||
<div class="stat-card">
|
||
<div class="stat-num" id="dashTests">0</div>
|
||
<div class="stat-label">Өтілген тесттер</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-num" id="dashPercent">0%</div>
|
||
<div class="stat-label">Дұрыс жауаптар</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-num" id="dashTime">0с</div>
|
||
<div class="stat-label">Дайындық уақыты</div>
|
||
</div>
|
||
</div>
|
||
<h3 style="margin-bottom:16px">Соңғы Нәтижелер</h3>
|
||
<div id="resultsList"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ADMIN PAGE -->
|
||
<div id="admin" class="page">
|
||
<div class="container">
|
||
<h2 style="margin-bottom:24px">Админ Панель</h2>
|
||
|
||
<div class="card" style="margin-bottom:24px">
|
||
<h3 style="margin-bottom:16px">📊 Оқушылар</h3>
|
||
<div id="studentsList"></div>
|
||
</div>
|
||
|
||
<div class="card" style="margin-bottom:24px">
|
||
<h3 style="margin-bottom:16px">Тест Жүктеу</h3>
|
||
<div class="upload-area" onclick="document.getElementById('testFile').click()">
|
||
<div style="font-size:48px;margin-bottom:16px">📁</div>
|
||
<p>Тест файлы таңдау (Excel/CSV)</p>
|
||
<input type="file" id="testFile" accept=".xlsx,.xls,.csv" style="display:none" onchange="handleTestUpload(this)">
|
||
</div>
|
||
<div id="testUploadResult" style="margin-top:16px;color:var(--gray-500)"></div>
|
||
</div>
|
||
|
||
<div class="card" style="margin-bottom:24px">
|
||
<h3 style="margin-bottom:16px">Видео Жүктеу</h3>
|
||
<div class="upload-area" onclick="document.getElementById('videoFile').click()">
|
||
<div style="font-size:48px;margin-bottom:16px">🎥</div>
|
||
<p>Видео файл таңдау (MP4)</p>
|
||
<input type="file" id="videoFile" accept=".mp4,.avi,.mov" style="display:none" onchange="handleVideoUpload(this)">
|
||
</div>
|
||
<div id="videoUploadResult" style="margin-top:16px;color:var(--gray-500)"></div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3 style="margin-bottom:16px">Барлық Тесттер</h3>
|
||
<div id="adminTestsList"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// MOCK DATA - бастапқы деректер
|
||
const TOPICS = [
|
||
{ id: 1, title: "Қазақстан Ежелгі Дәуірде", desc: "Алғашқы адамдар, қола дәуірі, сақтар", tests: 5 },
|
||
{ id: 2, title: "Орта Ғасырлардағы Қазақстан", desc: "Түркі қағанаты, Қарахан мемлекеті", tests: 8 },
|
||
{ id: 3, title: "Қазақ Хандығы", desc: "Қазақ хандығының құрылуы, дамуы", tests: 12 },
|
||
{ id: 4, title: "Ресей Империясы", desc: "Қазақстан Ресей құрамында", tests: 7 },
|
||
{ id: 5, title: "Кеңес Дәуірі", desc: "Қазан төңкерісі, КСРО кезеңі", tests: 15 },
|
||
{ id: 6, title: "Тәуелсіз Қазақстан", desc: "1991 жылдан бері", tests: 6 }
|
||
];
|
||
|
||
const MOCK_TESTS = [
|
||
{ id: 1, topicId: 1, title: "Ежелгі Қазақстан", questions: 20, time: "20 мин" },
|
||
{ id: 2, topicId: 2, title: "Орта Ғасырлар", questions: 25, time: "25 мин" },
|
||
{ id: 3, topicId: 3, title: "Қазақ Хандығы", questions: 30, time: "30 мин" }
|
||
];
|
||
|
||
const MOCK_VIDEOS = [
|
||
{ id: 1, title: "Қазақ Хандығының Құрылуы", duration: "15:30", topic: "Қазақ Хандығы" },
|
||
{ id: 2, title: "Сақтардың Мәдениеті", duration: "12:45", topic: "Ежелгі Дәуір" },
|
||
{ id: 3, title: "Тәуелсіздік Жолы", duration: "20:15", topic: "Тәуелсіз Қазақстан" }
|
||
];
|
||
|
||
const SAMPLE_QUESTIONS = [
|
||
{ q: "Қазақ хандығы қай жылы құрылды?", options: ["1465", "1456", "1564", "1380"], correct: 0 },
|
||
{ q: "Алтын адам қай жерден табылды?", options: ["Есік қорғаны", "Берел қорғаны", "Тамғалы", "Отырар"], correct: 0 },
|
||
{ q: "Қазақтың тұңғыш ханы кім?", options: ["Керей", "Жәнібек", "Қасым", "Хақназар"], correct: 0 },
|
||
{ q: "Ұлы Жібек жолы қай ғасырда дамыды?", options: ["V-XV ғғ", "III-VII ғғ", "X-XV ғғ", "I-III ғғ"], correct: 2 },
|
||
{ q: "Абылай хан қай жылы туылды?", options: ["1711", "1723", "1705", "1718"], correct: 0 }
|
||
];
|
||
|
||
let currentUser = null;
|
||
let currentTest = null;
|
||
let currentQuestionIndex = 0;
|
||
let userAnswers = [];
|
||
let testStartTime = null;
|
||
const ADMIN_PHONE = '77474700532';
|
||
const ADMIN_PASS = 'Aa123456';
|
||
const ADMIN_NAME = 'Бижігіт Айбек';
|
||
|
||
// USER DATA - локальды сақтау
|
||
function getUserData() {
|
||
if (!currentUser) return null;
|
||
const data = localStorage.getItem('ubt_user_' + currentUser);
|
||
return data ? JSON.parse(data) : { tests: [], results: [], totalTime: 0 };
|
||
}
|
||
|
||
function saveUserData(data) {
|
||
if (!currentUser) return;
|
||
localStorage.setItem('ubt_user_' + currentUser, JSON.stringify(data));
|
||
}
|
||
|
||
function getAllStudents() {
|
||
const users = JSON.parse(localStorage.getItem('ubt_students') || '{}');
|
||
return users;
|
||
}
|
||
|
||
function saveStudent(phone, data) {
|
||
const users = JSON.parse(localStorage.getItem('ubt_students') || '{}');
|
||
users[phone] = data;
|
||
localStorage.setItem('ubt_students', JSON.stringify(users));
|
||
}
|
||
|
||
function banStudent(phone) {
|
||
const users = JSON.parse(localStorage.getItem('ubt_students') || '{}');
|
||
if (users[phone]) {
|
||
users[phone].banned = true;
|
||
localStorage.setItem('ubt_students', JSON.stringify(users));
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function unbanStudent(phone) {
|
||
const users = JSON.parse(localStorage.getItem('ubt_students') || '{}');
|
||
if (users[phone]) {
|
||
users[phone].banned = false;
|
||
localStorage.setItem('ubt_students', JSON.stringify(users));
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// PAGE NAVIGATION
|
||
function showPage(pageId) {
|
||
// Check admin-only pages
|
||
if (pageId === 'admin' && currentUser !== 'admin') {
|
||
alert('⛔ Админ панельге кіру рұқсаты жоқ!');
|
||
showPage('home');
|
||
return;
|
||
}
|
||
|
||
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
||
document.getElementById(pageId).classList.add('active');
|
||
if (pageId === 'dashboard') updateDashboard();
|
||
if (pageId === 'admin') {
|
||
if (currentUser === 'admin') {
|
||
updateAdminList();
|
||
renderStudentsList();
|
||
} else {
|
||
showPage('home');
|
||
return;
|
||
}
|
||
}
|
||
if (pageId === 'home') { renderTopics(); renderRecentTests(); }
|
||
window.scrollTo(0, 0);
|
||
}
|
||
|
||
function toggleMobileMenu() {
|
||
const menu = document.getElementById('mobileMenu');
|
||
menu.classList.toggle('show');
|
||
}
|
||
|
||
function updateAdminLinks() {
|
||
const isAdmin = currentUser === 'admin';
|
||
document.querySelectorAll('.admin-only').forEach(el => {
|
||
el.style.display = isAdmin ? 'block' : 'none';
|
||
});
|
||
}
|
||
|
||
// RENDER TOPICS
|
||
async function renderTopics() {
|
||
const grid = document.getElementById('topicsGrid');
|
||
if (!grid) return;
|
||
|
||
try {
|
||
const res = await fetch('/api/tests');
|
||
const tests = await res.json();
|
||
|
||
const topicCounts = {};
|
||
tests.forEach(t => {
|
||
topicCounts[t.topicId] = (topicCounts[t.topicId] || 0) + 1;
|
||
});
|
||
|
||
grid.innerHTML = TOPICS.map(t => {
|
||
const count = topicCounts[t.id] || 0;
|
||
const hasTests = count > 0;
|
||
return `
|
||
<div class="topic-card" style="${hasTests ? '' : 'opacity:0.6;cursor:not-allowed'}"
|
||
onclick="${hasTests ? 'showTopicTests(' + t.id + ')' : ''}">
|
||
<h3>${t.title}</h3>
|
||
<p>${t.desc}</p>
|
||
<div style="margin-top:16px">
|
||
<span class="badge" style="background:${hasTests ? 'var(--cyan-50)' : '#e5e7eb'}">
|
||
${count} тест${hasTests ? '' : ' (жоқ)'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
`}).join('');
|
||
} catch (e) {
|
||
grid.innerHTML = TOPICS.map(t => `
|
||
<div class="topic-card">
|
||
<h3>${t.title}</h3>
|
||
<p>${t.desc}</p>
|
||
<div style="margin-top:16px"><span class="badge">0 тест</span></div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
}
|
||
|
||
async function showTopicTests(topicId) {
|
||
try {
|
||
const res = await fetch('/api/tests');
|
||
const tests = await res.json();
|
||
const topicTests = tests.filter(t => t.topicId === topicId);
|
||
|
||
if (topicTests.length === 0) {
|
||
alert('📭 Бұл тақырыпта әлі тест жоқ.\n\nАдминге хабарлаңыз немесе өзіңіз админ ретінде кіріп, тест жүктеңіз.');
|
||
return;
|
||
}
|
||
|
||
if (topicTests.length === 1) {
|
||
startServerTest(topicTests[0].id);
|
||
return;
|
||
}
|
||
|
||
const choice = prompt('Тақырыпта ' + topicTests.length + ' тест бар:\n\n' +
|
||
topicTests.map((t, i) => (i+1) + ') ' + t.title).join('\n') + '\n\nТест нөмірін енгізіңіз (1-' + topicTests.length + '):');
|
||
if (choice) {
|
||
const idx = parseInt(choice) - 1;
|
||
if (idx >= 0 && idx < topicTests.length) {
|
||
startServerTest(topicTests[idx].id);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
alert('Қате: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// RENDER RECENT TESTS
|
||
async function renderRecentTests() {
|
||
const grid = document.getElementById('recentTests');
|
||
if (!grid) return;
|
||
|
||
try {
|
||
const res = await fetch('/api/tests');
|
||
const tests = await res.json();
|
||
|
||
if (tests.length === 0) {
|
||
grid.innerHTML = `
|
||
<div class="card" style="grid-column:1/-1;text-align:center;padding:40px">
|
||
<div style="font-size:48px;margin-bottom:16px">📭</div>
|
||
<h3 style="margin-bottom:8px">Тест жоқ</h3>
|
||
<p style="color:var(--gray-500);margin-bottom:16px">Админ панельден тест файлы жүктеңіз</p>
|
||
<button class="btn" onclick="showPage('login')">Админ кіру</button>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
grid.innerHTML = tests.slice(0, 3).map(t => `
|
||
<div class="card" onclick="startServerTest(${t.id})" style="cursor:pointer">
|
||
<h4 style="margin-bottom:8px">${t.title}</h4>
|
||
<p style="color:var(--gray-500);font-size:14px">${t.questions.length} сұрақ</p>
|
||
</div>
|
||
`).join('');
|
||
} catch (e) {
|
||
grid.innerHTML = MOCK_TESTS.slice(0, 3).map(t => `
|
||
<div class="card" onclick="startTestById(${t.id})" style="cursor:pointer">
|
||
<h4 style="margin-bottom:8px">${t.title}</h4>
|
||
<p style="color:var(--gray-500);font-size:14px">${t.questions} сұрақ • ${t.time}</p>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
}
|
||
|
||
// RENDER VIDEOS
|
||
async function renderVideos() {
|
||
const grid = document.getElementById('videosGrid');
|
||
const noVideos = document.getElementById('noVideos');
|
||
if (!grid) return;
|
||
|
||
try {
|
||
const res = await fetch('/api/videos');
|
||
const videos = await res.json();
|
||
|
||
if (videos.length === 0) {
|
||
grid.innerHTML = '';
|
||
if (noVideos) noVideos.style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
if (noVideos) noVideos.style.display = 'none';
|
||
|
||
grid.innerHTML = videos.map(v => `
|
||
<div class="video-card">
|
||
<video controls style="width:100%;height:200px;background:#000">
|
||
<source src="/api/video/${v.filename}" type="video/mp4">
|
||
</video>
|
||
<div class="video-info">
|
||
<h4>${v.title}</h4>
|
||
<p>${v.topic}</p>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
} catch (e) {
|
||
if (noVideos) noVideos.style.display = 'block';
|
||
grid.innerHTML = '';
|
||
}
|
||
}
|
||
|
||
// TEST FUNCTIONS
|
||
function startTest(topicId) {
|
||
const topic = TOPICS.find(t => t.id === topicId);
|
||
currentTest = {
|
||
id: topicId,
|
||
title: topic.title,
|
||
questions: SAMPLE_QUESTIONS.map(q => ({...q}))
|
||
};
|
||
startTestFlow();
|
||
}
|
||
|
||
function startTestById(testId) {
|
||
const test = MOCK_TESTS.find(t => t.id === testId);
|
||
currentTest = {
|
||
id: test.id,
|
||
title: test.title,
|
||
questions: SAMPLE_QUESTIONS.map(q => ({...q}))
|
||
};
|
||
startTestFlow();
|
||
}
|
||
|
||
async function startServerTest(testId) {
|
||
try {
|
||
const res = await fetch('/api/tests');
|
||
const tests = await res.json();
|
||
const test = tests.find(t => t.id === testId);
|
||
if (test) {
|
||
currentTest = { id: test.id, title: test.title, questions: test.questions };
|
||
startTestFlow();
|
||
}
|
||
} catch (e) {
|
||
alert('Тест жүктелмеді: ' + e.message);
|
||
}
|
||
}
|
||
|
||
function startTestFlow() {
|
||
currentQuestionIndex = 0;
|
||
userAnswers = new Array(currentTest.questions.length).fill(null);
|
||
testStartTime = Date.now();
|
||
showPage('test');
|
||
document.getElementById('testTitle').textContent = currentTest.title;
|
||
renderQuestion();
|
||
}
|
||
|
||
function renderQuestion() {
|
||
const q = currentTest.questions[currentQuestionIndex];
|
||
const total = currentTest.questions.length;
|
||
const percent = ((currentQuestionIndex + 1) / total) * 100;
|
||
|
||
document.getElementById('questionNum').textContent = `${currentQuestionIndex + 1}/${total}`;
|
||
document.getElementById('testProgress').style.width = percent + '%';
|
||
|
||
const content = document.getElementById('testContent');
|
||
content.innerHTML = `
|
||
<div class="test-question">
|
||
<h4>${q.q}</h4>
|
||
${q.options.map((opt, i) => `
|
||
<div class="test-option ${userAnswers[currentQuestionIndex] === i ? 'selected' : ''}"
|
||
onclick="selectAnswer(${i})">${opt}</div>
|
||
`).join('')}
|
||
</div>
|
||
`;
|
||
|
||
document.getElementById('prevBtn').style.display = currentQuestionIndex === 0 ? 'none' : 'block';
|
||
document.getElementById('nextBtn').style.display = currentQuestionIndex === total - 1 ? 'none' : 'block';
|
||
document.getElementById('finishBtn').style.display = currentQuestionIndex === total - 1 ? 'block' : 'none';
|
||
}
|
||
|
||
function selectAnswer(index) {
|
||
userAnswers[currentQuestionIndex] = index;
|
||
renderQuestion();
|
||
}
|
||
|
||
function nextQuestion() {
|
||
if (currentQuestionIndex < currentTest.questions.length - 1) {
|
||
currentQuestionIndex++;
|
||
renderQuestion();
|
||
}
|
||
}
|
||
|
||
function prevQuestion() {
|
||
if (currentQuestionIndex > 0) {
|
||
currentQuestionIndex--;
|
||
renderQuestion();
|
||
}
|
||
}
|
||
|
||
async function finishTest() {
|
||
const correct = userAnswers.filter((a, i) => a === currentTest.questions[i].correct).length;
|
||
const total = currentTest.questions.length;
|
||
const percent = Math.round((correct / total) * 100);
|
||
const timeSpent = Math.floor((Date.now() - testStartTime) / 1000);
|
||
|
||
// Save result to server
|
||
if (currentUser) {
|
||
try {
|
||
await fetch('/api/result', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
username: currentUser,
|
||
test: currentTest.title,
|
||
correct: correct,
|
||
total: total,
|
||
percent: percent,
|
||
time: timeSpent,
|
||
date: new Date().toISOString()
|
||
})
|
||
});
|
||
} catch (e) {
|
||
console.error('Failed to save result:', e);
|
||
}
|
||
}
|
||
|
||
// Show results
|
||
const content = document.getElementById('testContent');
|
||
content.innerHTML = `
|
||
<div class="card" style="text-align:center;padding:40px">
|
||
<div style="font-size:64px;margin-bottom:16px">${percent >= 60 ? '🎉' : '📚'}</div>
|
||
<h3 style="margin-bottom:8px">Тест Аяқталды!</h3>
|
||
<p style="font-size:32px;font-weight:800;color:var(--cyan);margin-bottom:16px">${percent}%</p>
|
||
<p style="color:var(--gray-500);margin-bottom:24px">${correct}/${total} дұрыс жауап</p>
|
||
<button class="btn" onclick="showPage('dashboard')">Дашбордқа</button>
|
||
</div>
|
||
`;
|
||
document.getElementById('finishBtn').style.display = 'none';
|
||
document.getElementById('nextBtn').style.display = 'none';
|
||
}
|
||
|
||
// DASHBOARD
|
||
async function updateDashboard() {
|
||
if (!currentUser) {
|
||
document.getElementById('dashTests').textContent = '0';
|
||
document.getElementById('dashPercent').textContent = '0%';
|
||
document.getElementById('dashTime').textContent = '0с';
|
||
return;
|
||
}
|
||
|
||
let userData = { tests: [], totalTime: 0 };
|
||
try {
|
||
const res = await fetch('/api/user/' + encodeURIComponent(currentUser));
|
||
userData = await res.json();
|
||
} catch (e) {
|
||
userData = getUserData() || { tests: [], totalTime: 0 };
|
||
}
|
||
|
||
const testsCount = userData.tests?.length || 0;
|
||
const totalTime = userData.totalTime || 0;
|
||
|
||
let avgPercent = 0;
|
||
if (userData.tests && userData.tests.length > 0) {
|
||
avgPercent = Math.round(userData.tests.reduce((sum, r) => sum + (r.percent || 0), 0) / userData.tests.length);
|
||
}
|
||
|
||
document.getElementById('dashTests').textContent = testsCount;
|
||
document.getElementById('dashPercent').textContent = avgPercent + '%';
|
||
document.getElementById('dashTime').textContent = formatTime(totalTime);
|
||
|
||
const resultsList = document.getElementById('resultsList');
|
||
if (userData.tests && userData.tests.length > 0) {
|
||
resultsList.innerHTML = userData.tests.slice(-5).reverse().map(r => `
|
||
<div class="card" style="margin-bottom:12px;padding:16px">
|
||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||
<div>
|
||
<h4 style="margin-bottom:4px">${r.test}</h4>
|
||
<p style="color:var(--gray-500);font-size:14px">${new Date(r.date).toLocaleDateString()}</p>
|
||
</div>
|
||
<div style="text-align:right">
|
||
<div style="font-size:24px;font-weight:800;color:${r.percent >= 60 ? '#22c55e' : '#ef4444'}">${r.percent}%</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
} else {
|
||
resultsList.innerHTML = '<p style="color:var(--gray-500);text-align:center;padding:40px">Әлі тест тапсырылмаған</p>';
|
||
}
|
||
}
|
||
|
||
function formatTime(seconds) {
|
||
if (seconds < 60) return seconds + 'с';
|
||
const m = Math.floor(seconds / 60);
|
||
const s = seconds % 60;
|
||
return m + 'м ' + s + 'с';
|
||
}
|
||
|
||
// ADMIN FUNCTIONS
|
||
function renderStudentsList() {
|
||
if (currentUser !== 'admin') {
|
||
document.getElementById('studentsList').innerHTML = '<p style="color:#ef4444;text-align:center;padding:20px">⛔ Админ емессіз</p>';
|
||
return;
|
||
}
|
||
|
||
const students = getAllStudents();
|
||
const list = document.getElementById('studentsList');
|
||
|
||
const studentPhones = Object.keys(students);
|
||
if (studentPhones.length === 0) {
|
||
list.innerHTML = '<p style="color:var(--gray-500);text-align:center;padding:20px">Әлі ешкім тіркелген жоқ</p>';
|
||
return;
|
||
}
|
||
|
||
list.innerHTML = studentPhones.map(phone => {
|
||
const s = students[phone];
|
||
const isBanned = s.banned === true;
|
||
const testsCount = (s.tests || []).length;
|
||
return `
|
||
<div style="padding:16px;border:1px solid ${isBanned ? '#ef4444' : 'var(--gray-100)'};border-radius:12px;margin-bottom:12px;background:${isBanned ? '#fef2f2' : 'var(--white)'}">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px">
|
||
<div style="flex:1;min-width:200px">
|
||
<div style="font-weight:700;font-size:16px">${s.name || 'Аты жоқ'}</div>
|
||
<div style="color:var(--gray-500);font-size:14px">${phone}</div>
|
||
<div style="font-size:13px;color:var(--gray-500);margin-top:4px">
|
||
Тіркелген: ${new Date(s.registeredAt).toLocaleDateString('ru-RU')} • Тест: ${testsCount}
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;gap:8px;align-items:center">
|
||
${isBanned
|
||
? `<span class="badge" style="background:#ef4444;color:white">Блокталған</span>
|
||
<button class="btn" onclick="unbanStudent('${phone}')" style="background:#22c55e;padding:8px 16px;font-size:14px">Ашу</button>`
|
||
: `<span class="badge" style="background:#22c55e;color:white">Белсенді</span>
|
||
<button class="btn" onclick="banStudent('${phone}')" style="background:#ef4444;padding:8px 16px;font-size:14px">Блоктау</button>`
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
function banStudent(phone) {
|
||
if (!confirm(phone + ' нөмірін блоктау шынымен керек пе?')) return;
|
||
if (banStudent(phone)) {
|
||
alert('Оқушы блокталды');
|
||
renderStudentsList();
|
||
}
|
||
}
|
||
|
||
function unbanStudent(phone) {
|
||
if (unbanStudent(phone)) {
|
||
alert('Оқушының бұғаты алынды');
|
||
renderStudentsList();
|
||
}
|
||
}
|
||
|
||
async function updateAdminList() {
|
||
if (currentUser !== 'admin') {
|
||
document.getElementById('adminTestsList').innerHTML = '<p style="color:#ef4444;text-align:center;padding:20px">⛔ Админ емессіз</p>';
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const res = await fetch('/api/tests');
|
||
const tests = await res.json();
|
||
const list = document.getElementById('adminTestsList');
|
||
|
||
if (tests.length === 0) {
|
||
list.innerHTML = '<p style="color:var(--gray-500);text-align:center;padding:20px">Тест жоқ</p>';
|
||
return;
|
||
}
|
||
|
||
list.innerHTML = tests.map(t => `
|
||
<div style="padding:12px;border-bottom:1px solid var(--gray-100);display:flex;justify-content:space-between">
|
||
<span>${t.title}</span>
|
||
<span class="badge">${t.questions.length} сұрақ</span>
|
||
</div>
|
||
`).join('');
|
||
} catch (e) {
|
||
// Fallback to mock
|
||
}
|
||
}
|
||
|
||
async function handleTestUpload(input) {
|
||
if (currentUser !== ADMIN_USER) {
|
||
alert('⛔ Тек админ жүктей алады');
|
||
return;
|
||
}
|
||
const file = input.files[0];
|
||
if (!file) return;
|
||
|
||
const title = prompt('Тест атауы:', file.name.split('.')[0]);
|
||
if (!title) return;
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
formData.append('title', title);
|
||
formData.append('topicId', '1');
|
||
|
||
document.getElementById('testUploadResult').textContent = '⏳ Жүктелуде...';
|
||
|
||
try {
|
||
const res = await fetch('/api/upload-test', { method: 'POST', body: formData });
|
||
const data = await res.json();
|
||
if (data.ok) {
|
||
document.getElementById('testUploadResult').textContent = `✅ Жүктелді! ${data.count} сұрақ табылды`;
|
||
updateAdminList();
|
||
} else {
|
||
document.getElementById('testUploadResult').textContent = '❌ Қате: ' + data.error;
|
||
}
|
||
} catch (e) {
|
||
document.getElementById('testUploadResult').textContent = '❌ Қате: ' + e.message;
|
||
}
|
||
}
|
||
|
||
async function handleVideoUpload(input) {
|
||
if (currentUser !== ADMIN_USER) {
|
||
alert('⛔ Тек админ жүктей алады');
|
||
return;
|
||
}
|
||
const file = input.files[0];
|
||
if (!file) return;
|
||
|
||
const title = prompt('Видео атауы:', file.name.split('.')[0]);
|
||
if (!title) return;
|
||
|
||
const topic = prompt('Тақырыбы:', 'Жалпы');
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
formData.append('title', title);
|
||
formData.append('topic', topic || 'Жалпы');
|
||
|
||
document.getElementById('videoUploadResult').textContent = '⏳ Жүктелуде...';
|
||
|
||
try {
|
||
const res = await fetch('/api/upload-video', { method: 'POST', body: formData });
|
||
const data = await res.json();
|
||
if (data.ok) {
|
||
document.getElementById('videoUploadResult').textContent = `✅ Жүктелді! ${Math.round(data.video.size/1024)}KB`;
|
||
renderVideos();
|
||
} else {
|
||
document.getElementById('videoUploadResult').textContent = '❌ Қате: ' + data.error;
|
||
}
|
||
} catch (e) {
|
||
document.getElementById('videoUploadResult').textContent = '❌ Қате: ' + e.message;
|
||
}
|
||
}
|
||
|
||
// PHONE MASK
|
||
document.getElementById('regPhone').addEventListener('input', function(e) {
|
||
let x = e.target.value.replace(/\D/g, '').substring(0, 11);
|
||
if (x.startsWith('8')) x = '7' + x.substring(1);
|
||
if (!x.startsWith('7')) x = '7' + x;
|
||
|
||
let formatted = '+' + x;
|
||
if (x.length > 1) formatted = '+' + x.substring(0, 1) + ' (' + x.substring(1, 4);
|
||
if (x.length > 4) formatted = '+' + x.substring(0, 1) + ' (' + x.substring(1, 4) + ') ' + x.substring(4, 7);
|
||
if (x.length > 7) formatted = '+' + x.substring(0, 1) + ' (' + x.substring(1, 4) + ') ' + x.substring(4, 7) + '-' + x.substring(7, 9);
|
||
if (x.length > 9) formatted = '+' + x.substring(0, 1) + ' (' + x.substring(1, 4) + ') ' + x.substring(4, 7) + '-' + x.substring(7, 9) + '-' + x.substring(9, 11);
|
||
|
||
e.target.value = formatted;
|
||
});
|
||
|
||
document.getElementById('loginPhone').addEventListener('input', document.getElementById('regPhone').oninput);
|
||
|
||
// AUTH
|
||
document.getElementById('loginForm').addEventListener('submit', function(e) {
|
||
e.preventDefault();
|
||
const phone = document.getElementById('loginPhone').value.replace(/\D/g, '');
|
||
const password = document.getElementById('loginPassword').value;
|
||
|
||
// Admin check
|
||
if (phone === ADMIN_PHONE && password === ADMIN_PASS) {
|
||
currentUser = 'admin';
|
||
const adminName = ADMIN_NAME;
|
||
document.getElementById('loginLink').textContent = '👑 ' + adminName;
|
||
document.getElementById('mobileLoginLink').textContent = '👑 ' + adminName;
|
||
updateAdminLinks();
|
||
showPage('admin');
|
||
renderStudentsList();
|
||
alert('Қош келдіңіз, ' + adminName + '!');
|
||
return;
|
||
}
|
||
|
||
// Check if banned
|
||
const students = JSON.parse(localStorage.getItem('ubt_students') || '{}');
|
||
if (students[phone] && students[phone].banned) {
|
||
alert('🚫 Сіздің аккаунтыңыз бұғатталған.\nТехникалық қолдауге хабарласыңыз.');
|
||
return;
|
||
}
|
||
|
||
// Student auth
|
||
if (students[phone] && students[phone].password === password) {
|
||
currentUser = phone;
|
||
const name = students[phone].name || phone;
|
||
document.getElementById('loginLink').textContent = '👤 ' + name;
|
||
document.getElementById('mobileLoginLink').textContent = '👤 ' + name;
|
||
updateAdminLinks();
|
||
showPage('home');
|
||
alert('Қош келдіңіз, ' + name + '!');
|
||
} else {
|
||
alert('Нөмір немесе құпиясөз қате');
|
||
}
|
||
});
|
||
|
||
document.getElementById('registerForm').addEventListener('submit', function(e) {
|
||
e.preventDefault();
|
||
const phone = document.getElementById('regPhone').value.replace(/\D/g, '');
|
||
const name = document.getElementById('regName').value;
|
||
const password = document.getElementById('regPassword').value;
|
||
|
||
if (phone.length < 11) {
|
||
alert('Телефон нөмірін дұрыс енгізіңіз (+7...)');
|
||
return;
|
||
}
|
||
|
||
const students = JSON.parse(localStorage.getItem('ubt_students') || '{}');
|
||
if (students[phone]) {
|
||
alert('Бұл нөмір тіркелген!');
|
||
return;
|
||
}
|
||
|
||
students[phone] = {
|
||
phone: phone,
|
||
name: name,
|
||
password: password,
|
||
registeredAt: new Date().toISOString(),
|
||
banned: false
|
||
};
|
||
localStorage.setItem('ubt_students', JSON.stringify(students));
|
||
alert('Тіркелдіңіз! Енді кіріңіз');
|
||
showPage('login');
|
||
});
|
||
|
||
// INIT
|
||
renderTopics();
|
||
renderRecentTests();
|
||
renderVideos();
|
||
updateAdminLinks();
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|