1459 lines
68 KiB
PHP
1459 lines
68 KiB
PHP
<?php
|
||
|
||
namespace App\Views;
|
||
|
||
class StudentPortal
|
||
{
|
||
public static function render(): string
|
||
{
|
||
return <<<'HTML'
|
||
<!DOCTYPE html>
|
||
<html lang="ar" dir="rtl">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>بوابة الطالب — منصة صَقِل التعليمية</title>
|
||
<link rel="icon" type="image/jpeg" href="/assets/images/saqel_logo.jpg">
|
||
<link rel="apple-touch-icon" href="/assets/images/saqel_logo.jpg">
|
||
<!-- Apple SF Pro & Alexandria Arabic Web Fonts -->
|
||
<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=Alexandria:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||
|
||
<!-- Apple-Grade Luxury Cupertino CSS -->
|
||
<style>
|
||
:root {
|
||
--bg-dark: #000000;
|
||
--bg-card: #121214;
|
||
--bg-card-hover: #18181B;
|
||
--bg-input: #09090B;
|
||
--border: rgba(255, 255, 255, 0.08);
|
||
--border-focus: #0071E3;
|
||
--text-primary: #F5F5F7;
|
||
--text-secondary: #A1A1A6;
|
||
--text-muted: #6E6E73;
|
||
--accent-blue: #0071E3;
|
||
--accent-blue-hover: #0077ED;
|
||
--accent-cyan: #00F5D4;
|
||
--accent-gold: #F59E0B;
|
||
--accent-red: #EF4444;
|
||
--accent-green: #10B981;
|
||
--accent-purple: #8B5CF6;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
margin: 0;
|
||
padding: 0;
|
||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro AR", "SF Pro Display", "SF Pro Text", "SF Arabic", "Alexandria", "Segoe UI", Roboto, sans-serif;
|
||
letter-spacing: -0.015em;
|
||
-webkit-font-smoothing: antialiased;
|
||
-moz-osx-font-smoothing: grayscale;
|
||
}
|
||
|
||
body {
|
||
background-color: var(--bg-dark);
|
||
color: var(--text-primary);
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow-x: hidden;
|
||
background-image:
|
||
radial-gradient(circle at 50% 0%, rgba(0, 113, 227, 0.15) 0%, transparent 60%),
|
||
radial-gradient(circle at 90% 40%, rgba(245, 158, 11, 0.05) 0%, transparent 50%),
|
||
radial-gradient(circle at 10% 80%, rgba(0, 245, 212, 0.05) 0%, transparent 50%);
|
||
background-size: 100% 100%;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 0 24px;
|
||
width: 100%;
|
||
}
|
||
|
||
/* Apple Glass Header */
|
||
header {
|
||
border-bottom: 1px solid var(--border);
|
||
background-color: rgba(0, 0, 0, 0.8);
|
||
backdrop-filter: blur(24px) saturate(180%);
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
}
|
||
.header-content {
|
||
height: 68px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
.logo-area {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
text-decoration: none;
|
||
}
|
||
.logo-img {
|
||
width: 38px;
|
||
height: 38px;
|
||
border-radius: 10px;
|
||
object-fit: cover;
|
||
box-shadow: 0 0 16px rgba(0, 113, 227, 0.35);
|
||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||
}
|
||
.logo-area:hover .logo-img { transform: scale(1.06); }
|
||
.brand-title {
|
||
font-size: 19px;
|
||
font-weight: 800;
|
||
color: #FFFFFF;
|
||
letter-spacing: -0.03em;
|
||
}
|
||
.portal-pill {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: var(--accent-cyan);
|
||
background: rgba(0, 245, 212, 0.1);
|
||
border: 1px solid rgba(0, 245, 212, 0.25);
|
||
padding: 3px 10px;
|
||
border-radius: 980px;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.ws-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
padding: 4px 12px;
|
||
border-radius: 980px;
|
||
transition: all 0.3s;
|
||
}
|
||
.ws-online { background: rgba(16, 185, 129, 0.15); color: #34D399; border: 1px solid rgba(16, 185, 129, 0.3); }
|
||
.ws-offline { background: rgba(239, 68, 68, 0.15); color: #F87171; border: 1px solid rgba(239, 68, 68, 0.3); }
|
||
.ws-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; box-shadow: 0 0 6px currentColor; }
|
||
|
||
/* Auth Form Box */
|
||
.auth-card-wrapper {
|
||
max-width: 440px;
|
||
margin: 60px auto 40px;
|
||
width: 100%;
|
||
}
|
||
.auth-header { text-align: center; margin-bottom: 28px; }
|
||
.auth-title { font-size: 26px; font-weight: 800; color: #FFF; margin-bottom: 8px; }
|
||
.auth-subtitle { font-size: 14px; color: var(--text-secondary); }
|
||
.auth-box {
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border);
|
||
border-radius: 28px;
|
||
padding: 36px;
|
||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6);
|
||
}
|
||
|
||
/* Form Controls */
|
||
.form-group { margin-bottom: 20px; }
|
||
.form-label { display: block; font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 8px; }
|
||
.input-text {
|
||
width: 100%;
|
||
padding: 13px 16px;
|
||
background: var(--bg-input);
|
||
border: 1px solid var(--border);
|
||
border-radius: 14px;
|
||
color: #FFF;
|
||
font-size: 14px;
|
||
outline: none;
|
||
transition: all 0.2s;
|
||
}
|
||
.input-text:focus { border-color: var(--accent-blue); box-shadow: 0 0 0 3px rgba(0, 113, 227, 0.25); }
|
||
.phone-input-group { display: flex; gap: 8px; }
|
||
.country-badge {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border: 1px solid var(--border);
|
||
border-radius: 14px;
|
||
padding: 0 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
font-weight: 700;
|
||
font-size: 13px;
|
||
color: var(--text-secondary);
|
||
}
|
||
.btn-primary {
|
||
width: 100%;
|
||
padding: 13px;
|
||
border-radius: 980px;
|
||
border: none;
|
||
background: var(--accent-blue);
|
||
color: #FFF;
|
||
font-size: 14.5px;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||
box-shadow: 0 4px 20px rgba(0, 113, 227, 0.4);
|
||
}
|
||
.btn-primary:hover { background: var(--accent-blue-hover); transform: scale(1.01); }
|
||
.btn-secondary {
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-muted);
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
transition: color 0.2s;
|
||
}
|
||
.btn-secondary:hover { color: #FFF; }
|
||
|
||
/* Dashboard UI */
|
||
.dashboard-hero {
|
||
background: linear-gradient(135deg, rgba(18, 18, 20, 0.95), rgba(24, 24, 27, 0.8));
|
||
border: 1px solid var(--border);
|
||
border-radius: 28px;
|
||
padding: 32px 36px;
|
||
margin: 32px 0 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
flex-wrap: wrap;
|
||
gap: 24px;
|
||
}
|
||
.readiness-box {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
background: rgba(0, 0, 0, 0.6);
|
||
border: 1px solid var(--border);
|
||
border-radius: 20px;
|
||
padding: 16px 24px;
|
||
}
|
||
.gauge-circle {
|
||
width: 58px;
|
||
height: 58px;
|
||
border-radius: 50%;
|
||
background: radial-gradient(circle, rgba(0, 245, 212, 0.2) 0%, transparent 70%);
|
||
border: 3px solid var(--accent-cyan);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 18px;
|
||
font-weight: 900;
|
||
color: #FFFFFF;
|
||
box-shadow: 0 0 16px rgba(0, 245, 212, 0.3);
|
||
}
|
||
|
||
/* Tabs Nav */
|
||
.tabs-nav {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-bottom: 24px;
|
||
border-bottom: 1px solid var(--border);
|
||
padding-bottom: 12px;
|
||
overflow-x: auto;
|
||
}
|
||
.tab-btn {
|
||
background: rgba(255, 255, 255, 0.04);
|
||
border: 1px solid var(--border);
|
||
color: var(--text-secondary);
|
||
font-size: 13.5px;
|
||
font-weight: 700;
|
||
padding: 10px 22px;
|
||
border-radius: 980px;
|
||
cursor: pointer;
|
||
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||
white-space: nowrap;
|
||
}
|
||
.tab-btn:hover { background: rgba(255, 255, 255, 0.08); color: #FFF; }
|
||
.tab-btn.active {
|
||
background: var(--accent-blue);
|
||
color: #FFF;
|
||
border-color: var(--accent-blue);
|
||
box-shadow: 0 2px 12px rgba(0, 113, 227, 0.4);
|
||
}
|
||
|
||
.studio-card {
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border);
|
||
border-radius: 28px;
|
||
padding: 32px;
|
||
margin-bottom: 40px;
|
||
}
|
||
|
||
/* Socratic Interactive Video Player */
|
||
.video-container {
|
||
position: relative;
|
||
background: #000000;
|
||
border: 1px solid var(--border);
|
||
border-radius: 20px;
|
||
overflow: hidden;
|
||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.8);
|
||
}
|
||
.video-element {
|
||
width: 100%;
|
||
height: 480px;
|
||
object-fit: cover;
|
||
display: block;
|
||
background: #050507;
|
||
}
|
||
.player-bar {
|
||
background: rgba(18, 18, 20, 0.95);
|
||
border-top: 1px solid var(--border);
|
||
padding: 14px 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
}
|
||
.time-badge {
|
||
font-family: monospace;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
color: var(--accent-cyan);
|
||
background: rgba(0, 245, 212, 0.1);
|
||
padding: 4px 10px;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
/* Socratic Checkpoint Modal Overlay */
|
||
.socratic-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.88);
|
||
backdrop-filter: blur(20px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 32px;
|
||
z-index: 10;
|
||
text-align: center;
|
||
animation: fadeIn 0.3s ease;
|
||
}
|
||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||
|
||
.quiz-option-btn {
|
||
width: 100%;
|
||
max-width: 580px;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||
color: #FFF;
|
||
padding: 14px 20px;
|
||
border-radius: 14px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
margin-bottom: 10px;
|
||
cursor: pointer;
|
||
text-align: right;
|
||
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||
}
|
||
.quiz-option-btn:hover {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-color: var(--accent-blue);
|
||
transform: translateX(-4px);
|
||
}
|
||
.quiz-option-btn.correct {
|
||
background: rgba(16, 185, 129, 0.25) !important;
|
||
border-color: #10B981 !important;
|
||
color: #34D399 !important;
|
||
}
|
||
.quiz-option-btn.wrong {
|
||
background: rgba(239, 68, 68, 0.25) !important;
|
||
border-color: #EF4444 !important;
|
||
color: #F87171 !important;
|
||
}
|
||
|
||
/* Real-time Chat Box */
|
||
.student-chat-box {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 480px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 20px;
|
||
overflow: hidden;
|
||
background: #09090B;
|
||
}
|
||
.chat-messages {
|
||
flex: 1;
|
||
padding: 20px;
|
||
overflow-y: auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
.chat-msg {
|
||
max-width: 75%;
|
||
padding: 12px 18px;
|
||
border-radius: 18px;
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
}
|
||
.chat-msg.student {
|
||
align-self: flex-start;
|
||
background: var(--accent-blue);
|
||
color: #FFF;
|
||
border-bottom-right-radius: 4px;
|
||
}
|
||
.chat-msg.teacher {
|
||
align-self: flex-end;
|
||
background: #18181B;
|
||
border: 1px solid var(--border);
|
||
color: #FFF;
|
||
border-bottom-left-radius: 4px;
|
||
}
|
||
.chat-time { display: block; font-size: 11px; opacity: 0.6; margin-top: 4px; }
|
||
.chat-input-bar {
|
||
padding: 14px;
|
||
background: #121214;
|
||
border-top: 1px solid var(--border);
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
|
||
/* Exam Taking Modal & Diagnostic Report */
|
||
.exam-modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.85);
|
||
backdrop-filter: blur(20px);
|
||
z-index: 200;
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20px;
|
||
}
|
||
.exam-card {
|
||
background: #121214;
|
||
border: 1px solid var(--border);
|
||
border-radius: 28px;
|
||
max-width: 680px;
|
||
width: 100%;
|
||
padding: 36px;
|
||
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.8);
|
||
position: relative;
|
||
}
|
||
|
||
/* DRM Watermark */
|
||
@keyframes bounce-watermark {
|
||
0%, 100% { transform: translate(10px, 10px); }
|
||
50% { transform: translate(140px, 80px); }
|
||
}
|
||
.watermark-anim {
|
||
position: fixed; bottom: 20px; left: 20px; z-index: 99; pointer-events: none; opacity: 0.35;
|
||
animation: bounce-watermark 12s infinite ease-in-out; background: rgba(0, 0, 0, 0.75);
|
||
border: 1px solid rgba(255, 255, 255, 0.2); padding: 4px 10px; border-radius: 6px;
|
||
font-family: monospace; font-size: 11px; color: var(--accent-cyan);
|
||
}
|
||
|
||
.spinner {
|
||
width: 18px; height: 18px; border: 3px solid rgba(255, 255, 255, 0.3);
|
||
border-top-color: #FFF; border-radius: 50%; animation: spin 0.8s linear infinite;
|
||
}
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
|
||
footer {
|
||
border-top: 1px solid var(--border);
|
||
padding: 24px 0;
|
||
background: #000000;
|
||
color: var(--text-muted);
|
||
font-size: 12px;
|
||
margin-top: auto;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- Header -->
|
||
<header>
|
||
<div class="container">
|
||
<div class="header-content">
|
||
<a href="/student" class="logo-area">
|
||
<img src="/assets/images/saqel_logo.jpg" alt="صَقِل" class="logo-img">
|
||
<div>
|
||
<span class="brand-title">صَقِل</span>
|
||
<span class="portal-pill">بوابة الطالب الذكية</span>
|
||
</div>
|
||
</a>
|
||
|
||
<div id="header_user_actions">
|
||
<div id="auth_user_badge" style="display: none; align-items: center; gap: 12px;">
|
||
<span id="ws_status_badge" class="ws-badge ws-offline">
|
||
<span class="ws-dot"></span>
|
||
<span id="ws_status_text">Workerman جاري الاتصال...</span>
|
||
</span>
|
||
<span style="font-size: 13px; color: var(--text-secondary);" id="student_display_name"></span>
|
||
<button onclick="handleLogout()" style="background: none; border: 1px solid rgba(239,68,68,0.4); color: #F87171; border-radius: 980px; padding: 6px 14px; font-size: 12px; cursor: pointer;">
|
||
تسجيل الخروج
|
||
</button>
|
||
</div>
|
||
<a href="/teacher" class="nav-btn" id="switch_to_teacher_link" style="color: var(--text-secondary); font-size: 13px; text-decoration: none;">
|
||
<span>أنت معلم؟</span>
|
||
<span style="color: var(--accent-gold); font-weight: 700; margin-right: 4px;">استوديو المعلمين ←</span>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Main Content -->
|
||
<main>
|
||
<div class="container">
|
||
|
||
<!-- 1. AUTHENTICATION (PHONE & OTP) -->
|
||
<div id="auth_container" class="auth-card-wrapper">
|
||
<div class="auth-header">
|
||
<h1 class="auth-title">ادخل لعالم التمكين والفهم</h1>
|
||
<p class="auth-subtitle">تسجيل دخول آمن وسريع عبر رمز الواتساب المعتمد</p>
|
||
</div>
|
||
|
||
<div class="auth-box">
|
||
<div id="alert_box_error" class="alert alert-error" style="display: none; color: #F87171; background: rgba(239,68,68,0.15); border: 1px solid rgba(239,68,68,0.3); padding: 12px; border-radius: 12px; font-size: 13px; margin-bottom: 16px;">
|
||
<span>⚠️ </span><span id="alert_error_msg"></span>
|
||
</div>
|
||
<div id="alert_box_success" class="alert alert-success" style="display: none; color: #34D399; background: rgba(16,185,129,0.15); border: 1px solid rgba(16,185,129,0.3); padding: 12px; border-radius: 12px; font-size: 13px; margin-bottom: 16px;">
|
||
<span>✓ </span><span id="alert_success_msg"></span>
|
||
</div>
|
||
|
||
<!-- Step 1: Phone -->
|
||
<div id="step_phone_container">
|
||
<div class="form-group">
|
||
<label class="form-label">الاسم الكامل للطالب</label>
|
||
<input type="text" id="student_fullname" placeholder="مثال: أحمد محمد الغويري" class="input-text">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">رقم الهاتف (الواتساب) <span style="color: #EF4444;">*</span></label>
|
||
<div class="phone-input-group">
|
||
<div class="country-badge">🇯🇴 +962</div>
|
||
<input type="tel" id="student_phone" required placeholder="790000000" class="input-text">
|
||
</div>
|
||
</div>
|
||
|
||
<button type="button" id="btn_send_otp" onclick="handleSendOtp(event)" class="btn-primary">
|
||
<span id="btn_send_text">إرسال رمز التحقق (OTP) ←</span>
|
||
<div id="btn_send_spinner" class="spinner" style="display: none;"></div>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Step 2: OTP Code -->
|
||
<div id="step_otp_container" style="display: none;">
|
||
<div style="text-align: center; margin-bottom: 20px;">
|
||
<span style="font-size: 12px; color: var(--text-muted);">تم إرسال رمز التحقق إلى:</span>
|
||
<div style="font-family: monospace; font-size: 15px; font-weight: 800; color: var(--accent-cyan); margin-top: 4px;" id="otp_target_display"></div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label" style="text-align: center;">أدخل رمز التحقق (6 أرقام)</label>
|
||
<input type="text" id="student_otp_code" maxlength="6" autofocus placeholder="• • • • • •" class="input-text" style="text-align: center; font-size: 22px; letter-spacing: 6px;">
|
||
</div>
|
||
|
||
<button type="button" id="btn_verify_otp" onclick="handleVerifyOtp(event)" class="btn-primary">
|
||
<span id="btn_verify_text">تأكيد الدخول للمنصة ✨</span>
|
||
<div id="btn_verify_spinner" class="spinner" style="display: none;"></div>
|
||
</button>
|
||
|
||
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 18px;">
|
||
<button type="button" onclick="switchToPhoneStep()" class="btn-secondary">← تعديل الرقم</button>
|
||
<span id="resend_timer_label" style="font-size: 12px; color: var(--text-muted); display: none;"></span>
|
||
<button type="button" id="btn_resend" onclick="handleSendOtp(event)" class="btn-secondary" style="display: none; color: var(--accent-cyan);">إعادة إرسال الرمز ↺</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 2. STUDENT DASHBOARD (AUTHENTICATED) -->
|
||
<div id="dashboard_container" style="width: 100%; display: none;">
|
||
|
||
<!-- Hero & Tawjihi Readiness Score -->
|
||
<div class="dashboard-hero">
|
||
<div>
|
||
<span style="display: inline-block; font-size: 11px; font-weight: 800; color: var(--accent-cyan); background: rgba(0,245,212,0.12); border: 1px solid rgba(0,245,212,0.3); padding: 4px 12px; border-radius: 980px; margin-bottom: 10px;">توجيهي 2008 — الفرع العلمي</span>
|
||
<h2 style="font-size: 26px; font-weight: 800; color: #FFFFFF;" id="dashboard_welcome_title">أهلاً بك يا بطلنا 🚀</h2>
|
||
<p style="font-size: 13px; color: var(--text-secondary); margin-top: 4px;">صقل الفهم، حل الأسئلة الوزارية، والتواصل الحي مع أستاذك.</p>
|
||
</div>
|
||
|
||
<div class="readiness-box">
|
||
<div class="gauge-circle" id="readiness_gauge_val">87.5%</div>
|
||
<div>
|
||
<span style="font-size: 11px; font-weight: 700; color: var(--text-muted); display: block;">مؤشر الجاهزية للوزاري</span>
|
||
<span style="font-size: 12px; font-weight: 800; color: #34D399;" id="readiness_status_text">مستواك ممتاز ومستقر (+2.4% هذا الأسبوع)</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tabs Navigation -->
|
||
<div class="tabs-nav">
|
||
<button class="tab-btn active" id="tab_btn_lessons" onclick="switchStudentTab('lessons')">🎬 الحصص ومشغل الفيديو السقراطي</button>
|
||
<button class="tab-btn" id="tab_btn_chat" onclick="switchStudentTab('chat')">💬 اسأل الأستاذ حمزة الغويري (Workerman 0ms)</button>
|
||
<button class="tab-btn" id="tab_btn_exams" onclick="switchStudentTab('exams')">📝 الامتحانات وهرمية التقييم المعرفي</button>
|
||
</div>
|
||
|
||
<!-- TAB 1: Socratic Interactive Lesson Player -->
|
||
<div id="tab_lessons_content" class="studio-card">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 18px; flex-wrap: wrap; gap: 10px;">
|
||
<div>
|
||
<h3 style="font-size: 18px; font-weight: 800;">الدرس 4: قواعد الاشتقاق الأساسية — رياضيات علمي</h3>
|
||
<span style="font-size: 12px; color: var(--text-muted);">الأستاذ حمزة الغويري • Bunny Stream DRM Protection</span>
|
||
</div>
|
||
<span style="font-size: 11px; font-weight: 800; color: var(--accent-cyan); background: rgba(0,245,212,0.1); border: 1px solid rgba(0,245,212,0.3); padding: 5px 14px; border-radius: 980px;">
|
||
Checkpoint Active (00:15)
|
||
</span>
|
||
</div>
|
||
|
||
<div class="video-container">
|
||
<video id="lesson_video_player" class="video-element" controls poster="/assets/images/saqel_logo.jpg">
|
||
<source src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" type="video/mp4">
|
||
متصفحك لا يدعم مشغل الفيديو.
|
||
</video>
|
||
|
||
<!-- Socratic Checkpoint Overlay Modal -->
|
||
<div id="socratic_quiz_modal" class="socratic-overlay" style="display: none;">
|
||
<span style="font-size: 11px; font-weight: 800; color: var(--accent-gold); background: rgba(245, 158, 11, 0.15); border: 1px solid rgba(245, 158, 11, 0.4); padding: 4px 14px; border-radius: 980px; margin-bottom: 14px;">
|
||
⚠️ توقف الشرح لفحص الفهم اللحظي (Socratic Active Recall)
|
||
</span>
|
||
<h3 style="font-size: 18px; font-weight: 800; color: #FFFFFF; margin-bottom: 18px; max-width: 580px;">
|
||
إذا كان f(x) = sin(3x)، فما هي مشتقة الاقتران f'(x)؟
|
||
</h3>
|
||
|
||
<div style="width: 100%; display: flex; flex-direction: column; align-items: center;">
|
||
<button type="button" class="quiz-option-btn" onclick="handleCheckpointAnswer(this, true)">
|
||
أ) 3 cos(3x) (مشتقة الزاوية ضرب مشتقة الاقتران)
|
||
</button>
|
||
<button type="button" class="quiz-option-btn" onclick="handleCheckpointAnswer(this, false)">
|
||
ب) cos(3x)
|
||
</button>
|
||
<button type="button" class="quiz-option-btn" onclick="handleCheckpointAnswer(this, false)">
|
||
ج) -3 cos(3x)
|
||
</button>
|
||
</div>
|
||
|
||
<div id="checkpoint_feedback" style="margin-top: 14px; font-size: 13.5px; font-weight: 700; display: none;"></div>
|
||
</div>
|
||
|
||
<div class="player-bar">
|
||
<div style="display: flex; align-items: center; gap: 12px;">
|
||
<span class="time-badge" id="video_time_display">00:00 / 09:56</span>
|
||
<span style="font-size: 12px; color: var(--text-muted);">نقطة التثبيت المعرفي التالية عند: 00:15</span>
|
||
</div>
|
||
<button type="button" onclick="triggerCheckpointDemo()" style="background: rgba(245,158,11,0.15); border: 1px solid rgba(245,158,11,0.3); color: var(--accent-gold); border-radius: 980px; padding: 5px 14px; font-size: 11.5px; cursor: pointer; font-weight: 700;">
|
||
محاكاة نقطة الكويز اللحظي (00:15) ⏱️
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB 2: Real-time Chat with Teacher (Workerman WebSocket) -->
|
||
<div id="tab_chat_content" class="studio-card" style="display: none;">
|
||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px;">
|
||
<div>
|
||
<h3 style="font-size: 18px; font-weight: 800;">محادثة معلم المادة المباشرة 💬</h3>
|
||
<p style="font-size: 12px; color: var(--text-muted);">اسأل الأستاذ حمزة الغويري عن أي فكرة في الدرس وتلقى الإجابة فوراً.</p>
|
||
</div>
|
||
<span style="font-size: 11px; font-weight: 800; background: rgba(16,185,129,0.15); border: 1px solid rgba(16,185,129,0.4); color: #34D399; padding: 4px 12px; border-radius: 980px;">
|
||
الأستاذ حمزة الغويري (متصل الآن 🟢)
|
||
</span>
|
||
</div>
|
||
|
||
<div class="student-chat-box">
|
||
<div class="chat-messages" id="student_chat_messages">
|
||
<div class="chat-msg teacher">
|
||
أهلاً بك يا بطل! إذا واجهت أي خطوة غير واضحة في درس قواعد الاشتقاق، اكتب لي سؤالك هنا وسأجيبك فوراً.
|
||
<span class="chat-time">قبل قليل</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="chat-input-bar">
|
||
<input type="text" id="student_chat_input" placeholder="اكتب استفسارك للأستاذ حمزة..." class="input-text" onkeypress="if(event.key==='Enter') sendStudentMessage()">
|
||
<button type="button" onclick="sendStudentMessage()" class="btn-primary" style="width: auto; padding: 0 26px; font-size: 13px;">إرسال ↵</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB 3: Multi-Level Exams & AI Diagnostics -->
|
||
<div id="tab_exams_content" class="studio-card" style="display: none;">
|
||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px;">
|
||
<div>
|
||
<h3 style="font-size: 18px; font-weight: 800;">هرمية الامتحانات والتشخيص الذكي 📝</h3>
|
||
<p style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">4 مستويات من الامتحانات لقياس الفهم وسد الثغرات قبل الامتحان الوزاري.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 18px;">
|
||
<!-- Level 1 -->
|
||
<div style="background: rgba(0,0,0,0.5); border: 1px solid var(--border); border-radius: 20px; padding: 24px;">
|
||
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
||
<span style="font-size: 11px; font-weight: 800; color: var(--accent-cyan); background: rgba(0,245,212,0.1); padding: 2px 8px; border-radius: 6px;">المستوى 1: اختبار الدرس</span>
|
||
<span style="font-size: 11px; color: var(--text-muted);">4 أسئلة • 10 دقائق</span>
|
||
</div>
|
||
<h4 style="font-size: 16px; font-weight: 800;">امتحان قواعد الاشتقاق</h4>
|
||
<p style="font-size: 12px; color: var(--text-muted); margin: 6px 0 16px 0;">فحص المهارات الرياضية المباشرة بعد الدرس.</p>
|
||
<button type="button" onclick="startExamModal('امتحان قواعد الاشتقاق', 1)" class="btn-primary" style="padding: 10px; font-size: 13px;">بدء الامتحان الآن ⏱️</button>
|
||
</div>
|
||
|
||
<!-- Level 2 -->
|
||
<div style="background: rgba(0,0,0,0.5); border: 1px solid var(--border); border-radius: 20px; padding: 24px;">
|
||
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
||
<span style="font-size: 11px; font-weight: 800; color: var(--accent-purple); background: rgba(139,92,246,0.1); padding: 2px 8px; border-radius: 6px;">المستوى 2: امتحان الوحدة</span>
|
||
<span style="font-size: 11px; color: var(--text-muted);">15 سؤالاً • 30 دقيقة</span>
|
||
</div>
|
||
<h4 style="font-size: 16px; font-weight: 800;">امتحان وحدة التفاضل الشامل</h4>
|
||
<p style="font-size: 12px; color: var(--text-muted); margin: 6px 0 16px 0;">محاكاة معيارية لكافة أنماط الأسئلة الوزارية للوحدة.</p>
|
||
<button type="button" onclick="startExamModal('امتحان وحدة التفاضل الشامل', 2)" class="btn-primary" style="padding: 10px; font-size: 13px; background: var(--accent-purple);">بدء امتحان الوحدة 📚</button>
|
||
</div>
|
||
|
||
<!-- Level 3 -->
|
||
<div style="background: rgba(0,0,0,0.5); border: 1px solid rgba(245,158,11,0.4); border-radius: 20px; padding: 24px;">
|
||
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
||
<span style="font-size: 11px; font-weight: 800; color: var(--accent-gold); background: rgba(245,158,11,0.1); padding: 2px 8px; border-radius: 6px;">المستوى 3: ذكاء اصطناعي</span>
|
||
<span style="font-size: 11px; color: var(--text-muted);">تكيفي مخصص</span>
|
||
</div>
|
||
<h4 style="font-size: 16px; font-weight: 800;">امتحان سد الثغرات التلقائي</h4>
|
||
<p style="font-size: 12px; color: var(--text-muted); margin: 6px 0 16px 0;">يولد أسئلة تكيفية تركز على نقاط ضعفك بدقة.</p>
|
||
<button type="button" onclick="startExamModal('امتحان سد الثغرات التلقائي', 3)" class="btn-primary" style="padding: 10px; font-size: 13px; background: linear-gradient(135deg, #F59E0B, #D97706);">توليد وبدء التقييم 🧠</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Dynamic DRM Watermark -->
|
||
<div class="watermark-anim">
|
||
<span id="drm_watermark_text">SAQEL-DRM • 079XXXXXXX</span>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
</main>
|
||
|
||
<!-- 3. INTERACTIVE EXAM TAKING MODAL -->
|
||
<div id="exam_taking_modal" class="exam-modal">
|
||
<div class="exam-card">
|
||
<!-- Header -->
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||
<div>
|
||
<h3 style="font-size: 18px; font-weight: 800;" id="exam_title_display">امتحان قواعد الاشتقاق</h3>
|
||
<span style="font-size: 12px; color: var(--text-muted);" id="exam_progress_text">السؤال 1 من 3</span>
|
||
</div>
|
||
<div style="background: rgba(239,68,68,0.15); border: 1px solid rgba(239,68,68,0.3); color: #F87171; padding: 4px 14px; border-radius: 980px; font-family: monospace; font-size: 13px; font-weight: 800;" id="exam_timer_badge">
|
||
⏱️ 14:59
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Question Screen -->
|
||
<div id="exam_question_container">
|
||
<div style="background: rgba(255,255,255,0.03); border: 1px solid var(--border); border-radius: 16px; padding: 20px; margin-bottom: 20px;">
|
||
<h4 style="font-size: 16px; font-weight: 700; line-height: 1.6;" id="exam_question_text">
|
||
إذا كان f(x) = (2x + 1)³، فما هي قيمة المشتقة f'(1)؟
|
||
</h4>
|
||
</div>
|
||
|
||
<div id="exam_options_list" style="display: flex; flex-direction: column; gap: 10px; margin-bottom: 24px;">
|
||
<!-- Options injected dynamically -->
|
||
</div>
|
||
|
||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||
<button type="button" onclick="closeExamModal()" class="btn-secondary">إلغاء الخروج</button>
|
||
<button type="button" id="btn_next_question" onclick="handleNextQuestion()" class="btn-primary" style="width: auto; padding: 10px 28px;">
|
||
السؤال التالي ←
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- AI Diagnostic Report (Shown on Submit) -->
|
||
<div id="exam_result_container" style="display: none; text-align: center;">
|
||
<div style="width: 72px; height: 72px; border-radius: 50%; background: rgba(16,185,129,0.15); border: 2px solid #10B981; color: #34D399; font-size: 32px; display: flex; align-items: center; justify-content: center; margin: 0 auto 16px;">
|
||
✓
|
||
</div>
|
||
<h3 style="font-size: 22px; font-weight: 800; margin-bottom: 6px;">تم إنهاء الامتحان وحساب التشخيص الذكي!</h3>
|
||
<p style="font-size: 13px; color: var(--text-muted); margin-bottom: 20px;">تقرير التثبيت المعرفي الصادر من محرك صَقِل للذكاء الاصطناعي</p>
|
||
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 20px;">
|
||
<div style="background: rgba(255,255,255,0.03); border: 1px solid var(--border); border-radius: 16px; padding: 16px;">
|
||
<span style="font-size: 11px; color: var(--text-muted); display: block;">النتيجة النهائية</span>
|
||
<strong style="font-size: 24px; color: #34D399;" id="result_score_display">100%</strong>
|
||
</div>
|
||
<div style="background: rgba(255,255,255,0.03); border: 1px solid var(--border); border-radius: 16px; padding: 16px;">
|
||
<span style="font-size: 11px; color: var(--text-muted); display: block;">أثر مؤشر التوجيهي</span>
|
||
<strong style="font-size: 24px; color: var(--accent-cyan);" id="result_readiness_display">+2.4%</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="background: rgba(245,158,11,0.08); border: 1px solid rgba(245,158,11,0.25); border-radius: 16px; padding: 18px; text-align: right; margin-bottom: 24px;">
|
||
<strong style="font-size: 13px; color: var(--accent-gold); display: block; margin-bottom: 4px;">🔍 التوصية السقراطية الذكية:</strong>
|
||
<p style="font-size: 12.5px; color: var(--text-secondary); line-height: 1.6;" id="result_recommendation_text">
|
||
أظهرت إتقاناً ممتازاً لقاعدة السلسلة. يُوصى بمواصلة الدرس الخامس للحفاظ على نسق الجاهزية الوزارية.
|
||
</p>
|
||
</div>
|
||
|
||
<button type="button" onclick="closeExamModal()" class="btn-primary">
|
||
العودة لدروس المادة ✓
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Footer -->
|
||
<footer>
|
||
<div class="container">
|
||
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px;">
|
||
<span>منصة صَقِل التعليمية — الجيل الثاني للتعليم التفاعلي © 2026</span>
|
||
<span style="color: var(--accent-cyan);">الخادم متصل ومشفر بـ Workerman & WSS 🔒</span>
|
||
</div>
|
||
</div>
|
||
</footer>
|
||
|
||
<!-- Logic Controller with Full Real-time WebSocket Client -->
|
||
<script>
|
||
let wsSocket = null;
|
||
let countdownTimer = null;
|
||
let timerSeconds = 0;
|
||
let studentData = null;
|
||
let checkpointTriggered = false;
|
||
|
||
// Exam State
|
||
let currentExamIndex = 0;
|
||
let userSelectedAnswers = [];
|
||
const sampleQuestions = [
|
||
{
|
||
text: "إذا كان f(x) = (2x + 1)³، فما هي قيمة المشتقة f'(1)؟",
|
||
options: ["54 (3 × 2 × 3²)", "27", "18", "9"],
|
||
correct: 0,
|
||
topic: "قاعدة السلسلة"
|
||
},
|
||
{
|
||
text: "ما هي مشتقة الاقتران f(x) = tan(2x)؟",
|
||
options: ["2 sec²(2x)", "sec²(2x)", "2 tan(2x)", "-2 sec²(2x)"],
|
||
correct: 0,
|
||
topic: "المشتقات المثلثية"
|
||
},
|
||
{
|
||
text: "إذا كان ميل المماس لمنحنى عند النقطة (1, 3) يساوي 4، فما هي معادلة المماس؟",
|
||
options: ["y - 3 = 4(x - 1)", "y - 1 = 4(x - 3)", "y = 4x + 3", "y = 4x - 3"],
|
||
correct: 0,
|
||
topic: "التفسير الهندسي للمشتقة"
|
||
}
|
||
];
|
||
|
||
document.addEventListener('DOMContentLoaded', async () => {
|
||
const token = localStorage.getItem('saqel_student_jwt');
|
||
const cachedUser = localStorage.getItem('saqel_student_user');
|
||
if (token) {
|
||
if (cachedUser) {
|
||
try {
|
||
const u = JSON.parse(cachedUser);
|
||
renderDashboard(u);
|
||
} catch (e) {}
|
||
}
|
||
initWebSocket(token);
|
||
await checkStudentSession(token);
|
||
}
|
||
|
||
// Video player time listener for Socratic checkpoint
|
||
const video = document.getElementById('lesson_video_player');
|
||
if (video) {
|
||
video.addEventListener('timeupdate', () => {
|
||
const cur = Math.floor(video.currentTime);
|
||
const dur = Math.floor(video.duration || 596);
|
||
document.getElementById('video_time_display').textContent = `${formatTime(cur)} / ${formatTime(dur)}`;
|
||
|
||
// Trigger checkpoint at second 15 automatically once
|
||
if (cur === 15 && !checkpointTriggered) {
|
||
checkpointTriggered = true;
|
||
video.pause();
|
||
document.getElementById('socratic_quiz_modal').style.display = 'flex';
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
function formatTime(secs) {
|
||
const m = Math.floor(secs / 60).toString().padStart(2, '0');
|
||
const s = (secs % 60).toString().padStart(2, '0');
|
||
return `${m}:${s}`;
|
||
}
|
||
|
||
let wsPingInterval = null;
|
||
|
||
// 1. Initialize Real-time WebSocket (Workerman)
|
||
function initWebSocket(token) {
|
||
if (!token) return;
|
||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||
const wsUrl = `${wsProtocol}//${window.location.host}/ws`;
|
||
|
||
if (wsSocket && (wsSocket.readyState === WebSocket.OPEN || wsSocket.readyState === WebSocket.CONNECTING)) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
wsSocket = new WebSocket(wsUrl);
|
||
|
||
wsSocket.onopen = () => {
|
||
wsSocket.send(JSON.stringify({
|
||
event: 'auth',
|
||
data: { token: token }
|
||
}));
|
||
|
||
// Heartbeat keep-alive every 20s
|
||
clearInterval(wsPingInterval);
|
||
wsPingInterval = setInterval(() => {
|
||
if (wsSocket && wsSocket.readyState === WebSocket.OPEN) {
|
||
wsSocket.send(JSON.stringify({ event: 'ping' }));
|
||
}
|
||
}, 20000);
|
||
};
|
||
|
||
wsSocket.onmessage = (event) => {
|
||
try {
|
||
const msg = JSON.parse(event.data);
|
||
handleIncomingWsEvent(msg);
|
||
} catch (e) {
|
||
console.error('WS Parse error:', e);
|
||
}
|
||
};
|
||
|
||
wsSocket.onclose = () => {
|
||
updateWsStatus(false);
|
||
clearInterval(wsPingInterval);
|
||
setTimeout(() => {
|
||
const currentToken = localStorage.getItem('saqel_student_jwt');
|
||
if (currentToken) initWebSocket(currentToken);
|
||
}, 5000);
|
||
};
|
||
|
||
wsSocket.onerror = (err) => {
|
||
updateWsStatus(false);
|
||
};
|
||
} catch (e) {
|
||
updateWsStatus(false);
|
||
}
|
||
}
|
||
|
||
function updateWsStatus(isOnline) {
|
||
const badge = document.getElementById('ws_status_badge');
|
||
const text = document.getElementById('ws_status_text');
|
||
if (!badge || !text) return;
|
||
if (isOnline) {
|
||
badge.className = 'ws-badge ws-online';
|
||
text.textContent = 'Workerman متصل 🟢';
|
||
} else {
|
||
badge.className = 'ws-badge ws-offline';
|
||
text.textContent = 'Workerman غير متصل 🔴';
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// Luxury Audio Chime & Real-time Notifications
|
||
// ==========================================
|
||
function playChimeNotification() {
|
||
try {
|
||
const ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||
const now = ctx.currentTime;
|
||
|
||
const osc1 = ctx.createOscillator();
|
||
const gain1 = ctx.createGain();
|
||
osc1.type = 'sine';
|
||
osc1.frequency.setValueAtTime(587.33, now);
|
||
gain1.gain.setValueAtTime(0.2, now);
|
||
gain1.gain.exponentialRampToValueAtTime(0.001, now + 0.35);
|
||
osc1.connect(gain1);
|
||
gain1.connect(ctx.destination);
|
||
osc1.start(now);
|
||
osc1.stop(now + 0.35);
|
||
|
||
const osc2 = ctx.createOscillator();
|
||
const gain2 = ctx.createGain();
|
||
osc2.type = 'sine';
|
||
osc2.frequency.setValueAtTime(880.00, now + 0.09);
|
||
gain2.gain.setValueAtTime(0.25, now + 0.09);
|
||
gain2.gain.exponentialRampToValueAtTime(0.001, now + 0.55);
|
||
osc2.connect(gain2);
|
||
gain2.connect(ctx.destination);
|
||
osc2.start(now + 0.09);
|
||
osc2.stop(now + 0.55);
|
||
} catch (e) {
|
||
console.warn('Audio Context note:', e);
|
||
}
|
||
}
|
||
|
||
function showLuxuryToast(title, body, icon = '👨🏫') {
|
||
let container = document.getElementById('saqel_toast_stack');
|
||
if (!container) {
|
||
container = document.createElement('div');
|
||
container.id = 'saqel_toast_stack';
|
||
container.style.cssText = 'position: fixed; top: 24px; left: 24px; z-index: 99999; display: flex; flex-direction: column; gap: 10px; pointer-events: none; direction: rtl;';
|
||
document.body.appendChild(container);
|
||
}
|
||
|
||
const toast = document.createElement('div');
|
||
toast.style.cssText = `
|
||
pointer-events: auto;
|
||
min-width: 280px;
|
||
max-width: 380px;
|
||
background: rgba(22, 27, 34, 0.92);
|
||
backdrop-filter: blur(24px);
|
||
-webkit-backdrop-filter: blur(24px);
|
||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||
border-right: 4px solid var(--accent-color, #38BDF8);
|
||
border-radius: 16px;
|
||
padding: 14px 18px;
|
||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6), 0 0 25px rgba(56, 189, 248, 0.2);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
animation: toastSlideIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||
cursor: pointer;
|
||
`;
|
||
|
||
toast.innerHTML = `
|
||
<div style="font-size: 24px;">${icon}</div>
|
||
<div style="flex: 1; min-width: 0;">
|
||
<div style="font-size: 13px; font-weight: 700; color: #FFFFFF; margin-bottom: 2px;">${escapeHtml(title)}</div>
|
||
<div style="font-size: 12px; color: var(--text-muted, #94A3B8); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${escapeHtml(body)}</div>
|
||
</div>
|
||
`;
|
||
|
||
container.appendChild(toast);
|
||
|
||
setTimeout(() => {
|
||
toast.style.opacity = '0';
|
||
toast.style.transform = 'translateX(-30px)';
|
||
toast.style.transition = 'all 0.3s ease';
|
||
setTimeout(() => toast.remove(), 300);
|
||
}, 5000);
|
||
}
|
||
|
||
function triggerDesktopNotification(title, body) {
|
||
if ('Notification' in window && Notification.permission === 'granted') {
|
||
try {
|
||
new Notification(title, {
|
||
body: body,
|
||
icon: '/favicon.ico'
|
||
});
|
||
} catch (e) {}
|
||
}
|
||
}
|
||
|
||
function handleIncomingWsEvent(msg) {
|
||
const event = msg.event;
|
||
const data = msg.data;
|
||
|
||
if (event === 'authenticated') {
|
||
updateWsStatus(true);
|
||
} else if (event === 'chat_message') {
|
||
appendChatMessage({ ...data, is_mine: false });
|
||
playChimeNotification();
|
||
showLuxuryToast('رسالة جديدة من الأستاذ حمزة الغويري 👨🏫', data.message || 'رد المعلم على استفسارك');
|
||
triggerDesktopNotification('صَقِل — رد الأستاذ حمزة الغويري', data.message || 'رد المعلم على استفسارك');
|
||
} else if (event === 'drm_session_terminated') {
|
||
alert('⚠️ تم فتح هذا الحساب من متصفح آخر.');
|
||
handleLogout();
|
||
}
|
||
}
|
||
|
||
let activeTeacherId = null;
|
||
|
||
function switchStudentTab(tab) {
|
||
document.getElementById('tab_lessons_content').style.display = (tab === 'lessons') ? 'block' : 'none';
|
||
document.getElementById('tab_chat_content').style.display = (tab === 'chat') ? 'block' : 'none';
|
||
document.getElementById('tab_exams_content').style.display = (tab === 'exams') ? 'block' : 'none';
|
||
|
||
document.getElementById('tab_btn_lessons').className = (tab === 'lessons') ? 'tab-btn active' : 'tab-btn';
|
||
document.getElementById('tab_btn_chat').className = (tab === 'chat') ? 'tab-btn active' : 'tab-btn';
|
||
document.getElementById('tab_btn_exams').className = (tab === 'exams') ? 'tab-btn active' : 'tab-btn';
|
||
|
||
if (tab === 'chat') {
|
||
loadStudentChat();
|
||
}
|
||
}
|
||
|
||
async function loadStudentChat() {
|
||
const token = localStorage.getItem('saqel_student_jwt');
|
||
if (!token) return;
|
||
try {
|
||
const res = await fetch('/api/chat/conversations', {
|
||
headers: { 'Authorization': 'Bearer ' + token }
|
||
});
|
||
const data = await res.json();
|
||
if (res.ok && data.data && data.data.length > 0) {
|
||
const teacher = data.data.find(c => c.role === 'teacher') || data.data[0];
|
||
if (teacher) {
|
||
activeTeacherId = teacher.user_id;
|
||
const badge = document.getElementById('chat_teacher_badge_name');
|
||
if (badge) {
|
||
badge.textContent = `${teacher.full_name} (${teacher.specialization || 'مدرس المادة'}) (متصل الآن 🟢)`;
|
||
}
|
||
loadStudentMessages(activeTeacherId);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('Load student chat notice:', e);
|
||
}
|
||
}
|
||
|
||
async function loadStudentMessages(teacherId) {
|
||
const token = localStorage.getItem('saqel_student_jwt');
|
||
if (!token || !teacherId) return;
|
||
try {
|
||
const res = await fetch(`/api/chat/messages?other_user_id=${teacherId}`, {
|
||
headers: { 'Authorization': 'Bearer ' + token }
|
||
});
|
||
const data = await res.json();
|
||
if (res.ok && data.data?.messages) {
|
||
const messagesArea = document.getElementById('student_chat_messages');
|
||
messagesArea.innerHTML = '';
|
||
if (data.data.messages.length === 0) {
|
||
messagesArea.innerHTML = '<div style="text-align: center; color: var(--text-muted); font-size: 13px; margin-top: 40px;">مرحباً بك! اكتب سؤالك هنا وسيجيبك أستاذ المادة فوراً.</div>';
|
||
} else {
|
||
data.data.messages.forEach(msg => appendChatMessage(msg));
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('Fetch student messages notice:', e);
|
||
}
|
||
}
|
||
|
||
// Real-time Chat Rendering
|
||
function appendChatMessage(msg) {
|
||
const messagesArea = document.getElementById('student_chat_messages');
|
||
const isMine = msg.is_mine || false;
|
||
const div = document.createElement('div');
|
||
div.className = `chat-msg ${isMine ? 'student' : 'teacher'}`;
|
||
div.innerHTML = `
|
||
<div>${msg.message}</div>
|
||
<span class="chat-time">${msg.created_at || 'الآن'}</span>
|
||
`;
|
||
messagesArea.appendChild(div);
|
||
messagesArea.scrollTop = messagesArea.scrollHeight;
|
||
}
|
||
|
||
async function sendStudentMessage() {
|
||
const input = document.getElementById('student_chat_input');
|
||
const message = input.value.trim();
|
||
if (!message) return;
|
||
|
||
input.value = '';
|
||
const token = localStorage.getItem('saqel_student_jwt');
|
||
|
||
// If activeTeacherId is null, try to find teacher
|
||
if (!activeTeacherId) {
|
||
await loadStudentChat();
|
||
}
|
||
const targetId = activeTeacherId || 1;
|
||
|
||
// 1. Send via REST API to guarantee database persistence
|
||
try {
|
||
const res = await fetch('/api/chat/messages', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': 'Bearer ' + token
|
||
},
|
||
body: JSON.stringify({
|
||
receiver_id: targetId,
|
||
message: message,
|
||
message_type: 'text'
|
||
})
|
||
});
|
||
const data = await res.json();
|
||
if (res.ok && data.data) {
|
||
appendChatMessage({ message: message, is_mine: true, created_at: 'الآن' });
|
||
}
|
||
} catch (err) {
|
||
console.error('REST chat send notice:', err);
|
||
appendChatMessage({ message: message, is_mine: true, created_at: 'الآن' });
|
||
}
|
||
|
||
// 2. Also broadcast over WebSocket
|
||
if (wsSocket && wsSocket.readyState === WebSocket.OPEN) {
|
||
wsSocket.send(JSON.stringify({
|
||
event: 'chat_send',
|
||
data: {
|
||
receiver_id: targetId,
|
||
message: message,
|
||
message_type: 'text'
|
||
}
|
||
}));
|
||
}
|
||
}
|
||
|
||
// Exam Taking & AI Diagnostic Modal
|
||
function startExamModal(title, level) {
|
||
document.getElementById('exam_title_display').textContent = title;
|
||
document.getElementById('exam_question_container').style.display = 'block';
|
||
document.getElementById('exam_result_container').style.display = 'none';
|
||
document.getElementById('exam_taking_modal').style.display = 'flex';
|
||
currentExamIndex = 0;
|
||
userSelectedAnswers = [];
|
||
renderCurrentExamQuestion();
|
||
}
|
||
|
||
function closeExamModal() {
|
||
document.getElementById('exam_taking_modal').style.display = 'none';
|
||
}
|
||
|
||
function renderCurrentExamQuestion() {
|
||
const q = sampleQuestions[currentExamIndex];
|
||
document.getElementById('exam_progress_text').textContent = `السؤال ${currentExamIndex + 1} من ${sampleQuestions.length} • (${q.topic})`;
|
||
document.getElementById('exam_question_text').textContent = q.text;
|
||
|
||
const list = document.getElementById('exam_options_list');
|
||
list.innerHTML = '';
|
||
|
||
q.options.forEach((opt, idx) => {
|
||
const btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.className = 'quiz-option-btn';
|
||
btn.textContent = `${['أ', 'ب', 'ج', 'د'][idx]}) ${opt}`;
|
||
btn.onclick = () => selectExamOption(idx);
|
||
list.appendChild(btn);
|
||
});
|
||
|
||
const nextBtn = document.getElementById('btn_next_question');
|
||
nextBtn.textContent = (currentExamIndex === sampleQuestions.length - 1) ? 'إنهاء الامتحان وحساب التقييم ✨' : 'السؤال التالي ←';
|
||
}
|
||
|
||
function selectExamOption(idx) {
|
||
userSelectedAnswers[currentExamIndex] = idx;
|
||
const btns = document.querySelectorAll('#exam_options_list .quiz-option-btn');
|
||
btns.forEach((b, i) => {
|
||
if (i === idx) {
|
||
b.style.borderColor = 'var(--accent-blue)';
|
||
b.style.background = 'rgba(0, 113, 227, 0.2)';
|
||
} else {
|
||
b.style.borderColor = 'rgba(255,255,255,0.15)';
|
||
b.style.background = 'rgba(255,255,255,0.05)';
|
||
}
|
||
});
|
||
}
|
||
|
||
function handleNextQuestion() {
|
||
if (userSelectedAnswers[currentExamIndex] === undefined) {
|
||
alert('يرجى اختيار إجابة للمتابعة');
|
||
return;
|
||
}
|
||
|
||
if (currentExamIndex < sampleQuestions.length - 1) {
|
||
currentExamIndex++;
|
||
renderCurrentExamQuestion();
|
||
} else {
|
||
finishExamEvaluation();
|
||
}
|
||
}
|
||
|
||
function finishExamEvaluation() {
|
||
document.getElementById('exam_question_container').style.display = 'none';
|
||
document.getElementById('exam_result_container').style.display = 'block';
|
||
|
||
// Calculate score
|
||
let correctCount = 0;
|
||
userSelectedAnswers.forEach((ans, idx) => {
|
||
if (ans === sampleQuestions[idx].correct) correctCount++;
|
||
});
|
||
|
||
const pct = Math.round((correctCount / sampleQuestions.length) * 100);
|
||
document.getElementById('result_score_display').textContent = `${pct}%`;
|
||
document.getElementById('result_readiness_display').textContent = `+2.4%`;
|
||
document.getElementById('readiness_gauge_val').textContent = `89.9%`;
|
||
document.getElementById('readiness_status_text').textContent = `مستواك ممتاز ومتقدم (+4.8% إجمالي)`;
|
||
}
|
||
|
||
// Auth Logic
|
||
function showError(msg) {
|
||
document.getElementById('alert_box_success').style.display = 'none';
|
||
const errBox = document.getElementById('alert_box_error');
|
||
document.getElementById('alert_error_msg').textContent = msg;
|
||
errBox.style.display = 'block';
|
||
}
|
||
function showSuccess(msg) {
|
||
document.getElementById('alert_box_error').style.display = 'none';
|
||
const okBox = document.getElementById('alert_box_success');
|
||
document.getElementById('alert_success_msg').textContent = msg;
|
||
okBox.style.display = 'block';
|
||
}
|
||
function clearAlerts() {
|
||
document.getElementById('alert_box_error').style.display = 'none';
|
||
document.getElementById('alert_box_success').style.display = 'none';
|
||
}
|
||
|
||
function switchToPhoneStep() {
|
||
document.getElementById('step_otp_container').style.display = 'none';
|
||
document.getElementById('step_phone_container').style.display = 'block';
|
||
clearAlerts();
|
||
clearInterval(countdownTimer);
|
||
}
|
||
|
||
function switchToOtpStep(maskedPhone) {
|
||
document.getElementById('step_phone_container').style.display = 'none';
|
||
document.getElementById('step_otp_container').style.display = 'block';
|
||
document.getElementById('otp_target_display').textContent = maskedPhone;
|
||
const otpInput = document.getElementById('student_otp_code');
|
||
otpInput.value = '';
|
||
otpInput.focus();
|
||
startTimer(60);
|
||
}
|
||
|
||
function startTimer(seconds) {
|
||
timerSeconds = seconds;
|
||
clearInterval(countdownTimer);
|
||
const timerLabel = document.getElementById('resend_timer_label');
|
||
const resendBtn = document.getElementById('btn_resend');
|
||
resendBtn.style.display = 'none';
|
||
timerLabel.style.display = 'inline';
|
||
timerLabel.textContent = `إعادة الإرسال بعد (${timerSeconds}ث)`;
|
||
|
||
countdownTimer = setInterval(() => {
|
||
timerSeconds--;
|
||
if (timerSeconds > 0) {
|
||
timerLabel.textContent = `إعادة الإرسال بعد (${timerSeconds}ث)`;
|
||
} else {
|
||
clearInterval(countdownTimer);
|
||
timerLabel.style.display = 'none';
|
||
resendBtn.style.display = 'inline';
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
async function handleSendOtp(e) {
|
||
if (e) e.preventDefault();
|
||
clearAlerts();
|
||
|
||
const phone = document.getElementById('student_phone').value.trim();
|
||
const fullName = document.getElementById('student_fullname').value.trim();
|
||
const sendBtn = document.getElementById('btn_send_otp');
|
||
const sendText = document.getElementById('btn_send_text');
|
||
const sendSpinner = document.getElementById('btn_send_spinner');
|
||
|
||
if (!phone) {
|
||
showError('يرجى إدخال رقم الهاتف');
|
||
return;
|
||
}
|
||
|
||
sendBtn.disabled = true;
|
||
sendText.textContent = 'جارٍ إرسال البطاقة عبر الواتساب...';
|
||
sendSpinner.style.display = 'block';
|
||
|
||
try {
|
||
const res = await fetch('/api/auth/otp/request', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ phone_number: phone, role: 'student', full_name: fullName })
|
||
});
|
||
const data = await res.json();
|
||
if (res.ok && data.status === 'success') {
|
||
showSuccess(data.message || 'تم إرسال بطاقة التحقق عبر الواتساب!');
|
||
switchToOtpStep(data.data?.phone_masked || phone);
|
||
} else {
|
||
showError(data.message || 'فشل إرسال رمز التحقق.');
|
||
}
|
||
} catch (err) {
|
||
showError('حدث خطأ في الاتصال بالسيرفر.');
|
||
} finally {
|
||
sendBtn.disabled = false;
|
||
sendText.textContent = 'إرسال رمز التحقق (OTP) ←';
|
||
sendSpinner.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
async function handleVerifyOtp(e) {
|
||
if (e) e.preventDefault();
|
||
clearAlerts();
|
||
|
||
const phone = document.getElementById('student_phone').value.trim();
|
||
const fullName = document.getElementById('student_fullname').value.trim();
|
||
const otpCode = document.getElementById('student_otp_code').value.trim();
|
||
const verifyBtn = document.getElementById('btn_verify_otp');
|
||
const verifyText = document.getElementById('btn_verify_text');
|
||
const verifySpinner = document.getElementById('btn_verify_spinner');
|
||
|
||
if (!otpCode || otpCode.length < 6) {
|
||
showError('يرجى إدخال رمز التحقق المكون من 6 أرقام');
|
||
return;
|
||
}
|
||
|
||
verifyBtn.disabled = true;
|
||
verifyText.textContent = 'جارٍ التحقق...';
|
||
verifySpinner.style.display = 'block';
|
||
|
||
try {
|
||
const res = await fetch('/api/auth/otp/verify', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ phone_number: phone, otp: otpCode, role: 'student', full_name: fullName })
|
||
});
|
||
const data = await res.json();
|
||
if (res.ok && data.status === 'success' && data.data?.token) {
|
||
const token = data.data.token;
|
||
const user = data.data.user;
|
||
localStorage.setItem('saqel_student_jwt', token);
|
||
if (user) {
|
||
localStorage.setItem('saqel_student_user', JSON.stringify(user));
|
||
}
|
||
document.cookie = "saqel_student_jwt=" + token + "; path=/; max-age=2592000; SameSite=Lax";
|
||
initWebSocket(token);
|
||
renderDashboard(user);
|
||
await checkStudentSession(token);
|
||
} else {
|
||
showError(data.message || 'رمز التحقق غير صحيح');
|
||
}
|
||
} catch (err) {
|
||
showError('حدث خطأ في التحقق من الرمز.');
|
||
} finally {
|
||
verifyBtn.disabled = false;
|
||
verifyText.textContent = 'تأكيد الدخول للمنصة ✨';
|
||
verifySpinner.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
async function checkStudentSession(token) {
|
||
try {
|
||
const res = await fetch('/api/auth/me', {
|
||
headers: { 'Authorization': 'Bearer ' + token }
|
||
});
|
||
if (res.status === 401) {
|
||
handleLogout();
|
||
return;
|
||
}
|
||
const data = await res.json();
|
||
if (res.ok && data.data) {
|
||
studentData = data.data;
|
||
localStorage.setItem('saqel_student_user', JSON.stringify(data.data));
|
||
renderDashboard(data.data);
|
||
}
|
||
} catch (e) {
|
||
console.error('Session check notice:', e);
|
||
}
|
||
}
|
||
|
||
function renderDashboard(user) {
|
||
document.getElementById('auth_container').style.display = 'none';
|
||
document.getElementById('dashboard_container').style.display = 'block';
|
||
document.getElementById('switch_to_teacher_link').style.display = 'none';
|
||
document.getElementById('auth_user_badge').style.display = 'flex';
|
||
|
||
const displayName = user?.full_name || 'طالبنا المتميز';
|
||
document.getElementById('student_display_name').textContent = `أهلاً، ${displayName}`;
|
||
document.getElementById('dashboard_welcome_title').textContent = `أهلاً بك يا ${displayName} 🚀`;
|
||
document.getElementById('drm_watermark_text').textContent = `SAQEL-DRM • ${user?.phone_number || '079XXXXXXX'}`;
|
||
|
||
loadStudentChat();
|
||
}
|
||
|
||
function handleLogout() {
|
||
localStorage.removeItem('saqel_student_jwt');
|
||
localStorage.removeItem('saqel_student_user');
|
||
document.cookie = "saqel_student_jwt=; path=/; max-age=0;";
|
||
if (wsSocket) wsSocket.close();
|
||
location.reload();
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|
||
HTML;
|
||
}
|
||
}
|