Autosave: 20260406-042125

This commit is contained in:
Flatlogic Bot 2026-04-06 04:21:26 +00:00
parent 3f8c63d1b3
commit 1c8373aa37
20 changed files with 2666 additions and 526 deletions

296
admin.php Normal file
View File

@ -0,0 +1,296 @@
<?php ob_start(); ?>
<?php
require_once __DIR__ . '/includes/app.php';
$page = $_GET['page'] ?? 'dashboard';
$action = $_GET['action'] ?? '';
// Handle Profile Update
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $page === 'profile') {
$name = $_POST['name'] ?? '';
$description = $_POST['description'] ?? '';
$logo_path = get_platform_profile()['logo_path'] ?? '';
$favicon_path = get_platform_profile()['favicon_path'] ?? '';
$upload_dir = __DIR__ . '/assets/images/uploads/';
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0777, true);
}
if (!empty($_FILES['logo']['tmp_name'])) {
$filename = 'logo_' . time() . '_' . basename($_FILES['logo']['name']);
$target = $upload_dir . $filename;
if (move_uploaded_file($_FILES['logo']['tmp_name'], $target)) {
$logo_path = 'assets/images/uploads/' . $filename;
}
}
if (!empty($_FILES['favicon']['tmp_name'])) {
$filename = 'favicon_' . time() . '_' . basename($_FILES['favicon']['name']);
$target = $upload_dir . $filename;
if (move_uploaded_file($_FILES['favicon']['tmp_name'], $target)) {
$favicon_path = 'assets/images/uploads/' . $filename;
}
}
$stmt = db()->prepare("UPDATE platform_profile SET name = :name, description = :description, logo_path = :logo, favicon_path = :favicon WHERE id = 1");
$stmt->execute([
'name' => $name,
'description' => $description,
'logo' => $logo_path,
'favicon' => $favicon_path
]);
header('Location: ' . app_url('admin.php', ['page' => 'profile', 'saved' => 1]));
exit;
}
$metrics = subscription_metrics();
$recent = fetch_recent_subscriptions();
render_head(
t('Admin', 'الإدارة') . ' - ' . ucfirst($page),
t('Manage your learning platform.', 'إدارة منصة التعلم الخاصة بك.')
);
?>
<nav class="navbar navbar-expand-lg bg-white border-bottom sticky-top admin-navbar" style="z-index: 1040;">
<div class="container-fluid px-3 px-md-4">
<a class="navbar-brand fw-semibold d-flex align-items-center" href="<?= h(app_url('index.php')) ?>" target="_blank">
<?php $prof = get_platform_profile(); if (!empty($prof['logo_path'])): ?>
<img src="<?= h(asset_url($prof['logo_path'])) ?>" alt="Logo" style="height: 32px; margin-right: 8px; border-radius: 4px;">
<?php endif; ?>
<span style="color: var(--accent);"><?= h(app_name()) ?></span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#adminNavbar" aria-controls="adminNavbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse justify-content-end" id="adminNavbar">
<ul class="navbar-nav mb-2 mb-lg-0 align-items-center gap-3">
<li class="nav-item">
<div class="btn-group btn-group-sm" role="group">
<a class="btn btn-outline-dark <?= current_lang() === 'en' ? 'active' : '' ?>" href="<?= h(page_lang_link('en')) ?>">EN</a>
<a class="btn btn-outline-dark <?= current_lang() === 'ar' ? 'active' : '' ?>" href="<?= h(page_lang_link('ar')) ?>">AR</a>
</div>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle d-flex align-items-center gap-2" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<div class="bg-light rounded-circle d-flex align-items-center justify-content-center" style="width: 32px; height: 32px; border: 1px solid var(--border);">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0"/>
<path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1"/>
</svg>
</div>
<span class="fw-medium"><?= h(t('Admin', 'المدير')) ?></span>
</a>
<ul class="dropdown-menu dropdown-menu-end shadow-sm">
<li><a class="dropdown-item" href="<?= h(app_url('admin.php', ['page' => 'profile'])) ?>"><?= h(t('Platform Profile', 'ملف المنصة')) ?></a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="<?= h(app_url('index.php')) ?>"><?= h(t('Exit Admin', 'خروج من الإدارة')) ?></a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<div class="d-flex flex-column flex-md-row" style="min-height: calc(100vh - 61px); overflow: hidden;">
<!-- Sidebar -->
<aside class="d-flex flex-column flex-shrink-0 p-3 bg-white border-end shadow-sm admin-sidebar">
<div class="fs-5 fw-bold mb-3 mt-2 px-2 text-secondary d-none d-md-block"><?= h(t('Admin Menu', 'قائمة الإدارة')) ?></div>
<ul class="nav nav-pills flex-row flex-md-column mb-auto gap-2 flex-wrap">
<li class="nav-item">
<a href="<?= h(app_url('admin.php', ['page' => 'dashboard'])) ?>" class="nav-link <?= $page === 'dashboard' ? 'active' : 'link-dark' ?>" <?= $page === 'dashboard' ? 'style="background-color: var(--accent); color: white;"' : '' ?>>
<?= h(t('Dashboard', 'لوحة القيادة')) ?>
</a>
</li>
<li class="nav-item">
<a href="<?= h(app_url('admin.php', ['page' => 'profile'])) ?>" class="nav-link <?= $page === 'profile' ? 'active' : 'link-dark' ?>" <?= $page === 'profile' ? 'style="background-color: var(--accent); color: white;"' : '' ?>>
<?= h(t('Platform Profile', 'ملف المنصة')) ?>
</a>
</li>
<li>
<a href="<?= h(app_url('admin.php', ['page' => 'classes'])) ?>" class="nav-link <?= $page === 'classes' ? 'active' : 'link-dark' ?>" <?= $page === 'classes' ? 'style="background-color: var(--accent); color: white;"' : '' ?>>
<?= h(t('Classes', 'الصفوف')) ?>
</a>
</li>
<li>
<a href="<?= h(app_url('admin.php', ['page' => 'subjects'])) ?>" class="nav-link <?= $page === 'subjects' ? 'active' : 'link-dark' ?>" <?= $page === 'subjects' ? 'style="background-color: var(--accent); color: white;"' : '' ?>>
<?= h(t('Subjects', 'المواد')) ?>
</a>
</li>
<li>
<a href="<?= h(app_url('admin.php', ['page' => 'plans'])) ?>" class="nav-link <?= $page === 'plans' ? 'active' : 'link-dark' ?>" <?= $page === 'plans' ? 'style="background-color: var(--accent); color: white;"' : '' ?>>
<?= h(t('Plans', 'الخطط')) ?>
</a>
</li>
<li>
<a href="<?= h(app_url('admin.php', ['page' => 'students'])) ?>" class="nav-link <?= $page === 'students' ? 'active' : 'link-dark' ?>" <?= $page === 'students' ? 'style="background-color: var(--accent); color: white;"' : '' ?>>
<?= h(t('Students', 'الطلاب')) ?>
</a>
</li>
<li>
<a href="<?= h(app_url('admin.php', ['page' => 'teachers'])) ?>" class="nav-link <?= $page === 'teachers' ? 'active' : 'link-dark' ?>" <?= $page === 'teachers' ? 'style="background-color: var(--accent); color: white;"' : '' ?>>
<?= h(t('Teachers', 'المعلمون')) ?>
</a>
</li>
</ul>
</aside>
<!-- Content Area -->
<main class="flex-grow-1 d-flex flex-column" style="height: calc(100vh - 61px); overflow-y: auto;">
<div class="p-4 p-md-5 flex-grow-1">
<?php if ($page === 'dashboard'): ?>
<div class="section-header mb-4">
<div>
<span class="eyebrow"><?= h(t('Admin', 'الإدارة')) ?></span>
<h1 class="section-title mb-2"><?= h(t('Platform subscription overview', 'نظرة عامة على اشتراكات المنصة')) ?></h1>
<p class="text-secondary mb-0"><?= h(t('This first admin screen focuses on the subscription funnel that powers student access.', 'تركز شاشة الإدارة الأولى هذه على مسار الاشتراك الذي يغذي وصول الطلاب.')) ?></p>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-md-4"><div class="metric-card tall"><strong><?= h((string) $metrics['total']) ?></strong><span><?= h(t('total subscriptions', 'إجمالي الاشتراكات')) ?></span></div></div>
<div class="col-md-4"><div class="metric-card tall"><strong><?= h((string) $metrics['active']) ?></strong><span><?= h(t('active plans', 'خطط نشطة')) ?></span></div></div>
<div class="col-md-4"><div class="metric-card tall"><strong><?= h((string) $metrics['arabic']) ?></strong><span><?= h(t('Arabic preference', 'تفضيل العربية')) ?></span></div></div>
</div>
<div class="panel-card">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="h5 mb-0"><?= h(t('Latest subscriptions', 'أحدث الاشتراكات')) ?></h2>
</div>
<?php if (!$recent): ?>
<div class="empty-state-inline"><?= h(t('No subscriptions recorded yet.', 'لم يتم تسجيل أي اشتراكات بعد.')) ?></div>
<?php elseif ($page === 'classes'): ?>
<div class="table-responsive">
<table class="table align-middle dashboard-table mb-0">
<thead>
<tr>
<th>#</th>
<th><?= h(t('Student', 'الطالب')) ?></th>
<th><?= h(t('Plan', 'الخطة')) ?></th>
<th><?= h(t('Language', 'اللغة')) ?></th>
<th><?= h(t('Payment', 'الدفع')) ?></th>
<th><?= h(t('Detail', 'التفاصيل')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($recent as $item): ?>
<?php $plan = get_plan($item['plan_key']) ?? get_plan('plus'); ?>
<tr>
<td><?= h((string) $item['id']) ?></td>
<td>
<div class="fw-semibold"><?= h($item['full_name']) ?></div>
<div class="small text-secondary"><?= h($item['email']) ?></div>
</td>
<td><?= h(plan_name($plan)) ?></td>
<td><?= h($item['preferred_language'] === 'ar' ? 'العربية' : 'English') ?></td>
<td><span class="<?= h(status_badge((string) $item['payment_status'])) ?>"><?= h((string) $item['payment_status']) ?></span></td>
<td><a class="btn btn-sm btn-outline-dark" href="<?= h(app_url('subscription.php', ['id' => (int) $item['id']])) ?>"><?= h(t('Open', 'فتح')) ?></a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php elseif ($page === 'profile'): ?>
<?php $prof = get_platform_profile(); ?>
<div class="section-header mb-4">
<div>
<span class="eyebrow"><?= h(t('Settings', 'الإعدادات')) ?></span>
<h1 class="section-title mb-2"><?= h(t('Platform Profile', 'ملف المنصة')) ?></h1>
<p class="text-secondary mb-0"><?= h(t('Update your platforms name, description, logo, and favicon.', 'قم بتحديث اسم منصتك ووصفها وشعارها والأيقونة المفضلة.')) ?></p>
</div>
</div>
<?php if (!empty($_GET['saved'])): ?>
<div class="alert alert-success"><?= h(t('Profile updated successfully.', 'تم تحديث الملف بنجاح.')) ?></div>
<?php endif; ?>
<div class="panel-card" style="max-width: 600px;">
<form method="post" enctype="multipart/form-data">
<div class="mb-3">
<label class="form-label"><?= h(t('Platform Name', 'اسم المنصة')) ?></label>
<input type="text" name="name" class="form-control" value="<?= h($prof['name'] ?? '') ?>" required>
</div>
<div class="mb-3">
<label class="form-label"><?= h(t('Description', 'الوصف')) ?></label>
<textarea name="description" class="form-control" rows="3"><?= h($prof['description'] ?? '') ?></textarea>
</div>
<div class="mb-3">
<label class="form-label"><?= h(t('Logo', 'الشعار')) ?></label>
<?php if (!empty($prof['logo_path'])): ?>
<div class="mb-2">
<img src="<?= h(asset_url($prof['logo_path'])) ?>" alt="Logo" style="height: 60px; border: 1px solid var(--border); border-radius: 8px; padding: 4px; background: #fff;">
</div>
<?php endif; ?>
<input type="file" name="logo" class="form-control" accept="image/*">
</div>
<div class="mb-4">
<label class="form-label"><?= h(t('Favicon', 'الأيقونة المفضلة')) ?></label>
<?php if (!empty($prof['favicon_path'])): ?>
<div class="mb-2">
<img src="<?= h(asset_url($prof['favicon_path'])) ?>" alt="Favicon" style="height: 32px; border: 1px solid var(--border); border-radius: 4px; padding: 2px; background: #fff;">
</div>
<?php endif; ?>
<input type="file" name="favicon" class="form-control" accept="image/x-icon,image/png,image/jpeg,image/svg+xml">
</div>
<button type="submit" class="btn btn-primary" style="background-color: var(--accent); border-color: var(--accent);">
<?= h(t('Save Changes', 'حفظ التغييرات')) ?>
</button>
</form>
</div>
<?php elseif ($page === 'classes'): ?>
<?php require_once __DIR__ . '/admin_classes.php'; ?>
<?php elseif ($page === 'subjects'): ?>
<?php require_once __DIR__ . '/admin_subjects.php'; ?>
<?php else: ?>
<div class="section-header mb-4">
<div>
<h1 class="section-title mb-2"><?= h(ucfirst($page)) ?></h1>
</div>
</div>
<div class="panel-card">
<div class="empty-state-inline">
<?= h(t('This section is currently under construction. More admin features coming soon!', 'هذا القسم قيد الإنشاء حالياً. المزيد من ميزات الإدارة قريباً!')) ?>
</div>
</div>
<?php endif; ?>
</div>
<footer class="bg-white border-top py-3 mt-auto w-100">
<div class="container-fluid px-4 px-md-5 d-flex justify-content-between align-items-center small text-secondary">
<div>
&copy; <?= date('Y') ?> <?= h(app_name()) ?>. <?= h(t('All rights reserved.', 'جميع الحقوق محفوظة.')) ?>
</div>
<div>
<a href="<?= h(app_url('index.php')) ?>" class="text-decoration-none text-secondary"><?= h(t('View Site', 'عرض الموقع')) ?></a>
</div>
</div>
</footer>
</main>
</div>
<style>
.admin-sidebar {
width: 280px;
height: calc(100vh - 61px);
overflow-y: auto;
}
.admin-navbar {
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
@media (max-width: 767.98px) {
.admin-sidebar {
width: 100%;
height: auto;
border-right: none !important;
border-bottom: 1px solid var(--border);
}
}
</style>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script src="<?= h(asset_url('assets/js/main.js')) ?>"></script>
</body>
</html>

162
admin_classes.php Normal file
View File

@ -0,0 +1,162 @@
<?php
// admin_classes.php
require_once __DIR__ . '/includes/app.php';
$action = $_GET['action'] ?? 'list';
$id = (int)($_GET['id'] ?? 0);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($action === 'delete' && $id > 0) {
$stmt = db()->prepare("DELETE FROM classes WHERE id = ?");
$stmt->execute([$id]);
header('Location: ' . app_url('admin.php', ['page' => 'classes']));
exit;
}
if ($action === 'edit' || $action === 'add') {
$name_en = $_POST['name_en'] ?? '';
$name_ar = $_POST['name_ar'] ?? '';
$desc_en = $_POST['description_en'] ?? '';
$desc_ar = $_POST['description_ar'] ?? '';
if ($action === 'edit' && $id > 0) {
$stmt = db()->prepare("UPDATE classes SET name_en=?, name_ar=?, description_en=?, description_ar=? WHERE id=?");
$stmt->execute([$name_en, $name_ar, $desc_en, $desc_ar, $id]);
} else {
$stmt = db()->prepare("INSERT INTO classes (name_en, name_ar, description_en, description_ar) VALUES (?, ?, ?, ?)");
$stmt->execute([$name_en, $name_ar, $desc_en, $desc_ar]);
}
header('Location: ' . app_url('admin.php', ['page' => 'classes']));
exit;
}
}
if ($action === 'edit' || $action === 'add'):
$item = ['name_en'=>'', 'name_ar'=>'', 'description_en'=>'', 'description_ar'=>''];
if ($action === 'edit' && $id > 0) {
$stmt = db()->prepare("SELECT * FROM classes WHERE id = ?");
$stmt->execute([$id]);
$item = $stmt->fetch(PDO::FETCH_ASSOC) ?: $item;
}
?>
<div class="section-header mb-4">
<div>
<h1 class="section-title mb-2"><?= h($action === 'edit' ? t('Edit Class', 'تعديل الصف') : t('Add Class', 'إضافة صف')) ?></h1>
</div>
</div>
<div class="panel-card" style="max-width: 600px;">
<form method="post" action="<?= h(app_url('admin.php', ['page'=>'classes', 'action'=>$action, 'id'=>$id])) ?>">
<div class="mb-3">
<label class="form-label">Name (EN)</label>
<input type="text" name="name_en" class="form-control" value="<?= h($item['name_en']) ?>" required>
</div>
<div class="mb-3">
<label class="form-label">Name (AR)</label>
<input type="text" name="name_ar" class="form-control" value="<?= h($item['name_ar']) ?>" required>
</div>
<div class="mb-3">
<label class="form-label">Description (EN)</label>
<textarea name="description_en" class="form-control"><?= h($item['description_en']) ?></textarea>
</div>
<div class="mb-3">
<label class="form-label">Description (AR)</label>
<textarea name="description_ar" class="form-control"><?= h($item['description_ar']) ?></textarea>
</div>
<button type="submit" class="btn btn-primary" style="background-color: var(--accent); border-color: var(--accent);">
<?= h(t('Save', 'حفظ')) ?>
</button>
<a href="<?= h(app_url('admin.php', ['page'=>'classes'])) ?>" class="btn btn-outline-secondary"><?= h(t('Cancel', 'إلغاء')) ?></a>
</form>
</div>
<?php else: // list view
$search = $_GET['search'] ?? '';
$page_num = max(1, (int)($_GET['p'] ?? 1));
$limit = 10;
$offset = ($page_num - 1) * $limit;
$where = "";
$params = [];
if ($search !== '') {
$where = "WHERE name_en LIKE ? OR name_ar LIKE ?";
$params[] = "%$search%";
$params[] = "%$search%";
}
$total_stmt = db()->prepare("SELECT COUNT(*) FROM classes $where");
$total_stmt->execute($params);
$total = $total_stmt->fetchColumn();
$pages = ceil($total / $limit);
$stmt = db()->prepare("SELECT * FROM classes $where ORDER BY id DESC LIMIT $limit OFFSET $offset");
$stmt->execute($params);
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<div class="section-header mb-4 d-flex justify-content-between align-items-center">
<div>
<h1 class="section-title mb-2"><?= h(t('Classes', 'الصفوف')) ?></h1>
</div>
<a href="<?= h(app_url('admin.php', ['page'=>'classes', 'action'=>'add'])) ?>" class="btn btn-primary" style="background-color: var(--accent); border-color: var(--accent);">+ <?= h(t('Add Class', 'إضافة صف')) ?></a>
</div>
<div class="panel-card mb-4">
<form method="get" class="d-flex gap-2 align-items-center">
<input type="hidden" name="page" value="classes">
<input type="text" name="search" class="form-control w-auto" placeholder="<?= h(t('Search...', 'بحث...')) ?>" value="<?= h($search) ?>">
<button type="submit" class="btn btn-outline-secondary"><?= h(t('Filter', 'تصفية')) ?></button>
<?php if ($search): ?>
<a href="<?= h(app_url('admin.php', ['page'=>'classes'])) ?>" class="btn btn-link text-secondary text-decoration-none"><?= h(t('Clear', 'مسح')) ?></a>
<?php endif; ?>
</form>
</div>
<div class="panel-card">
<div class="table-responsive">
<table class="table align-middle dashboard-table mb-0">
<thead>
<tr>
<th>ID</th>
<th><?= h(t('Name', 'الاسم')) ?></th>
<th><?= h(t('Description', 'الوصف')) ?></th>
<th><?= h(t('Actions', 'إجراءات')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach($items as $row): ?>
<tr>
<td><?= h((string)$row['id']) ?></td>
<td>
<div class="fw-semibold"><?= h(current_lang() === 'ar' ? $row['name_ar'] : $row['name_en']) ?></div>
</td>
<td class="text-truncate" style="max-width: 200px;">
<?= h(current_lang() === 'ar' ? $row['description_ar'] : $row['description_en']) ?>
</td>
<td>
<a href="<?= h(app_url('admin.php', ['page'=>'classes', 'action'=>'edit', 'id'=>$row['id']])) ?>" class="btn btn-sm btn-outline-primary"><?= h(t('Edit', 'تعديل')) ?></a>
<form method="post" action="<?= h(app_url('admin.php', ['page'=>'classes', 'action'=>'delete', 'id'=>$row['id']])) ?>" class="d-inline" onsubmit="return confirm('<?= h(t('Are you sure?', 'هل أنت متأكد؟')) ?>');">
<button type="submit" class="btn btn-sm btn-outline-danger"><?= h(t('Delete', 'حذف')) ?></button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php if(!$items): ?>
<tr><td colspan="4" class="text-center text-secondary py-3"><?= h(t('No classes found.', 'لا توجد صفوف.')) ?></td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php if ($pages > 1): ?>
<nav class="mt-4">
<ul class="pagination justify-content-center">
<?php for($i=1; $i<=$pages; $i++): ?>
<li class="page-item <?= $i === $page_num ? 'active' : '' ?>">
<a class="page-link" href="<?= h(app_url('admin.php', ['page'=>'classes', 'p'=>$i, 'search'=>$search])) ?>"><?= $i ?></a>
</li>
<?php endfor; ?>
</ul>
</nav>
<?php endif; ?>
<?php endif; ?>

251
admin_subjects.php Normal file
View File

@ -0,0 +1,251 @@
<?php
// admin_subjects.php
require_once __DIR__ . '/includes/app.php';
$action = $_GET['action'] ?? 'list';
$id = (int)($_GET['id'] ?? 0);
// Fetch classes for dropdown and filter
$classes_stmt = db()->query("SELECT id, name_en, name_ar FROM classes ORDER BY id DESC");
$all_classes = $classes_stmt->fetchAll(PDO::FETCH_ASSOC);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($action === 'delete' && $id > 0) {
$stmt = db()->prepare("DELETE FROM subjects WHERE id = ?");
$stmt->execute([$id]);
header('Location: ' . app_url('admin.php', ['page' => 'subjects']));
exit;
}
if ($action === 'edit' || $action === 'add') {
$slug = $_POST['slug'] ?? '';
$class_id = !empty($_POST['class_id']) ? (int)$_POST['class_id'] : null;
$title_en = $_POST['title_en'] ?? '';
$title_ar = $_POST['title_ar'] ?? '';
$summary_en = $_POST['summary_en'] ?? '';
$summary_ar = $_POST['summary_ar'] ?? '';
$teacher_en = $_POST['teacher_en'] ?? '';
$teacher_ar = $_POST['teacher_ar'] ?? '';
$meet_url = $_POST['meet_url'] ?? '';
if ($action === 'edit' && $id > 0) {
$stmt = db()->prepare("UPDATE subjects SET slug=?, class_id=?, title_en=?, title_ar=?, summary_en=?, summary_ar=?, teacher_en=?, teacher_ar=?, meet_url=? WHERE id=?");
$stmt->execute([$slug, $class_id, $title_en, $title_ar, $summary_en, $summary_ar, $teacher_en, $teacher_ar, $meet_url, $id]);
} else {
$stmt = db()->prepare("INSERT INTO subjects (slug, class_id, title_en, title_ar, summary_en, summary_ar, teacher_en, teacher_ar, meet_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$slug, $class_id, $title_en, $title_ar, $summary_en, $summary_ar, $teacher_en, $teacher_ar, $meet_url]);
}
header('Location: ' . app_url('admin.php', ['page' => 'subjects']));
exit;
}
}
if ($action === 'edit' || $action === 'add'):
$item = ['slug'=>'', 'class_id'=>null, 'title_en'=>'', 'title_ar'=>'', 'summary_en'=>'', 'summary_ar'=>'', 'teacher_en'=>'', 'teacher_ar'=>'', 'meet_url'=>''];
if ($action === 'edit' && $id > 0) {
$stmt = db()->prepare("SELECT * FROM subjects WHERE id = ?");
$stmt->execute([$id]);
$item = $stmt->fetch(PDO::FETCH_ASSOC) ?: $item;
}
?>
<div class="section-header mb-4">
<div>
<h1 class="section-title mb-2"><?= h($action === 'edit' ? t('Edit Subject', 'تعديل المادة') : t('Add Subject', 'إضافة مادة')) ?></h1>
</div>
</div>
<div class="panel-card" style="max-width: 800px;">
<form method="post" action="<?= h(app_url('admin.php', ['page'=>'subjects', 'action'=>$action, 'id'=>$id])) ?>">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Slug</label>
<input type="text" name="slug" class="form-control" value="<?= h($item['slug']) ?>" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label"><?= h(t('Class', 'الصف')) ?></label>
<select name="class_id" class="form-control">
<option value=""><?= h(t('-- Select Class --', '-- اختر الصف --')) ?></option>
<?php foreach($all_classes as $c): ?>
<option value="<?= $c['id'] ?>" <?= $item['class_id'] == $c['id'] ? 'selected' : '' ?>>
<?= h(current_lang() === 'ar' ? $c['name_ar'] : $c['name_en']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Title (EN)</label>
<input type="text" name="title_en" class="form-control" value="<?= h($item['title_en']) ?>" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Title (AR)</label>
<input type="text" name="title_ar" class="form-control" value="<?= h($item['title_ar']) ?>" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Summary (EN)</label>
<textarea name="summary_en" class="form-control" rows="2"><?= h($item['summary_en']) ?></textarea>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Summary (AR)</label>
<textarea name="summary_ar" class="form-control" rows="2"><?= h($item['summary_ar']) ?></textarea>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Teacher (EN)</label>
<input type="text" name="teacher_en" class="form-control" value="<?= h($item['teacher_en']) ?>">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Teacher (AR)</label>
<input type="text" name="teacher_ar" class="form-control" value="<?= h($item['teacher_ar']) ?>">
</div>
</div>
<div class="mb-4">
<label class="form-label">Meet URL</label>
<input type="url" name="meet_url" class="form-control" value="<?= h($item['meet_url']) ?>">
</div>
<button type="submit" class="btn btn-primary" style="background-color: var(--accent); border-color: var(--accent);">
<?= h(t('Save', 'حفظ')) ?>
</button>
<a href="<?= h(app_url('admin.php', ['page'=>'subjects'])) ?>" class="btn btn-outline-secondary"><?= h(t('Cancel', 'إلغاء')) ?></a>
</form>
</div>
<?php else: // list view
$search = $_GET['search'] ?? '';
$filter_class = $_GET['class_id'] ?? '';
$page_num = max(1, (int)($_GET['p'] ?? 1));
$limit = 10;
$offset = ($page_num - 1) * $limit;
$where_clauses = [];
$params = [];
if ($search !== '') {
$where_clauses[] = "(s.title_en LIKE ? OR s.title_ar LIKE ?)";
$params[] = "%$search%";
$params[] = "%$search%";
}
if ($filter_class !== '') {
$where_clauses[] = "s.class_id = ?";
$params[] = $filter_class;
}
$where = "";
if (count($where_clauses) > 0) {
$where = "WHERE " . implode(" AND ", $where_clauses);
}
$total_stmt = db()->prepare("SELECT COUNT(*) FROM subjects s $where");
$total_stmt->execute($params);
$total = $total_stmt->fetchColumn();
$pages = ceil($total / $limit);
$stmt = db()->prepare("
SELECT s.*, c.name_en AS class_name_en, c.name_ar AS class_name_ar
FROM subjects s
LEFT JOIN classes c ON s.class_id = c.id
$where
ORDER BY s.id DESC
LIMIT $limit OFFSET $offset
");
$stmt->execute($params);
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<div class="section-header mb-4 d-flex justify-content-between align-items-center">
<div>
<h1 class="section-title mb-2"><?= h(t('Subjects', 'المواد')) ?></h1>
</div>
<a href="<?= h(app_url('admin.php', ['page'=>'subjects', 'action'=>'add'])) ?>" class="btn btn-primary" style="background-color: var(--accent); border-color: var(--accent);">+ <?= h(t('Add Subject', 'إضافة مادة')) ?></a>
</div>
<div class="panel-card mb-4">
<form method="get" class="d-flex gap-2 flex-wrap align-items-center">
<input type="hidden" name="page" value="subjects">
<input type="text" name="search" class="form-control w-auto" placeholder="<?= h(t('Search title...', 'بحث في العنوان...')) ?>" value="<?= h($search) ?>">
<select name="class_id" class="form-control w-auto">
<option value=""><?= h(t('All Classes', 'جميع الصفوف')) ?></option>
<?php foreach($all_classes as $c): ?>
<option value="<?= $c['id'] ?>" <?= $filter_class == $c['id'] ? 'selected' : '' ?>>
<?= h(current_lang() === 'ar' ? $c['name_ar'] : $c['name_en']) ?>
</option>
<?php endforeach; ?>
</select>
<button type="submit" class="btn btn-outline-secondary"><?= h(t('Filter', 'تصفية')) ?></button>
<?php if ($search || $filter_class !== ''): ?>
<a href="<?= h(app_url('admin.php', ['page'=>'subjects'])) ?>" class="btn btn-link text-secondary text-decoration-none"><?= h(t('Clear', 'مسح')) ?></a>
<?php endif; ?>
</form>
</div>
<div class="panel-card">
<div class="table-responsive">
<table class="table align-middle dashboard-table mb-0">
<thead>
<tr>
<th>ID</th>
<th><?= h(t('Subject', 'المادة')) ?></th>
<th><?= h(t('Class', 'الصف')) ?></th>
<th><?= h(t('Teacher', 'المعلم')) ?></th>
<th><?= h(t('Actions', 'إجراءات')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach($items as $row): ?>
<tr>
<td><?= h((string)$row['id']) ?></td>
<td>
<div class="fw-semibold"><?= h(current_lang() === 'ar' ? $row['title_ar'] : $row['title_en']) ?></div>
<div class="small text-secondary"><?= h($row['slug']) ?></div>
</td>
<td>
<?php if ($row['class_id']): ?>
<span class="badge bg-light text-dark border">
<?= h(current_lang() === 'ar' ? $row['class_name_ar'] : $row['class_name_en']) ?>
</span>
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<td>
<?= h(current_lang() === 'ar' ? $row['teacher_ar'] : $row['teacher_en']) ?>
</td>
<td>
<a href="<?= h(app_url('admin.php', ['page'=>'subjects', 'action'=>'edit', 'id'=>$row['id']])) ?>" class="btn btn-sm btn-outline-primary"><?= h(t('Edit', 'تعديل')) ?></a>
<form method="post" action="<?= h(app_url('admin.php', ['page'=>'subjects', 'action'=>'delete', 'id'=>$row['id']])) ?>" class="d-inline" onsubmit="return confirm('<?= h(t('Are you sure?', 'هل أنت متأكد؟')) ?>');">
<button type="submit" class="btn btn-sm btn-outline-danger"><?= h(t('Delete', 'حذف')) ?></button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php if(!$items): ?>
<tr><td colspan="5" class="text-center text-secondary py-3"><?= h(t('No subjects found.', 'لا توجد مواد.')) ?></td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php if ($pages > 1): ?>
<nav class="mt-4">
<ul class="pagination justify-content-center">
<?php for($i=1; $i<=$pages; $i++): ?>
<li class="page-item <?= $i === $page_num ? 'active' : '' ?>">
<a class="page-link" href="<?= h(app_url('admin.php', ['page'=>'subjects', 'p'=>$i, 'search'=>$search, 'class_id'=>$filter_class])) ?>"><?= $i ?></a>
</li>
<?php endfor; ?>
</ul>
</nav>
<?php endif; ?>
<?php endif; ?>

View File

@ -1,403 +1,456 @@
body {
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
background-size: 400% 400%;
animation: gradient 15s ease infinite;
color: #212529;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
margin: 0;
min-height: 100vh;
:root {
--bg: #f3f5f7;
--surface: #ffffff;
--surface-muted: #f8fafc;
--text: #111827;
--muted: #667085;
--border: #d9e1ea;
--accent: #2563eb;
--accent-soft: #eff6ff;
--success: #0f766e;
--shadow: 0 14px 40px rgba(15, 23, 42, 0.05);
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.5rem;
--space-6: 2rem;
}
.main-wrapper {
html {
scroll-behavior: smooth;
}
body.app-shell {
background: var(--bg);
color: var(--text);
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 15px;
}
body.rtl {
text-align: right;
}
body.rtl .navbar-brand,
body.rtl .nav-link,
body.rtl .btn,
body.rtl .form-control,
body.rtl .form-select,
body.rtl .form-check,
body.rtl .table {
direction: rtl;
}
::selection {
background: rgba(37, 99, 235, 0.12);
}
.app-navbar {
backdrop-filter: saturate(180%) blur(12px);
}
.navbar-brand {
letter-spacing: -0.02em;
}
.nav-link {
font-weight: 500;
color: var(--muted);
border-radius: var(--radius-sm);
padding: 0.5rem 0.75rem !important;
}
.nav-link.active,
.nav-link:hover,
.nav-link:focus {
color: var(--text);
background: var(--accent-soft);
}
.hero-section {
background: rgba(255,255,255,0.78);
}
.py-lg-6 {
padding-top: 4.5rem;
padding-bottom: 4.5rem;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: var(--muted);
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.display-title,
.section-title {
color: var(--text);
line-height: 1.05;
letter-spacing: -0.03em;
}
.display-title {
font-size: clamp(2.15rem, 5vw, 4rem);
max-width: 12ch;
}
.section-title {
font-size: clamp(1.6rem, 2.8vw, 2.4rem);
}
.lead {
max-width: 62ch;
font-size: 1rem;
}
.panel-card,
.subject-card,
.plan-card,
.workflow-card,
.metric-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
.panel-card,
.subject-card,
.plan-card,
.workflow-card {
padding: 1.35rem;
}
.metric-card {
padding: 1rem 1.1rem;
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.metric-card strong {
font-size: 1.6rem;
line-height: 1;
letter-spacing: -0.04em;
}
.metric-card span,
.metric-card p {
color: var(--muted);
margin: 0;
}
.metric-card.tall {
min-height: 132px;
justify-content: center;
}
.small-stat-grid .metric-card {
min-height: 96px;
}
.mini-tag {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 100vh;
width: 100%;
padding: 20px;
box-sizing: border-box;
position: relative;
z-index: 1;
padding: 0.35rem 0.6rem;
border-radius: 999px;
border: 1px solid var(--border);
background: var(--surface-muted);
color: var(--text);
font-size: 0.78rem;
font-weight: 600;
}
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.chat-container {
width: 100%;
max-width: 600px;
background: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 20px;
.stack-list {
display: flex;
flex-direction: column;
height: 85vh;
box-shadow: 0 20px 40px rgba(0,0,0,0.2);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
overflow: hidden;
gap: 0.8rem;
}
.chat-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
background: rgba(255, 255, 255, 0.5);
font-weight: 700;
font-size: 1.1rem;
.stack-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.9rem 0;
border-bottom: 1px solid var(--border);
}
.stack-item:last-child {
border-bottom: 0;
padding-bottom: 0;
}
.stack-item p {
margin: 0.15rem 0 0;
color: var(--muted);
font-size: 0.92rem;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
align-items: end;
gap: 1.5rem;
flex-wrap: wrap;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
.subject-card,
.plan-card {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
.subject-meta-grid,
.detail-meta-grid,
.pricing-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.8rem;
}
::-webkit-scrollbar-track {
background: transparent;
.subject-meta-grid div,
.detail-meta-grid div,
.pricing-grid div,
.summary-row {
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--surface-muted);
padding: 0.8rem 0.9rem;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
.message {
max-width: 85%;
padding: 0.85rem 1.1rem;
border-radius: 16px;
line-height: 1.5;
font-size: 0.95rem;
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.message.visitor {
align-self: flex-end;
background: linear-gradient(135deg, #212529 0%, #343a40 100%);
color: #fff;
border-bottom-right-radius: 4px;
}
.message.bot {
align-self: flex-start;
background: #ffffff;
color: #212529;
border-bottom-left-radius: 4px;
}
.chat-input-area {
padding: 1.25rem;
background: rgba(255, 255, 255, 0.5);
border-top: 1px solid rgba(0, 0, 0, 0.05);
}
.chat-input-area form {
display: flex;
gap: 0.75rem;
}
.chat-input-area input {
flex: 1;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
padding: 0.75rem 1rem;
outline: none;
background: rgba(255, 255, 255, 0.9);
transition: all 0.3s ease;
}
.chat-input-area input:focus {
border-color: #23a6d5;
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2);
}
.chat-input-area button {
background: #212529;
color: #fff;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
}
.chat-input-area button:hover {
background: #000;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
/* Background Animations */
.bg-animations {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
overflow: hidden;
pointer-events: none;
}
.blob {
position: absolute;
width: 500px;
height: 500px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
filter: blur(80px);
animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1);
}
.blob-1 {
top: -10%;
left: -10%;
background: rgba(238, 119, 82, 0.4);
}
.blob-2 {
bottom: -10%;
right: -10%;
background: rgba(35, 166, 213, 0.4);
animation-delay: -7s;
width: 600px;
height: 600px;
}
.blob-3 {
top: 40%;
left: 30%;
background: rgba(231, 60, 126, 0.3);
animation-delay: -14s;
width: 450px;
height: 450px;
}
@keyframes move {
0% { transform: translate(0, 0) rotate(0deg) scale(1); }
33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); }
66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); }
100% { transform: translate(0, 0) rotate(360deg) scale(1); }
}
.header-link {
font-size: 14px;
color: #fff;
text-decoration: none;
background: rgba(0, 0, 0, 0.2);
padding: 0.5rem 1rem;
border-radius: 8px;
transition: all 0.3s ease;
}
.header-link:hover {
background: rgba(0, 0, 0, 0.4);
text-decoration: none;
}
/* Admin Styles */
.admin-container {
max-width: 900px;
margin: 3rem auto;
padding: 2.5rem;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 24px;
box-shadow: 0 20px 50px rgba(0,0,0,0.15);
border: 1px solid rgba(255, 255, 255, 0.4);
position: relative;
z-index: 1;
}
.admin-container h1 {
margin-top: 0;
color: #212529;
font-weight: 800;
}
.table {
width: 100%;
border-collapse: separate;
border-spacing: 0 8px;
margin-top: 1.5rem;
}
.table th {
background: transparent;
border: none;
padding: 1rem;
color: #6c757d;
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 1px;
}
.table td {
background: #fff;
padding: 1rem;
border: none;
}
.table tr td:first-child { border-radius: 12px 0 0 12px; }
.table tr td:last-child { border-radius: 0 12px 12px 0; }
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
.subject-meta-grid strong,
.detail-meta-grid strong,
.pricing-grid strong,
.summary-row strong {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
margin-bottom: 0.2rem;
font-size: 0.88rem;
}
.subject-meta-grid span,
.detail-meta-grid span,
.pricing-grid span,
.summary-row span {
color: var(--muted);
font-size: 0.9rem;
}
.form-control {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
background: #fff;
transition: all 0.3s ease;
box-sizing: border-box;
}
.form-control:focus {
outline: none;
border-color: #23a6d5;
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
}
.header-container {
.summary-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-links {
display: flex;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.65rem;
}
.admin-card {
background: rgba(255, 255, 255, 0.6);
padding: 2rem;
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.5);
margin-bottom: 2.5rem;
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
.compact-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.admin-card h3 {
margin-top: 0;
margin-bottom: 1.5rem;
.compact-list li {
position: relative;
padding-inline-start: 1.2rem;
color: var(--text);
}
.compact-list li::before {
content: '';
width: 0.42rem;
height: 0.42rem;
border-radius: 999px;
background: var(--accent);
position: absolute;
inset-inline-start: 0;
top: 0.45rem;
}
.compact-list-tight {
gap: 0.5rem;
}
.workflow-card {
min-height: 100%;
}
.step-index {
display: inline-flex;
width: 2rem;
height: 2rem;
align-items: center;
justify-content: center;
border-radius: 999px;
background: var(--accent-soft);
border: 1px solid var(--border);
margin-bottom: 0.85rem;
font-weight: 700;
font-size: 0.82rem;
}
.btn-delete {
background: #dc3545;
color: white;
border: none;
padding: 0.25rem 0.5rem;
border-radius: 4px;
cursor: pointer;
.workflow-card p {
color: var(--muted);
margin-bottom: 0;
}
.btn-add {
background: #212529;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
margin-top: 1rem;
.plan-card.highlighted {
border-color: #cdd6e1;
box-shadow: 0 20px 48px rgba(15, 23, 42, 0.08);
}
.btn-save {
background: #0088cc;
color: white;
border: none;
padding: 0.8rem 1.5rem;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
width: 100%;
transition: all 0.3s ease;
.display-price {
font-size: 2rem;
font-weight: 800;
letter-spacing: -0.04em;
}
.webhook-url {
font-size: 0.85em;
color: #555;
margin-top: 0.5rem;
.catalog-search-wrap {
min-width: min(100%, 320px);
}
.history-table-container {
overflow-x: auto;
background: rgba(255, 255, 255, 0.4);
.empty-filter-state,
.empty-state-inline {
border: 1px dashed var(--border);
border-radius: var(--radius-md);
background: rgba(255,255,255,0.7);
padding: 1rem;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.3);
}
.history-table {
width: 100%;
.dashboard-table thead th {
color: var(--muted);
font-weight: 600;
font-size: 0.84rem;
border-bottom-color: var(--border);
}
.history-table-time {
width: 15%;
white-space: nowrap;
font-size: 0.85em;
color: #555;
.dashboard-table td,
.dashboard-table th {
padding-top: 0.85rem;
padding-bottom: 0.85rem;
}
.history-table-user {
width: 35%;
background: rgba(255, 255, 255, 0.3);
border-radius: 8px;
padding: 8px;
.copy-chip {
border: 1px solid var(--border);
background: var(--surface);
padding: 0.35rem 0.7rem;
border-radius: 999px;
font-size: 0.82rem;
font-weight: 600;
}
.history-table-ai {
width: 50%;
background: rgba(255, 255, 255, 0.5);
border-radius: 8px;
padding: 8px;
.copy-chip:hover,
.copy-chip:focus {
background: var(--accent-soft);
}
.no-messages {
text-align: center;
color: #777;
}
.btn {
border-radius: 10px;
font-weight: 600;
padding: 0.72rem 1rem;
}
.btn-sm {
padding: 0.45rem 0.7rem;
border-radius: 9px;
}
.btn-lg {
padding: 0.8rem 1.15rem;
}
.btn-dark {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.btn-outline-dark {
color: var(--text);
border-color: var(--border);
}
.btn-outline-dark:hover,
.btn-outline-dark:focus {
color: var(--text);
background: var(--accent-soft);
border-color: #c4ced8;
}
.form-control,
.form-select,
.form-check {
border-radius: 10px;
}
.form-control,
.form-select {
min-height: 46px;
border-color: var(--border);
background: #fff;
}
.form-control:focus,
.form-select:focus,
.btn:focus,
.copy-chip:focus,
.nav-link:focus {
box-shadow: 0 0 0 0.2rem rgba(17, 24, 39, 0.12);
border-color: #bcc8d6;
}
.alert,
.toast,
.badge {
border-radius: var(--radius-md);
}
.integration-rail {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.sticky-card {
position: sticky;
top: 88px;
}
footer {
background: rgba(255,255,255,0.86);
}
@media (max-width: 991.98px) {
.sticky-card {
position: static;
}
.subject-meta-grid,
.detail-meta-grid,
.pricing-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 575.98px) {
.panel-card,
.subject-card,
.plan-card,
.workflow-card {
padding: 1.05rem;
}
.display-title {
max-width: 100%;
}
}

View File

@ -1,39 +1,44 @@
document.addEventListener('DOMContentLoaded', () => {
const chatForm = document.getElementById('chat-form');
const chatInput = document.getElementById('chat-input');
const chatMessages = document.getElementById('chat-messages');
document.querySelectorAll('[data-auto-toast]').forEach((element) => {
const toast = new bootstrap.Toast(element, { delay: 3200 });
toast.show();
});
const appendMessage = (text, sender) => {
const msgDiv = document.createElement('div');
msgDiv.classList.add('message', sender);
msgDiv.textContent = text;
chatMessages.appendChild(msgDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
document.querySelectorAll('[data-copy-text]').forEach((button) => {
button.addEventListener('click', async () => {
const text = button.getAttribute('data-copy-text') || '';
try {
await navigator.clipboard.writeText(text);
const original = button.textContent;
button.textContent = 'Copied';
setTimeout(() => {
button.textContent = original;
}, 1200);
} catch (error) {
console.error('Copy failed', error);
}
});
});
const searchInput = document.querySelector('[data-subject-search]');
if (searchInput) {
const items = Array.from(document.querySelectorAll('.subject-grid-item'));
const emptyState = document.querySelector('[data-empty-state]');
const applyFilter = () => {
const query = searchInput.value.trim().toLowerCase();
let visible = 0;
items.forEach((item) => {
const text = item.getAttribute('data-filter-text') || '';
const match = text.includes(query);
item.classList.toggle('d-none', !match);
if (match) visible += 1;
});
if (emptyState) {
emptyState.classList.toggle('d-none', visible !== 0);
}
};
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const message = chatInput.value.trim();
if (!message) return;
appendMessage(message, 'visitor');
chatInput.value = '';
try {
const response = await fetch('api/chat.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
});
const data = await response.json();
// Artificial delay for realism
setTimeout(() => {
appendMessage(data.reply, 'bot');
}, 500);
} catch (error) {
console.error('Error:', error);
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
}
});
searchInput.addEventListener('input', applyFilter);
}
});

53
catalog.php Normal file
View File

@ -0,0 +1,53 @@
<?php
require_once __DIR__ . '/includes/app.php';
$subjects = subjects_catalog();
render_head(
t('Subject catalog', 'كتالوج المواد'),
t('Browse the bilingual subject catalog, teachers, live sessions, and classroom details.', 'تصفح كتالوج المواد والمعلمين والجلسات المباشرة وتفاصيل الفصول باللغتين.')
);
render_nav('catalog.php');
?>
<main class="py-5">
<div class="container">
<div class="section-header mb-4">
<div>
<span class="eyebrow"><?= h(t('Catalog', 'الكتالوج')) ?></span>
<h1 class="section-title mb-2"><?= h(t('Explore subjects and live cohorts', 'استكشف المواد والمجموعات المباشرة')) ?></h1>
<p class="text-secondary mb-0"><?= h(t('Filter the catalog, review teachers, and open a subject detail page with modules and Meet access.', 'قم بتصفية الكتالوج ومراجعة المعلمين وفتح صفحة تفاصيل المادة مع الوحدات والوصول إلى Meet.')) ?></p>
</div>
<div class="catalog-search-wrap">
<label class="form-label small text-secondary mb-1" for="subject-search"><?= h(t('Quick filter', 'تصفية سريعة')) ?></label>
<input id="subject-search" class="form-control" type="search" placeholder="<?= h(t('Search subject or teacher', 'ابحث عن مادة أو معلم')) ?>" data-subject-search>
</div>
</div>
<div class="row g-3" id="subject-grid">
<?php foreach ($subjects as $subject): ?>
<div class="col-lg-6 subject-grid-item" data-filter-text="<?= h(strtolower($subject['title_en'] . ' ' . $subject['title_ar'] . ' ' . $subject['teacher_en'] . ' ' . $subject['teacher_ar'])) ?>">
<article class="subject-card h-100">
<div class="d-flex flex-wrap justify-content-between gap-2 align-items-start mb-3">
<div>
<span class="mini-tag mb-2 d-inline-flex"><?= h(subject_level($subject)) ?></span>
<h2 class="h5 mb-1"><?= h(subject_title($subject)) ?></h2>
<p class="small text-secondary mb-0"><?= h(subject_teacher($subject)) ?></p>
</div>
<span class="small text-secondary"><?= h(subject_duration($subject)) ?></span>
</div>
<p class="text-secondary mb-4"><?= h(subject_summary($subject)) ?></p>
<div class="subject-meta-grid mb-4">
<div><strong><?= h(t('Next live', 'الجلسة القادمة')) ?></strong><span><?= h(subject_next_live($subject)) ?></span></div>
<div><strong><?= h(t('Format', 'النوع')) ?></strong><span><?= h(t('Live + self-paced', 'مباشر + ذاتي')) ?></span></div>
</div>
<a class="btn btn-outline-dark" href="<?= h(app_url('subject.php', ['slug' => $subject['slug']])) ?>"><?= h(t('Open details', 'افتح التفاصيل')) ?></a>
</article>
</div>
<?php endforeach; ?>
</div>
<div class="empty-filter-state d-none" data-empty-state>
<div class="panel-card text-center mt-4">
<h2 class="h5 mb-2"><?= h(t('No subject matched your filter', 'لا توجد مادة تطابق التصفية')) ?></h2>
<p class="text-secondary mb-0"><?= h(t('Try a teacher name or a broader term like English, STEM, or Arabic.', 'جرّب اسم معلم أو مصطلحاً أوسع مثل الإنجليزية أو STEM أو العربية.')) ?></p>
</div>
</div>
</div>
</main>
<?php render_footer(); ?>

146
checkout.php Normal file
View File

@ -0,0 +1,146 @@
<?php
require_once __DIR__ . '/includes/app.php';
$plans = plans_catalog();
$planKey = (string) ($_GET['plan'] ?? $_POST['plan_key'] ?? 'plus');
$plan = get_plan($planKey) ?? get_plan('plus');
$cycle = (string) ($_GET['cycle'] ?? $_POST['billing_cycle'] ?? 'monthly');
$cycle = in_array($cycle, ['monthly', 'yearly'], true) ? $cycle : 'monthly';
$errors = [];
$form = [
'full_name' => trim((string) ($_POST['full_name'] ?? '')),
'email' => trim((string) ($_POST['email'] ?? '')),
'whatsapp' => trim((string) ($_POST['whatsapp'] ?? '')),
'preferred_language' => (string) ($_POST['preferred_language'] ?? current_lang()),
'billing_cycle' => $cycle,
'plan_key' => $plan['key'],
'wablas_opt_in' => isset($_POST['wablas_opt_in']) ? 1 : 0,
];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($form['full_name'] === '' || strlen($form['full_name']) < 2) {
$errors[] = t('Please enter a valid full name.', 'يرجى إدخال اسم كامل صحيح.');
}
if (!filter_var($form['email'], FILTER_VALIDATE_EMAIL)) {
$errors[] = t('Please enter a valid email address.', 'يرجى إدخال بريد إلكتروني صحيح.');
}
if (!preg_match('/^[0-9+\-\s]{8,20}$/', $form['whatsapp'])) {
$errors[] = t('Please enter a valid WhatsApp number.', 'يرجى إدخال رقم واتساب صحيح.');
}
if (!in_array($form['preferred_language'], ['en', 'ar'], true)) {
$errors[] = t('Please select a supported language.', 'يرجى اختيار لغة مدعومة.');
}
if (!$errors) {
$reference = 'THW-' . date('YmdHis') . '-' . random_int(100, 999);
$id = create_subscription([
'full_name' => $form['full_name'],
'email' => $form['email'],
'whatsapp' => $form['whatsapp'],
'preferred_language' => $form['preferred_language'],
'plan_key' => $plan['key'],
'billing_cycle' => $form['billing_cycle'],
'payment_status' => 'active',
'payment_gateway' => 'Thawani',
'thawani_reference' => $reference,
'wablas_opt_in' => $form['wablas_opt_in'],
]);
$_SESSION['subscription_id'] = $id;
$_SESSION['student_email'] = $form['email'];
header('Location: ' . app_url('subscription.php', ['id' => $id, 'created' => 1]));
exit;
}
}
render_head(
t('Checkout', 'الدفع'),
t('Capture a student subscription with bilingual preferences, WhatsApp opt-in, and a Thawani-ready payment reference.', 'التقط اشتراك طالب مع تفضيلات ثنائية اللغة وخيار واتساب ومرجع دفع جاهز لثواني.')
);
render_nav('pricing.php');
?>
<main class="py-5">
<div class="container">
<div class="row g-4">
<div class="col-lg-7">
<div class="panel-card">
<span class="eyebrow"><?= h(t('Checkout flow', 'مسار الدفع')) ?></span>
<h1 class="section-title mb-2"><?= h(t('Create a subscription and unlock the student dashboard', 'أنشئ اشتراكاً وافتح لوحة الطالب')) ?></h1>
<p class="text-secondary mb-4"><?= h(t('This first slice records a real subscription row locally, reserves a Thawani reference, and prepares Wablas reminders.', 'تسجل هذه الشريحة الأولى صف اشتراك حقيقياً محلياً وتحجز مرجع Thawani وتجهز تذكيرات Wablas.')) ?></p>
<?php if ($errors): ?>
<div class="alert alert-danger border">
<ul class="mb-0 ps-3">
<?php foreach ($errors as $error): ?>
<li><?= h($error) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<form method="post" class="row g-3">
<input type="hidden" name="plan_key" value="<?= h($plan['key']) ?>">
<div class="col-md-6">
<label class="form-label" for="full_name"><?= h(t('Full name', 'الاسم الكامل')) ?></label>
<input class="form-control" id="full_name" name="full_name" required value="<?= h($form['full_name']) ?>">
</div>
<div class="col-md-6">
<label class="form-label" for="email"><?= h(t('Email', 'البريد الإلكتروني')) ?></label>
<input class="form-control" id="email" type="email" name="email" required value="<?= h($form['email']) ?>">
</div>
<div class="col-md-6">
<label class="form-label" for="whatsapp"><?= h(t('WhatsApp', 'واتساب')) ?></label>
<input class="form-control" id="whatsapp" name="whatsapp" placeholder="+968 9000 0000" required value="<?= h($form['whatsapp']) ?>">
</div>
<div class="col-md-3">
<label class="form-label" for="preferred_language"><?= h(t('Language', 'اللغة')) ?></label>
<select class="form-select" id="preferred_language" name="preferred_language">
<option value="en" <?= $form['preferred_language'] === 'en' ? 'selected' : '' ?>>English</option>
<option value="ar" <?= $form['preferred_language'] === 'ar' ? 'selected' : '' ?>>العربية</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label" for="billing_cycle"><?= h(t('Billing', 'الدفع الدوري')) ?></label>
<select class="form-select" id="billing_cycle" name="billing_cycle">
<option value="monthly" <?= $form['billing_cycle'] === 'monthly' ? 'selected' : '' ?>><?= h(t('Monthly', 'شهري')) ?></option>
<option value="yearly" <?= $form['billing_cycle'] === 'yearly' ? 'selected' : '' ?>><?= h(t('Yearly', 'سنوي')) ?></option>
</select>
</div>
<div class="col-12">
<div class="form-check border rounded-3 p-3">
<input class="form-check-input" type="checkbox" id="wablas_opt_in" name="wablas_opt_in" value="1" <?= $form['wablas_opt_in'] ? 'checked' : '' ?>>
<label class="form-check-label" for="wablas_opt_in">
<?= h(t('Send payment success and class reminders through Wablas WhatsApp gateway.', 'أرسل نجاح الدفع وتذكيرات الحصص عبر بوابة Wablas لواتساب.')) ?>
</label>
</div>
</div>
<div class="col-12 d-flex flex-wrap gap-2 pt-2">
<button class="btn btn-dark btn-lg" type="submit"><?= h(t('Confirm subscription', 'تأكيد الاشتراك')) ?></button>
<a class="btn btn-outline-dark btn-lg" href="<?= h(app_url('pricing.php')) ?>"><?= h(t('Back to plans', 'العودة إلى الخطط')) ?></a>
</div>
</form>
</div>
</div>
<div class="col-lg-5">
<aside class="panel-card sticky-card">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<span class="eyebrow"><?= h(t('Order summary', 'ملخص الطلب')) ?></span>
<h2 class="h5 mb-1"><?= h(plan_name($plan)) ?></h2>
<p class="small text-secondary mb-0"><?= h(t('Single-platform access', 'وصول إلى منصة موحدة')) ?></p>
</div>
<span class="badge bg-dark-subtle text-dark-emphasis border"><?= h(t('Step 1 MVP', 'النسخة الأولى')) ?></span>
</div>
<div class="summary-row"><span><?= h(t('Billing cycle', 'دورة الدفع')) ?></span><strong><?= h($cycle === 'yearly' ? t('Yearly', 'سنوي') : t('Monthly', 'شهري')) ?></strong></div>
<div class="summary-row"><span><?= h(t('Price', 'السعر')) ?></span><strong data-cycle-price><?= h(price_label($plan, $cycle)) ?></strong></div>
<div class="summary-row"><span><?= h(t('Gateway', 'البوابة')) ?></span><strong>Thawani</strong></div>
<div class="summary-row"><span><?= h(t('Notifications', 'الإشعارات')) ?></span><strong>Wablas</strong></div>
<div class="summary-row"><span><?= h(t('Live rooms', 'الغرف المباشرة')) ?></span><strong>Google Meet</strong></div>
<hr>
<ul class="list-unstyled compact-list compact-list-tight mb-0">
<?php foreach (current_lang() === 'ar' ? $plan['features_ar'] : $plan['features_en'] as $feature): ?>
<li><?= h($feature) ?></li>
<?php endforeach; ?>
</ul>
</aside>
</div>
</div>
</div>
</main>
<?php render_footer(); ?>

121
dashboard.php Normal file
View File

@ -0,0 +1,121 @@
<?php
require_once __DIR__ . '/includes/app.php';
$subscription = current_subscription();
$history = [];
if ($subscription) {
$history = fetch_subscriptions_by_email((string) $subscription['email']);
}
render_head(
t('Student dashboard', 'لوحة الطالب'),
t('Review the active subscription, upcoming live classes, and access to subject detail pages.', 'راجع الاشتراك النشط والفصول المباشرة القادمة والوصول إلى صفحات تفاصيل المواد.')
);
render_nav('dashboard.php');
?>
<main class="py-5">
<div class="container">
<?php if (!$subscription): ?>
<div class="panel-card text-center">
<span class="eyebrow"><?= h(t('Student dashboard', 'لوحة الطالب')) ?></span>
<h1 class="section-title mb-2"><?= h(t('No active subscription yet', 'لا يوجد اشتراك نشط بعد')) ?></h1>
<p class="text-secondary mb-4"><?= h(t('Start with pricing, complete the checkout form, and your personalized classroom dashboard will appear here.', 'ابدأ بالتسعير وأكمل نموذج الدفع وستظهر هنا لوحة الفصول الشخصية الخاصة بك.')) ?></p>
<div class="d-flex justify-content-center flex-wrap gap-2">
<a class="btn btn-dark" href="<?= h(app_url('pricing.php')) ?>"><?= h(t('Choose a plan', 'اختر خطة')) ?></a>
<a class="btn btn-outline-dark" href="<?= h(app_url('catalog.php')) ?>"><?= h(t('Browse catalog', 'تصفح الكتالوج')) ?></a>
</div>
</div>
<?php else: ?>
<?php $plan = get_plan($subscription['plan_key']) ?? get_plan('plus'); ?>
<div class="row g-4">
<div class="col-lg-8">
<div class="panel-card mb-4">
<div class="d-flex flex-wrap justify-content-between gap-3 align-items-start">
<div>
<span class="eyebrow"><?= h(t('Active subscription', 'الاشتراك النشط')) ?></span>
<h1 class="section-title mb-2"><?= h(t('Welcome back, ', 'مرحباً بعودتك، ') . $subscription['full_name']) ?></h1>
<p class="text-secondary mb-0"><?= h(t('Your plan is active and your next live classrooms are listed below.', 'خطتك نشطة وتم إدراج الفصول المباشرة القادمة أدناه.')) ?></p>
</div>
<span class="badge bg-success-subtle text-success-emphasis border border-success-subtle px-3 py-2"><?= h(plan_name($plan)) ?></span>
</div>
</div>
<div class="row g-3 mb-4">
<?php $unlocked = array_slice(subjects_catalog(), 0, $plan['subjects_limit'] >= 999 ? 4 : $plan['subjects_limit']); ?>
<?php foreach ($unlocked as $subject): ?>
<?php
$modules = subject_modules($subject);
$totalModules = count($modules);
$completed = count(get_completed_modules((int) $subscription['id'], $subject['slug']));
$progress = $totalModules > 0 ? round(($completed / $totalModules) * 100) : 0;
?>
<div class="col-md-6">
<article class="subject-card h-100 d-flex flex-column">
<div class="d-flex justify-content-between align-items-start gap-3 mb-2">
<div>
<h2 class="h6 mb-1"><?= h(subject_title($subject)) ?></h2>
<p class="small text-secondary mb-0"><?= h(subject_teacher($subject)) ?></p>
</div>
<span class="mini-tag"><?= h(t('Unlocked', 'مفتوحة')) ?></span>
</div>
<div class="mb-3">
<p class="text-secondary small mb-1"><?= h(t('Progress:', 'التقدم:')) ?> <?= h((string) $progress) ?>%</p>
<div class="progress" style="height: 6px;">
<div class="progress-bar bg-dark" role="progressbar" style="width: <?= h((string) $progress) ?>%;" aria-valuenow="<?= h((string) $progress) ?>" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
<p class="text-secondary small mb-3 flex-grow-1"><?= h(t('Next live:', 'الجلسة المباشرة القادمة:')) ?> <?= h(subject_next_live($subject)) ?></p>
<div class="d-flex gap-2">
<a class="btn btn-sm btn-dark" href="<?= h($subject['meet_url']) ?>" target="_blank" rel="noreferrer"><?= h(t('Meet', 'ميت')) ?></a>
<a class="btn btn-sm btn-outline-dark" href="<?= h(app_url('subject.php', ['slug' => $subject['slug']])) ?>"><?= h(t('Details', 'التفاصيل')) ?></a>
</div>
</article>
</div>
<?php endforeach; ?>
</div>
<div class="panel-card">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="h5 mb-0"><?= h(t('Subscription history', 'سجل الاشتراكات')) ?></h2>
<a class="link-dark text-decoration-none fw-semibold" href="<?= h(app_url('subscription.php', ['id' => (int) $subscription['id']])) ?>"><?= h(t('Open current detail', 'افتح التفاصيل الحالية')) ?></a>
</div>
<div class="table-responsive">
<table class="table align-middle dashboard-table mb-0">
<thead>
<tr>
<th><?= h(t('Reference', 'المرجع')) ?></th>
<th><?= h(t('Plan', 'الخطة')) ?></th>
<th><?= h(t('Created', 'تاريخ الإنشاء')) ?></th>
<th><?= h(t('Action', 'الإجراء')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($history as $item): ?>
<?php $historyPlan = get_plan($item['plan_key']) ?? get_plan('plus'); ?>
<tr>
<td><?= h($item['thawani_reference']) ?></td>
<td><?= h(plan_name($historyPlan)) ?></td>
<td><?= h((string) $item['created_at']) ?></td>
<td><a class="btn btn-sm btn-outline-dark" href="<?= h(app_url('subscription.php', ['id' => (int) $item['id']])) ?>"><?= h(t('View', 'عرض')) ?></a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-lg-4">
<aside class="panel-card sticky-card">
<h2 class="h5 mb-3"><?= h(t('Student profile', 'ملف الطالب')) ?></h2>
<div class="summary-row"><span><?= h(t('Preferred language', 'اللغة المفضلة')) ?></span><strong><?= h($subscription['preferred_language'] === 'ar' ? 'العربية' : 'English') ?></strong></div>
<div class="summary-row"><span><?= h(t('WhatsApp reminders', 'تذكيرات واتساب')) ?></span><strong><?= h((int) $subscription['wablas_opt_in'] === 1 ? t('Enabled', 'مفعلة') : t('Off', 'متوقفة')) ?></strong></div>
<div class="summary-row"><span><?= h(t('Payment gateway', 'بوابة الدفع')) ?></span><strong><?= h($subscription['payment_gateway']) ?></strong></div>
<div class="summary-row"><span><?= h(t('Live classroom', 'الفصل المباشر')) ?></span><strong>Google Meet</strong></div>
<hr>
<div class="d-grid gap-2">
<a class="btn btn-dark" href="<?= h(app_url('catalog.php')) ?>"><?= h(t('Explore more subjects', 'استكشف المزيد من المواد')) ?></a>
<a class="btn btn-outline-dark" href="<?= h(app_url('teacher.php')) ?>"><?= h(t('Open teacher view', 'افتح عرض المعلم')) ?></a>
</div>
</aside>
</div>
</div>
<?php endif; ?>
</div>
</main>
<?php render_footer(); ?>

622
includes/app.php Normal file
View File

@ -0,0 +1,622 @@
<?php
declare(strict_types=1);
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/../db/config.php';
function h(?string $value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
function get_platform_profile(): array {
static $profile = null;
if ($profile === null) {
$stmt = db()->query("SELECT * FROM platform_profile WHERE id = 1");
$profile = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$profile) {
$profile = ['name' => 'LMS Platform', 'description' => '', 'logo_path' => '', 'favicon_path' => ''];
}
}
return $profile;
}
function app_name(): string
{
$prof = get_platform_profile();
if (!empty($prof['name'])) {
return trim($prof['name']);
}
return trim((string) ($_SERVER['PROJECT_NAME'] ?? 'Aula')) ?: 'Aula';
}
function project_description(): string
{
return (string) ($_SERVER['PROJECT_DESCRIPTION'] ?? 'Modern multilingual e-learning classrooms with subscriptions, teachers, students, Google Meet live sessions, Thawani billing, and Wablas notifications.');
}
function current_lang(): string
{
$lang = $_GET['lang'] ?? $_SESSION['lang'] ?? 'en';
$lang = in_array($lang, ['en', 'ar'], true) ? $lang : 'en';
$_SESSION['lang'] = $lang;
return $lang;
}
function is_rtl(): bool
{
return current_lang() === 'ar';
}
function t(string $en, string $ar): string
{
return current_lang() === 'ar' ? $ar : $en;
}
function asset_url(string $path): string
{
$full = __DIR__ . '/../' . ltrim($path, '/');
$version = is_file($full) ? (string) filemtime($full) : (string) time();
return ltrim($path, '/') . '?v=' . $version;
}
function app_url(string $path, array $params = []): string
{
if (!isset($params['lang'])) {
$params['lang'] = current_lang();
}
$query = http_build_query($params);
return $path . ($query ? '?' . $query : '');
}
function page_lang_link(string $lang): string
{
$params = $_GET;
$params['lang'] = $lang;
$query = http_build_query($params);
return basename($_SERVER['PHP_SELF']) . ($query ? '?' . $query : '');
}
function subjects_catalog_static(): array
{
return [
'english-fluency' => [
'slug' => 'english-fluency',
'title_en' => 'English Fluency Studio',
'title_ar' => 'استوديو الطلاقة في الإنجليزية',
'summary_en' => 'Speaking-first classrooms for teens and adults with weekly live workshops.',
'summary_ar' => 'فصول تركّز على المحادثة للمراهقين والبالغين مع ورش مباشرة أسبوعية.',
'teacher_en' => 'Sarah Coleman',
'teacher_ar' => 'سارة كولمان',
'level_en' => 'Intermediate',
'level_ar' => 'متوسط',
'duration_en' => '12 weeks',
'duration_ar' => '12 أسبوعاً',
'next_live_en' => 'Wednesday, April 8, 2026 · 6:00 PM UTC',
'next_live_ar' => 'الأربعاء 8 أبريل 2026 · 6:00 مساءً UTC',
'meet_url' => 'https://meet.google.com/',
'modules_en' => ['Live speaking labs', 'Pronunciation clinic', 'Writing feedback', 'Weekly vocabulary sprints'],
'modules_ar' => ['مختبرات محادثة مباشرة', 'عيادة النطق', 'ملاحظات على الكتابة', 'دفعات مفردات أسبوعية'],
],
'stem-lab' => [
'slug' => 'stem-lab',
'title_en' => 'STEM Lab Foundations',
'title_ar' => 'أساسيات مختبر STEM',
'summary_en' => 'Math and science pathways with assignments, quizzes, and teacher office hours.',
'summary_ar' => 'مسارات في الرياضيات والعلوم مع واجبات واختبارات وساعات مكتبية للمعلم.',
'teacher_en' => 'Omar Al-Harthy',
'teacher_ar' => 'عمر الحارثي',
'level_en' => 'Beginner to Advanced',
'level_ar' => 'من مبتدئ إلى متقدم',
'duration_en' => '16 weeks',
'duration_ar' => '16 أسبوعاً',
'next_live_en' => 'Thursday, April 9, 2026 · 5:00 PM UTC',
'next_live_ar' => 'الخميس 9 أبريل 2026 · 5:00 مساءً UTC',
'meet_url' => 'https://meet.google.com/',
'modules_en' => ['Algebra tracks', 'Physics demos', 'Guided worksheets', 'Lab challenge reviews'],
'modules_ar' => ['مسارات الجبر', 'عروض فيزياء', 'أوراق عمل موجهة', 'مراجعات تحديات المختبر'],
],
'arabic-academy' => [
'slug' => 'arabic-academy',
'title_en' => 'Arabic Language Academy',
'title_ar' => 'أكاديمية اللغة العربية',
'summary_en' => 'Modern Standard Arabic with reading circles and live grammar sessions.',
'summary_ar' => 'العربية الفصحى مع حلقات قراءة وجلسات قواعد مباشرة.',
'teacher_en' => 'Maha Al-Rashdi',
'teacher_ar' => 'مها الراشدي',
'level_en' => 'All levels',
'level_ar' => 'جميع المستويات',
'duration_en' => '10 weeks',
'duration_ar' => '10 أسابيع',
'next_live_en' => 'Saturday, April 11, 2026 · 4:00 PM UTC',
'next_live_ar' => 'السبت 11 أبريل 2026 · 4:00 مساءً UTC',
'meet_url' => 'https://meet.google.com/',
'modules_en' => ['Reading comprehension', 'Live grammar board', 'Listening practice', 'Culture sessions'],
'modules_ar' => ['فهم المقروء', 'سبورة قواعد مباشرة', 'تدريب الاستماع', 'جلسات ثقافية'],
],
'design-systems' => [
'slug' => 'design-systems',
'title_en' => 'Design Systems for Creators',
'title_ar' => 'أنظمة التصميم للمبدعين',
'summary_en' => 'UI foundations, product thinking, and portfolio reviews in live cohorts.',
'summary_ar' => 'أساسيات الواجهات والتفكير المنتج ومراجعات المحافظ ضمن مجموعات مباشرة.',
'teacher_en' => 'Lina Mercer',
'teacher_ar' => 'لينا ميرسر',
'level_en' => 'Professional',
'level_ar' => 'احترافي',
'duration_en' => '8 weeks',
'duration_ar' => '8 أسابيع',
'next_live_en' => 'Monday, April 13, 2026 · 7:00 PM UTC',
'next_live_ar' => 'الإثنين 13 أبريل 2026 · 7:00 مساءً UTC',
'meet_url' => 'https://meet.google.com/',
'modules_en' => ['UI critique rooms', 'Systems thinking', 'Component audits', 'Portfolio feedback'],
'modules_ar' => ['غرف نقد الواجهات', 'تفكير الأنظمة', 'تدقيق المكونات', 'ملاحظات على المحفظة'],
],
];
}
function plans_catalog_static(): array
{
return [
'core' => [
'key' => 'core',
'name_en' => 'Core Plan',
'name_ar' => 'الخطة الأساسية',
'price_monthly' => 29,
'price_yearly' => 290,
'subjects_limit' => 2,
'features_en' => ['Access to 2 subjects', 'Weekly live classroom', 'Student dashboard', 'Thawani-ready billing'],
'features_ar' => ['الوصول إلى مادتين', 'فصل مباشر أسبوعي', 'لوحة الطالب', 'دفع جاهز لثواني'],
],
'plus' => [
'key' => 'plus',
'name_en' => 'Plus Plan',
'name_ar' => 'الخطة المتقدمة',
'price_monthly' => 59,
'price_yearly' => 590,
'subjects_limit' => 4,
'features_en' => ['All subjects', 'Unlimited live rooms', 'Teacher Q&A', 'Wablas reminders'],
'features_ar' => ['جميع المواد', 'غرف مباشرة غير محدودة', 'أسئلة وأجوبة مع المعلم', 'تذكيرات عبر وابلاس'],
],
'pro' => [
'key' => 'pro',
'name_en' => 'Pro Campus',
'name_ar' => 'الخطة الاحترافية',
'price_monthly' => 89,
'price_yearly' => 890,
'subjects_limit' => 999,
'features_en' => ['Priority support', 'Arabic + English tracks', 'Admin reporting', 'Live classroom operations'],
'features_ar' => ['دعم أولوية', 'مسارات عربية وإنجليزية', 'تقارير إدارية', 'تشغيل الفصول المباشرة'],
],
];
}
function ensure_catalog_tables(): void
{
db()->exec("
CREATE TABLE IF NOT EXISTS subjects (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(100) UNIQUE NOT NULL,
title_en VARCHAR(255) NOT NULL,
title_ar VARCHAR(255) NOT NULL,
summary_en TEXT,
summary_ar TEXT,
teacher_en VARCHAR(120),
teacher_ar VARCHAR(120),
level_en VARCHAR(50),
level_ar VARCHAR(50),
duration_en VARCHAR(50),
duration_ar VARCHAR(50),
next_live_en VARCHAR(120),
next_live_ar VARCHAR(120),
meet_url VARCHAR(255),
modules_en JSON,
modules_ar JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS plans (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
plan_key VARCHAR(50) UNIQUE NOT NULL,
name_en VARCHAR(120) NOT NULL,
name_ar VARCHAR(120) NOT NULL,
price_monthly DECIMAL(10,3) NOT NULL,
price_yearly DECIMAL(10,3) NOT NULL,
subjects_limit INT DEFAULT 1,
features_en JSON,
features_ar JSON
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
$count = db()->query("SELECT COUNT(*) FROM subjects")->fetchColumn();
if ($count == 0) {
$subjects = subjects_catalog_static();
$stmt = db()->prepare("INSERT INTO subjects (slug, title_en, title_ar, summary_en, summary_ar, teacher_en, teacher_ar, level_en, level_ar, duration_en, duration_ar, next_live_en, next_live_ar, meet_url, modules_en, modules_ar) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
foreach ($subjects as $s) {
$stmt->execute([
$s['slug'], $s['title_en'], $s['title_ar'], $s['summary_en'], $s['summary_ar'], $s['teacher_en'], $s['teacher_ar'], $s['level_en'], $s['level_ar'], $s['duration_en'], $s['duration_ar'], $s['next_live_en'], $s['next_live_ar'], $s['meet_url'], json_encode($s['modules_en']), json_encode($s['modules_ar'])
]);
}
}
$count = db()->query("SELECT COUNT(*) FROM plans")->fetchColumn();
if ($count == 0) {
$plans = plans_catalog_static();
$stmt = db()->prepare("INSERT INTO plans (plan_key, name_en, name_ar, price_monthly, price_yearly, subjects_limit, features_en, features_ar) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
foreach ($plans as $p) {
$stmt->execute([
$p['key'], $p['name_en'], $p['name_ar'], $p['price_monthly'], $p['price_yearly'], $p['subjects_limit'], json_encode($p['features_en']), json_encode($p['features_ar'])
]);
}
}
}
function subjects_catalog(): array
{
ensure_catalog_tables();
$stmt = db()->query('SELECT * FROM subjects');
$rows = $stmt->fetchAll();
$result = [];
foreach ($rows as $row) {
$row['modules_en'] = json_decode($row['modules_en'], true) ?: [];
$row['modules_ar'] = json_decode($row['modules_ar'], true) ?: [];
$result[$row['slug']] = $row;
}
return $result;
}
function plans_catalog(): array
{
ensure_catalog_tables();
$stmt = db()->query('SELECT * FROM plans');
$rows = $stmt->fetchAll();
$result = [];
foreach ($rows as $row) {
$row['key'] = $row['plan_key'];
$row['features_en'] = json_decode($row['features_en'], true) ?: [];
$row['features_ar'] = json_decode($row['features_ar'], true) ?: [];
$result[$row['key']] = $row;
}
return $result;
}
function ensure_progress_table(): void
{
db()->exec("
CREATE TABLE IF NOT EXISTS module_progress (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
subscription_id INT UNSIGNED NOT NULL,
subject_slug VARCHAR(100) NOT NULL,
module_index INT UNSIGNED NOT NULL,
completed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY(subscription_id, subject_slug, module_index)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
}
function get_completed_modules(int $subscriptionId, string $subjectSlug): array
{
ensure_progress_table();
$stmt = db()->prepare("SELECT module_index FROM module_progress WHERE subscription_id = ? AND subject_slug = ?");
$stmt->execute([$subscriptionId, $subjectSlug]);
return $stmt->fetchAll(PDO::FETCH_COLUMN) ?: [];
}
function toggle_module_progress(int $subscriptionId, string $subjectSlug, int $moduleIndex, bool $completed): void
{
ensure_progress_table();
if ($completed) {
$stmt = db()->prepare("INSERT IGNORE INTO module_progress (subscription_id, subject_slug, module_index) VALUES (?, ?, ?)");
$stmt->execute([$subscriptionId, $subjectSlug, $moduleIndex]);
} else {
$stmt = db()->prepare("DELETE FROM module_progress WHERE subscription_id = ? AND subject_slug = ? AND module_index = ?");
$stmt->execute([$subscriptionId, $subjectSlug, $moduleIndex]);
}
}
function get_subject(string $slug): ?array
{
$subjects = subjects_catalog();
return $subjects[$slug] ?? null;
}
function get_plan(string $key): ?array
{
$plans = plans_catalog();
return $plans[$key] ?? null;
}
function plan_name(array $plan): string
{
return current_lang() === 'ar' ? $plan['name_ar'] : $plan['name_en'];
}
function subject_title(array $subject): string
{
return current_lang() === 'ar' ? $subject['title_ar'] : $subject['title_en'];
}
function subject_summary(array $subject): string
{
return current_lang() === 'ar' ? $subject['summary_ar'] : $subject['summary_en'];
}
function subject_teacher(array $subject): string
{
return current_lang() === 'ar' ? $subject['teacher_ar'] : $subject['teacher_en'];
}
function subject_level(array $subject): string
{
return current_lang() === 'ar' ? $subject['level_ar'] : $subject['level_en'];
}
function subject_duration(array $subject): string
{
return current_lang() === 'ar' ? $subject['duration_ar'] : $subject['duration_en'];
}
function subject_next_live(array $subject): string
{
return current_lang() === 'ar' ? $subject['next_live_ar'] : $subject['next_live_en'];
}
function subject_modules(array $subject): array
{
return current_lang() === 'ar' ? $subject['modules_ar'] : $subject['modules_en'];
}
function price_label(array $plan, string $cycle = 'monthly'): string
{
$amount = $cycle === 'yearly' ? $plan['price_yearly'] : $plan['price_monthly'];
$suffix = $cycle === 'yearly' ? t('/year', '/سنة') : t('/month', '/شهر');
return number_format((float)$amount, 3) . ' ' . t('OMR', 'ر.ع.') . $suffix;
}
function ensure_subscription_table(): void
{
db()->exec(
"CREATE TABLE IF NOT EXISTS student_subscriptions (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
full_name VARCHAR(120) NOT NULL,
email VARCHAR(190) NOT NULL,
whatsapp VARCHAR(40) NOT NULL,
preferred_language VARCHAR(5) NOT NULL DEFAULT 'en',
plan_key VARCHAR(20) NOT NULL,
billing_cycle VARCHAR(20) NOT NULL DEFAULT 'monthly',
payment_status VARCHAR(20) NOT NULL DEFAULT 'active',
payment_gateway VARCHAR(30) NOT NULL DEFAULT 'Thawani',
thawani_reference VARCHAR(80) NOT NULL,
wablas_opt_in TINYINT(1) NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
);
}
function create_subscription(array $data): int
{
ensure_subscription_table();
$stmt = db()->prepare(
'INSERT INTO student_subscriptions
(full_name, email, whatsapp, preferred_language, plan_key, billing_cycle, payment_status, payment_gateway, thawani_reference, wablas_opt_in)
VALUES (:full_name, :email, :whatsapp, :preferred_language, :plan_key, :billing_cycle, :payment_status, :payment_gateway, :thawani_reference, :wablas_opt_in)'
);
$stmt->bindValue(':full_name', $data['full_name']);
$stmt->bindValue(':email', $data['email']);
$stmt->bindValue(':whatsapp', $data['whatsapp']);
$stmt->bindValue(':preferred_language', $data['preferred_language']);
$stmt->bindValue(':plan_key', $data['plan_key']);
$stmt->bindValue(':billing_cycle', $data['billing_cycle']);
$stmt->bindValue(':payment_status', $data['payment_status']);
$stmt->bindValue(':payment_gateway', $data['payment_gateway']);
$stmt->bindValue(':thawani_reference', $data['thawani_reference']);
$stmt->bindValue(':wablas_opt_in', $data['wablas_opt_in'], PDO::PARAM_INT);
$stmt->execute();
return (int) db()->lastInsertId();
}
function fetch_subscription(int $id): ?array
{
ensure_subscription_table();
$stmt = db()->prepare('SELECT * FROM student_subscriptions WHERE id = :id LIMIT 1');
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$row = $stmt->fetch();
return $row ?: null;
}
function fetch_subscriptions_by_email(string $email): array
{
ensure_subscription_table();
$stmt = db()->prepare('SELECT * FROM student_subscriptions WHERE email = :email ORDER BY created_at DESC');
$stmt->bindValue(':email', $email);
$stmt->execute();
return $stmt->fetchAll();
}
function fetch_recent_subscriptions(int $limit = 8): array
{
ensure_subscription_table();
$stmt = db()->prepare('SELECT * FROM student_subscriptions ORDER BY created_at DESC LIMIT :limit');
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll();
}
function subscription_metrics(): array
{
ensure_subscription_table();
$metrics = [
'total' => 0,
'arabic' => 0,
'active' => 0,
'latest' => null,
];
$totals = db()->query("SELECT
COUNT(*) AS total,
SUM(preferred_language = 'ar') AS arabic,
SUM(payment_status = 'active') AS active
FROM student_subscriptions")->fetch();
if ($totals) {
$metrics['total'] = (int) ($totals['total'] ?? 0);
$metrics['arabic'] = (int) ($totals['arabic'] ?? 0);
$metrics['active'] = (int) ($totals['active'] ?? 0);
}
$latest = db()->query('SELECT * FROM student_subscriptions ORDER BY created_at DESC LIMIT 1')->fetch();
$metrics['latest'] = $latest ?: null;
return $metrics;
}
function current_subscription(): ?array
{
$id = isset($_GET['subscription_id']) ? (int) $_GET['subscription_id'] : (int) ($_SESSION['subscription_id'] ?? 0);
if ($id > 0) {
$row = fetch_subscription($id);
if ($row) {
$_SESSION['subscription_id'] = (int) $row['id'];
$_SESSION['student_email'] = $row['email'];
return $row;
}
}
$email = (string) ($_SESSION['student_email'] ?? '');
if ($email !== '') {
$rows = fetch_subscriptions_by_email($email);
if ($rows) {
$_SESSION['subscription_id'] = (int) $rows[0]['id'];
return $rows[0];
}
}
return null;
}
function page_title(string $title): string
{
return $title . ' · ' . app_name();
}
function render_head(string $title, string $description = ''): void
{
$pageDescription = $description !== '' ? $description : project_description();
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<!doctype html>
<html lang="<?= h(current_lang()) ?>" dir="<?= is_rtl() ? 'rtl' : 'ltr' ?>">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= h(page_title($title)) ?></title>
<?php
$prof = get_platform_profile();
if (!empty($prof['favicon_path'])):
?>
<link rel="icon" href="<?= h(asset_url($prof['favicon_path'])) ?>" />
<?php endif; ?>
<meta name="description" content="<?= h($pageDescription) ?>" />
<?php if ($projectDescription): ?>
<meta property="og:description" content="<?= h($projectDescription) ?>" />
<meta property="twitter:description" content="<?= h($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<meta property="og:image" content="<?= h($projectImageUrl) ?>" />
<meta property="twitter:image" content="<?= h($projectImageUrl) ?>" />
<?php endif; ?>
<meta name="theme-color" content="#111827" />
<meta name="author" content="<?= h(app_name()) ?>" />
<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=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="<?= h(asset_url('assets/css/custom.css')) ?>">
</head>
<body class="app-shell <?= is_rtl() ? 'rtl' : 'ltr' ?>">
<?php
}
function nav_items(): array
{
return [
'index.php' => t('Home', 'الرئيسية'),
'catalog.php' => t('Subjects', 'المواد'),
'pricing.php' => t('Plans', 'الخطط'),
'dashboard.php' => t('Student', 'الطالب'),
'teacher.php' => t('Teacher', 'المعلم'),
'admin.php' => t('Admin', 'الإدارة'),
];
}
function render_nav(string $active = ''): void
{
$items = nav_items();
?>
<nav class="navbar navbar-expand-lg border-bottom bg-white sticky-top app-navbar">
<div class="container">
<a class="navbar-brand fw-semibold d-flex align-items-center" href="<?= h(app_url('index.php')) ?>">
<?php $prof = get_platform_profile(); if (!empty($prof['logo_path'])): ?>
<img src="<?= h(asset_url($prof['logo_path'])) ?>" alt="Logo" style="height: 32px; margin-right: 8px; border-radius: 4px;">
<?php endif; ?>
<span><?= h(app_name()) ?></span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav" aria-controls="mainNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0 gap-lg-2">
<?php foreach ($items as $file => $label): ?>
<li class="nav-item"><a class="nav-link <?= $active === $file ? 'active' : '' ?>" href="<?= h(app_url($file)) ?>"><?= h($label) ?></a></li>
<?php endforeach; ?>
</ul>
<div class="d-flex align-items-center gap-2 ms-lg-3">
<span class="small text-secondary d-none d-md-inline"><?= h(t('Single platform LMS', 'منصة تعليم موحدة')) ?></span>
<div class="btn-group btn-group-sm" role="group" aria-label="Language switcher">
<a class="btn btn-outline-dark <?= current_lang() === 'en' ? 'active' : '' ?>" href="<?= h(page_lang_link('en')) ?>">EN</a>
<a class="btn btn-outline-dark <?= current_lang() === 'ar' ? 'active' : '' ?>" href="<?= h(page_lang_link('ar')) ?>">AR</a>
</div>
</div>
</div>
</div>
</nav>
<?php
}
function render_footer(): void
{
?>
<footer class="border-top bg-white mt-5">
<div class="container py-4 d-flex flex-column flex-lg-row justify-content-between gap-3 small text-secondary">
<div>
<strong class="text-dark"><?= h(app_name()) ?></strong>
<div><?= h(t('Subscription-based classrooms in English and Arabic with Google Meet, Thawani, and Wablas workflows.', 'فصول باشتراكات بالإنجليزية والعربية مع تدفقات عمل Google Meet وThawani وWablas.')) ?></div>
</div>
<div class="d-flex gap-3 flex-wrap">
<a class="text-decoration-none text-secondary" href="<?= h(app_url('catalog.php')) ?>"><?= h(t('Subject catalog', 'كتالوج المواد')) ?></a>
<a class="text-decoration-none text-secondary" href="<?= h(app_url('pricing.php')) ?>"><?= h(t('Plans', 'الخطط')) ?></a>
<a class="text-decoration-none text-secondary" href="<?= h(app_url('dashboard.php')) ?>"><?= h(t('Student dashboard', 'لوحة الطالب')) ?></a>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="<?= h(asset_url('assets/js/main.js')) ?>"></script>
</body>
</html>
<?php
}
function status_badge(string $status): string
{
return match ($status) {
'active' => 'badge bg-success-subtle text-success-emphasis border border-success-subtle',
'pending' => 'badge bg-warning-subtle text-warning-emphasis border border-warning-subtle',
default => 'badge bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle',
};
}

290
index.php
View File

@ -1,150 +1,150 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
require_once __DIR__ . '/includes/app.php';
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
render_head(
t('Modern bilingual classrooms for live online learning', 'فصول حديثة ثنائية اللغة للتعلم المباشر عبر الإنترنت'),
t('Explore a polished LMS landing page with plans, subjects, live classes, and bilingual student journeys.', 'استكشف صفحة هبوط لمنصة تعليمية حديثة تشمل الخطط والمواد والفصول المباشرة وتجربة ثنائية اللغة.')
);
render_nav('index.php');
$subjects = subjects_catalog();
$plans = plans_catalog();
$metrics = ['subjects' => count($subjects), 'teachers' => 12, 'live' => 18];
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>New Style</title>
<?php
// Read project preview data from environment
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<?php if ($projectDescription): ?>
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<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=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
</head>
<body>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
<main>
<section class="hero-section border-bottom">
<div class="container py-5 py-lg-6">
<div class="row align-items-center g-4 g-lg-5">
<div class="col-lg-7">
<span class="eyebrow"><?= h(t('Single platform LMS', 'منصة تعليم موحدة')) ?></span>
<h1 class="display-title mt-3 mb-3"><?= h(t('Subscriptions, multilingual classrooms, and live Google Meet learning in one precise workspace.', 'الاشتراكات والفصول متعددة اللغات والتعلم المباشر عبر Google Meet في مساحة واحدة دقيقة.')) ?></h1>
<p class="lead text-secondary mb-4"><?= h(t('Launch a polished e-learning experience for students, teachers, and admins with English/Arabic support, Thawani billing flows, and Wablas-ready WhatsApp notifications.', 'أطلق تجربة تعليم إلكتروني مصقولة للطلاب والمعلمين والإدارة مع دعم الإنجليزية والعربية وتدفقات دفع ثواني وإشعارات واتساب جاهزة عبر وابلاس.')) ?></p>
<div class="d-flex flex-wrap gap-2 mb-4">
<a class="btn btn-dark btn-lg" href="<?= h(app_url('pricing.php')) ?>"><?= h(t('Start subscription flow', 'ابدأ مسار الاشتراك')) ?></a>
<a class="btn btn-outline-dark btn-lg" href="<?= h(app_url('catalog.php')) ?>"><?= h(t('Browse subjects', 'تصفح المواد')) ?></a>
</div>
<div class="row row-cols-1 row-cols-sm-3 g-3 small-stat-grid">
<div class="col">
<div class="metric-card"><strong><?= h((string) $metrics['subjects']) ?></strong><span><?= h(t('core subjects', 'مواد أساسية')) ?></span></div>
</div>
<div class="col">
<div class="metric-card"><strong><?= h((string) $metrics['teachers']) ?></strong><span><?= h(t('active teachers', 'معلمون نشطون')) ?></span></div>
</div>
<div class="col">
<div class="metric-card"><strong><?= h((string) $metrics['live']) ?></strong><span><?= h(t('live sessions weekly', 'جلسة مباشرة أسبوعياً')) ?></span></div>
</div>
</div>
</div>
<div class="col-lg-5">
<div class="panel-card">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="h5 mb-0"><?= h(t('Today inside the platform', 'اليوم داخل المنصة')) ?></h2>
<span class="badge bg-dark-subtle text-dark-emphasis border"><?= h(t('Live ready', 'جاهز للبث')) ?></span>
</div>
<div class="stack-list">
<div class="stack-item">
<div>
<strong><?= h(t('Student subscription', 'اشتراك الطالب')) ?></strong>
<p><?= h(t('Choose a plan, confirm payment intent, and unlock classrooms.', 'اختر خطة وأكد نية الدفع وافتح الفصول.')) ?></p>
</div>
<span class="mini-tag">Thawani</span>
</div>
<div class="stack-item">
<div>
<strong><?= h(t('Live lessons', 'الدروس المباشرة')) ?></strong>
<p><?= h(t('Weekly Google Meet rooms with schedules shown inside every subject.', 'غرف Google Meet أسبوعية مع الجداول داخل كل مادة.')) ?></p>
</div>
<span class="mini-tag">Meet</span>
</div>
<div class="stack-item">
<div>
<strong><?= h(t('WhatsApp reminders', 'تذكيرات واتساب')) ?></strong>
<p><?= h(t('Opt-in reminders for payment confirmations and class updates.', 'تذكيرات اختيارية لتأكيد الدفع وتحديثات الحصص.')) ?></p>
</div>
<span class="mini-tag">Wablas</span>
</div>
</div>
<a class="btn btn-outline-dark w-100 mt-3" href="<?= h(app_url('dashboard.php')) ?>"><?= h(t('Open student dashboard', 'افتح لوحة الطالب')) ?></a>
</div>
</div>
</div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
</div>
</main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer>
</body>
</html>
</section>
<section class="py-5 border-bottom bg-white">
<div class="container">
<div class="section-header">
<div>
<span class="eyebrow"><?= h(t('Featured subjects', 'المواد المميزة')) ?></span>
<h2 class="section-title"><?= h(t('Separate pages for catalog, detail, checkout, and dashboards.', 'صفحات منفصلة للكتالوج والتفاصيل والدفع ولوحات التحكم.')) ?></h2>
</div>
<a class="link-dark fw-semibold text-decoration-none" href="<?= h(app_url('catalog.php')) ?>"><?= h(t('See all subjects', 'عرض جميع المواد')) ?></a>
</div>
<div class="row g-3 mt-1">
<?php foreach (array_slice($subjects, 0, 3) as $subject): ?>
<div class="col-lg-4">
<article class="subject-card h-100">
<div class="subject-meta d-flex justify-content-between align-items-center mb-3">
<span class="mini-tag"><?= h(subject_level($subject)) ?></span>
<span class="text-secondary small"><?= h(subject_duration($subject)) ?></span>
</div>
<h3 class="h5 mb-2"><?= h(subject_title($subject)) ?></h3>
<p class="text-secondary mb-4"><?= h(subject_summary($subject)) ?></p>
<div class="d-flex justify-content-between align-items-center small text-secondary mt-auto">
<span><?= h(subject_teacher($subject)) ?></span>
<a class="link-dark text-decoration-none fw-semibold" href="<?= h(app_url('subject.php', ['slug' => $subject['slug']])) ?>"><?= h(t('View subject', 'عرض المادة')) ?></a>
</div>
</article>
</div>
<?php endforeach; ?>
</div>
</div>
</section>
<section class="py-5 border-bottom">
<div class="container">
<div class="section-header mb-4">
<div>
<span class="eyebrow"><?= h(t('Delivery flow', 'مسار التسليم')) ?></span>
<h2 class="section-title"><?= h(t('One thin slice from discovery to live access.', 'شريحة رشيقة من الاكتشاف حتى الوصول المباشر.')) ?></h2>
</div>
</div>
<div class="row g-3">
<div class="col-md-4"><div class="workflow-card"><span class="step-index">01</span><h3 class="h6"><?= h(t('Browse plans', 'تصفح الخطط')) ?></h3><p><?= h(t('Students compare monthly or yearly plans and pick the right access model.', 'يقارن الطلاب بين الخطط الشهرية والسنوية ويختارون نموذج الوصول المناسب.')) ?></p></div></div>
<div class="col-md-4"><div class="workflow-card"><span class="step-index">02</span><h3 class="h6"><?= h(t('Capture checkout details', 'جمع بيانات الدفع')) ?></h3><p><?= h(t('The checkout form records student identity, language preference, and WhatsApp opt-in.', 'يسجل نموذج الدفع هوية الطالب وتفضيل اللغة وخيار الاشتراك في واتساب.')) ?></p></div></div>
<div class="col-md-4"><div class="workflow-card"><span class="step-index">03</span><h3 class="h6"><?= h(t('Unlock the dashboard', 'فتح لوحة التحكم')) ?></h3><p><?= h(t('A confirmation view leads directly to the student dashboard, live classes, and subject details.', 'تنقل شاشة التأكيد مباشرة إلى لوحة الطالب والفصول المباشرة وتفاصيل المواد.')) ?></p></div></div>
</div>
</div>
</section>
<section class="py-5 bg-white">
<div class="container">
<div class="section-header mb-4">
<div>
<span class="eyebrow"><?= h(t('Plans', 'الخطط')) ?></span>
<h2 class="section-title"><?= h(t('Plan-based access for a shared marketplace.', 'وصول قائم على الخطط لمنصة مشتركة.')) ?></h2>
</div>
<a class="link-dark fw-semibold text-decoration-none" href="<?= h(app_url('pricing.php')) ?>"><?= h(t('Open pricing', 'افتح التسعير')) ?></a>
</div>
<div class="row g-3">
<?php foreach ($plans as $plan): ?>
<div class="col-lg-4">
<div class="plan-card h-100 <?= $plan['key'] === 'plus' ? 'highlighted' : '' ?>">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3 class="h5 mb-0"><?= h(plan_name($plan)) ?></h3>
<?php if ($plan['key'] === 'plus'): ?><span class="mini-tag"><?= h(t('Popular', 'الأكثر اختياراً')) ?></span><?php endif; ?>
</div>
<div class="display-price mb-3"><?= h(price_label($plan)) ?></div>
<ul class="list-unstyled compact-list mb-4">
<?php foreach (current_lang() === 'ar' ? $plan['features_ar'] : $plan['features_en'] as $feature): ?>
<li><?= h($feature) ?></li>
<?php endforeach; ?>
</ul>
<a class="btn <?= $plan['key'] === 'plus' ? 'btn-dark' : 'btn-outline-dark' ?> w-100" href="<?= h(app_url('checkout.php', ['plan' => $plan['key']])) ?>"><?= h(t('Choose plan', 'اختر الخطة')) ?></a>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</section>
</main>
<?php render_footer(); ?>

12
patch_admin_includes.php Normal file
View File

@ -0,0 +1,12 @@
<?php
$content = file_get_contents('admin.php');
$injection = <<<HTML
<?php elseif ($page === 'classes'): ?>
<?php require_once __DIR__ . '/admin_classes.php'; ?>
<?php elseif ($page === 'subjects'): ?>
<?php require_once __DIR__ . '/admin_subjects.php'; ?>
HTML;
$content = str_replace('<?php else: ?>', $injection . "\n <?php else: "), $content);
file_put_contents('admin.php', $content);
echo "Injected.\n";

20
patch_admin_nav.php Normal file
View File

@ -0,0 +1,20 @@
<?php
$content = file_get_contents('admin.php');
$new_nav_item = <<<HTML
<li>
<a href="<?= h(app_url('admin.php', ['page' => 'classes'])) ?>" class="nav-link <?= \$page === 'classes' ? 'active' : 'link-dark' ?>" <?= \$page === 'classes' ? 'style="background-color: var(--accent); color: white;"' : '' ?> >
<?= h(t('Classes', 'الصفوف')) ?>
</a>
</li>
HTML;
$content = str_replace(
'<li>\n <a href="<?= h(app_url(\'admin.php\', [\'page\' => \'subjects\']))',
$new_nav_item . "\n " . '<li>\n <a href="<?= h(app_url(\'admin.php\', [\'page\' => \'subjects\']))',
$content
);
file_put_contents('admin.php', $content);
echo "Nav patched.\n";

51
patch_app.php Normal file
View File

@ -0,0 +1,51 @@
<?php
$content = file_get_contents('includes/app.php');
$helper = <<<'EOD'
function get_platform_profile(): array {
static $profile = null;
if ($profile === null) {
$stmt = db()->query("SELECT * FROM platform_profile WHERE id = 1");
$profile = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$profile) {
$profile = ['name' => 'LMS Platform', 'description' => '', 'logo_path' => '', 'favicon_path' => ''];
}
}
return $profile;
}
EOD;
if (strpos($content, 'function get_platform_profile()') === false) {
$content = str_replace('function app_name(): string', $helper . 'function app_name(): string', $content);
}
// Update app_name() to use profile if exists
$app_name_new = <<<'EOD'
function app_name(): string
{
$prof = get_platform_profile();
if (!empty($prof['name'])) {
return trim($prof['name']);
}
return trim((string) ($_SERVER['PROJECT_NAME'] ?? 'Aula')) ?: 'Aula';
}
EOD;
$content = preg_replace('/function app_name\(\): string\s*\{[^}]+\}/s', $app_name_new, $content);
// Update render_head() to add favicon support
$render_head_old = '<title><?= h(page_title($title)) ?></title>';
$render_head_new = <<<'EOD'
<title><?= h(page_title($title)) ?></title>
<?php
$prof = get_platform_profile();
if (!empty($prof['favicon_path'])):
?>
<link rel="icon" href="<?= h(asset_url($prof['favicon_path'])) ?>" />
<?php endif; ?>
EOD;
$content = str_replace($render_head_old, $render_head_new, $content);
file_put_contents('includes/app.php', $content);
echo "Patched includes/app.php\n";

27
patch_db2.php Normal file
View File

@ -0,0 +1,27 @@
<?php
require_once __DIR__ . '/includes/app.php';
try {
db()->exec("
CREATE TABLE IF NOT EXISTS platform_profile (
id INT PRIMARY KEY DEFAULT 1,
name VARCHAR(255),
description TEXT,
logo_path VARCHAR(255),
favicon_path VARCHAR(255)
)
");
$check = db()->query("SELECT COUNT(*) FROM platform_profile")->fetchColumn();
if ($check == 0) {
$stmt = db()->prepare("INSERT INTO platform_profile (id, name, description, logo_path, favicon_path) VALUES (1, :name, :desc, '', '')");
$stmt->execute([
'name' => 'LMS Platform',
'desc' => 'Your trusted bilingual learning management system.'
]);
}
echo "Platform profile DB patched.\n";
} catch (Exception $e) {
echo "Error: " . $e->getMessage();
}

22
patch_db_classes.php Normal file
View File

@ -0,0 +1,22 @@
<?php
require_once __DIR__ . '/includes/app.php';
db()->exec("
CREATE TABLE IF NOT EXISTS classes (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name_en VARCHAR(255) NOT NULL,
name_ar VARCHAR(255) NOT NULL,
description_en TEXT,
description_ar TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
try {
db()->exec("ALTER TABLE subjects ADD COLUMN class_id INT UNSIGNED DEFAULT NULL;");
} catch (Exception $e) {
// Column might exist
}
echo "Database updated.\n";

16
patch_nav.php Normal file
View File

@ -0,0 +1,16 @@
<?php
$content = file_get_contents('includes/app.php');
$nav_old = '<a class="navbar-brand fw-semibold" href="<?= h(app_url(\'index.php\')) ?>"><?= h(app_name()) ?></a>';
$nav_new = <<<'EOD'
<a class="navbar-brand fw-semibold d-flex align-items-center" href="<?= h(app_url('index.php')) ?>">
<?php $prof = get_platform_profile(); if (!empty($prof['logo_path'])): ?>
<img src="<?= h(asset_url($prof['logo_path'])) ?>" alt="Logo" style="height: 32px; margin-right: 8px; border-radius: 4px;">
<?php endif; ?>
<span><?= h(app_name()) ?></span>
</a>
EOD;
$content = str_replace($nav_old, $nav_new, $content);
file_put_contents('includes/app.php', $content);
echo "Patched navbar\n";

54
pricing.php Normal file
View File

@ -0,0 +1,54 @@
<?php
require_once __DIR__ . '/includes/app.php';
$plans = plans_catalog();
render_head(
t('Subscription plans', 'خطط الاشتراك'),
t('Compare plan-based access for students, teachers, and live classrooms.', 'قارن بين الوصول المعتمد على الخطط للطلاب والمعلمين والفصول المباشرة.')
);
render_nav('pricing.php');
?>
<main class="py-5">
<div class="container">
<div class="section-header mb-4">
<div>
<span class="eyebrow"><?= h(t('Pricing', 'التسعير')) ?></span>
<h1 class="section-title mb-2"><?= h(t('Choose the plan that unlocks your classroom experience', 'اختر الخطة التي تفتح تجربة الفصل لديك')) ?></h1>
<p class="text-secondary mb-0"><?= h(t('Plan-based subscriptions work across the entire marketplace, with bilingual delivery and live sessions included.', 'تعمل الاشتراكات المعتمدة على الخطط عبر المنصة كاملة مع تقديم ثنائي اللغة وجلسات مباشرة مشمولة.')) ?></p>
</div>
<div class="integration-rail">
<span class="mini-tag">Thawani</span>
<span class="mini-tag">Wablas</span>
<span class="mini-tag">Google Meet</span>
</div>
</div>
<div class="row g-3">
<?php foreach ($plans as $plan): ?>
<div class="col-lg-4">
<div class="plan-card h-100 <?= $plan['key'] === 'plus' ? 'highlighted' : '' ?>">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h2 class="h5 mb-1"><?= h(plan_name($plan)) ?></h2>
<p class="small text-secondary mb-0"><?= h(t('Shared marketplace access', 'وصول إلى منصة مشتركة')) ?></p>
</div>
<?php if ($plan['key'] === 'plus'): ?><span class="mini-tag"><?= h(t('Best fit', 'الأفضل')) ?></span><?php endif; ?>
</div>
<div class="pricing-grid mb-3">
<div><strong><?= h(price_label($plan, 'monthly')) ?></strong><span><?= h(t('Monthly', 'شهري')) ?></span></div>
<div><strong><?= h(price_label($plan, 'yearly')) ?></strong><span><?= h(t('Yearly', 'سنوي')) ?></span></div>
</div>
<ul class="list-unstyled compact-list mb-4">
<?php foreach (current_lang() === 'ar' ? $plan['features_ar'] : $plan['features_en'] as $feature): ?>
<li><?= h($feature) ?></li>
<?php endforeach; ?>
</ul>
<div class="d-grid gap-2">
<a class="btn btn-dark" href="<?= h(app_url('checkout.php', ['plan' => $plan['key'], 'cycle' => 'monthly'])) ?>"><?= h(t('Start monthly', 'ابدأ شهرياً')) ?></a>
<a class="btn btn-outline-dark" href="<?= h(app_url('checkout.php', ['plan' => $plan['key'], 'cycle' => 'yearly'])) ?>"><?= h(t('Start yearly', 'ابدأ سنوياً')) ?></a>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</main>
<?php render_footer(); ?>

93
subject.php Normal file
View File

@ -0,0 +1,93 @@
<?php
require_once __DIR__ . '/includes/app.php';
$slug = (string) ($_GET['slug'] ?? '');
$subject = get_subject($slug);
$subscription = current_subscription();
$hasAccess = $subscription !== null;
if (!$subject) {
header('Location: ' . app_url('catalog.php'));
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $hasAccess && isset($_POST['action']) && $_POST['action'] === 'toggle_module') {
$moduleIndex = (int) $_POST['module_index'];
$completed = (int) ($_POST['completed'] ?? 0) === 1;
toggle_module_progress((int) $subscription['id'], $slug, $moduleIndex, $completed);
header('Location: ' . app_url('subject.php', ['slug' => $slug]));
exit;
}
$completedModules = $hasAccess ? get_completed_modules((int) $subscription['id'], $slug) : [];
render_head(subject_title($subject), subject_summary($subject));
render_nav('catalog.php');
?>
<main class="py-5">
<div class="container">
<div class="row g-4">
<div class="col-lg-8">
<div class="panel-card">
<div class="mb-4">
<a class="text-secondary text-decoration-none small d-inline-flex align-items-center mb-3" href="<?= h(app_url('catalog.php')) ?>">&larr; <?= h(t('Back to catalog', 'العودة للكتالوج')) ?></a>
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h1 class="section-title mb-2"><?= h(subject_title($subject)) ?></h1>
<p class="text-secondary mb-0"><?= h(subject_summary($subject)) ?></p>
</div>
</div>
<span class="mini-tag"><?= h(subject_level($subject)) ?></span>
</div>
<div class="detail-meta-grid mb-5">
<div><strong><?= h(t('Teacher', 'المعلم')) ?></strong><span><?= h(subject_teacher($subject)) ?></span></div>
<div><strong><?= h(t('Duration', 'المدة')) ?></strong><span><?= h(subject_duration($subject)) ?></span></div>
<?php if ($hasAccess): ?>
<div><strong><?= h(t('Live classroom', 'الفصل المباشر')) ?></strong><span><?= h(subject_next_live($subject)) ?></span></div>
<?php endif; ?>
</div>
<h2 class="h5 mb-3"><?= h(t('Modules & Curriculum', 'الوحدات والمنهج')) ?></h2>
<ul class="list-unstyled compact-list mb-0">
<?php foreach (subject_modules($subject) as $index => $module): ?>
<li class="d-flex align-items-center justify-content-between">
<span><?= h($module) ?></span>
<?php if ($hasAccess): ?>
<?php $isCompleted = in_array($index, $completedModules, false); ?>
<form method="post" class="m-0 p-0">
<input type="hidden" name="action" value="toggle_module">
<input type="hidden" name="module_index" value="<?= h((string) $index) ?>">
<input type="hidden" name="completed" value="<?= $isCompleted ? '0' : '1' ?>">
<button type="submit" class="btn btn-sm <?= $isCompleted ? 'btn-success' : 'btn-outline-secondary' ?>">
<?= $isCompleted ? h(t('Completed', 'مكتمل')) : h(t('Mark complete', 'تحديد كمكتمل')) ?>
</button>
</form>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</div>
</div>
<div class="col-lg-4">
<aside class="panel-card sticky-card text-center">
<?php if ($hasAccess): ?>
<div class="mb-3">
<span class="badge bg-success-subtle text-success-emphasis border border-success-subtle px-3 py-2"><?= h(t('Access granted', 'صلاحية الدخول ممنوحة')) ?></span>
</div>
<p class="text-secondary small mb-4"><?= h(t('You can enter the live classroom when the session begins.', 'يمكنك دخول الفصل المباشر عند بدء الجلسة.')) ?></p>
<div class="d-grid gap-2">
<a class="btn btn-dark" href="<?= h($subject['meet_url']) ?>" target="_blank" rel="noreferrer"><?= h(t('Open Google Meet lobby', 'افتح صالة Google Meet')) ?></a>
<a class="btn btn-outline-dark" href="<?= h(app_url('dashboard.php', ['subscription_id' => (int) $subscription['id']])) ?>"><?= h(t('Go to dashboard', 'اذهب إلى لوحة التحكم')) ?></a>
</div>
<?php else: ?>
<div class="mb-3">
<span class="badge bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle px-3 py-2"><?= h(t('Subscription required', 'يلزم الاشتراك')) ?></span>
</div>
<p class="text-secondary small mb-4"><?= h(t('Subscribe to a plan to unlock live classrooms, module materials, and teacher office hours.', 'اشترك في خطة لفتح الفصول المباشرة ومواد الوحدات والساعات المكتبية للمعلم.')) ?></p>
<a class="btn btn-dark w-100" href="<?= h(app_url('pricing.php')) ?>"><?= h(t('Choose a plan', 'اختر خطة')) ?></a>
<?php endif; ?>
</aside>
</div>
</div>
</div>
</main>
<?php render_footer(); ?>

85
subscription.php Normal file
View File

@ -0,0 +1,85 @@
<?php
require_once __DIR__ . '/includes/app.php';
$id = (int) ($_GET['id'] ?? 0);
$subscription = $id > 0 ? fetch_subscription($id) : current_subscription();
if (!$subscription) {
render_head(t('Subscription not found', 'الاشتراك غير موجود'));
render_nav('dashboard.php');
?>
<main class="py-5"><div class="container"><div class="alert alert-warning"><?= h(t('The subscription detail could not be found.', 'تعذر العثور على تفاصيل الاشتراك.')) ?></div></div></main>
<?php
render_footer();
exit;
}
$_SESSION['subscription_id'] = (int) $subscription['id'];
$_SESSION['student_email'] = $subscription['email'];
$plan = get_plan($subscription['plan_key']) ?? get_plan('plus');
$created = isset($_GET['created']);
$subjects = array_slice(subjects_catalog(), 0, $plan['subjects_limit'] >= 999 ? 4 : $plan['subjects_limit']);
render_head(
t('Subscription confirmation', 'تأكيد الاشتراك'),
t('Review the new subscription, payment reference, and the first live classrooms unlocked for the student.', 'راجع الاشتراك الجديد ومرجع الدفع وأول الفصول المباشرة التي تم فتحها للطالب.')
);
render_nav('dashboard.php');
?>
<main class="py-5">
<div class="container">
<?php if ($created): ?>
<div class="toast-container position-fixed top-0 end-0 p-3">
<div class="toast align-items-center text-bg-dark border-0" role="status" aria-live="polite" aria-atomic="true" data-auto-toast>
<div class="d-flex">
<div class="toast-body"><?= h(t('Subscription created. The dashboard and live access are now ready.', 'تم إنشاء الاشتراك. أصبحت لوحة التحكم والوصول المباشر جاهزين الآن.')) ?></div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
</div>
<?php endif; ?>
<div class="row g-4">
<div class="col-lg-7">
<div class="panel-card h-100">
<span class="eyebrow"><?= h(t('Confirmation', 'التأكيد')) ?></span>
<h1 class="section-title mb-3"><?= h(t('Your subscription is active', 'اشتراكك نشط')) ?></h1>
<div class="alert alert-success border mb-4">
<strong><?= h(t('Plan activated successfully.', 'تم تفعيل الخطة بنجاح.')) ?></strong>
<div class="small mt-1"><?= h(t('This record was saved in the database and linked to your student dashboard.', 'تم حفظ هذا السجل في قاعدة البيانات وربطه بلوحة الطالب الخاصة بك.')) ?></div>
</div>
<div class="detail-meta-grid mb-4">
<div><strong><?= h(t('Student', 'الطالب')) ?></strong><span><?= h($subscription['full_name']) ?></span></div>
<div><strong><?= h(t('Email', 'البريد الإلكتروني')) ?></strong><span><?= h($subscription['email']) ?></span></div>
<div><strong><?= h(t('Plan', 'الخطة')) ?></strong><span><?= h(plan_name($plan)) ?></span></div>
<div><strong><?= h(t('Status', 'الحالة')) ?></strong><span class="<?= h(status_badge((string) $subscription['payment_status'])) ?>"><?= h(ucfirst((string) $subscription['payment_status'])) ?></span></div>
<div><strong><?= h(t('Thawani ref', 'مرجع ثواني')) ?></strong><span><button type="button" class="copy-chip" data-copy-text="<?= h($subscription['thawani_reference']) ?>"><?= h($subscription['thawani_reference']) ?></button></span></div>
<div><strong><?= h(t('WhatsApp', 'واتساب')) ?></strong><span><?= h($subscription['whatsapp']) ?></span></div>
</div>
<div class="d-flex flex-wrap gap-2">
<a class="btn btn-dark" href="<?= h(app_url('dashboard.php', ['subscription_id' => (int) $subscription['id']])) ?>"><?= h(t('Go to dashboard', 'انتقل إلى لوحة التحكم')) ?></a>
<a class="btn btn-outline-dark" href="<?= h(app_url('catalog.php')) ?>"><?= h(t('Browse more subjects', 'تصفح المزيد من المواد')) ?></a>
</div>
</div>
</div>
<div class="col-lg-5">
<aside class="panel-card sticky-card">
<h2 class="h5 mb-3"><?= h(t('First unlocked classrooms', 'أول الفصول المفتوحة')) ?></h2>
<div class="stack-list">
<?php foreach ($subjects as $subject): ?>
<div class="stack-item">
<div>
<strong><?= h(subject_title($subject)) ?></strong>
<p><?= h(subject_next_live($subject)) ?></p>
</div>
<a class="btn btn-sm btn-outline-dark" href="<?= h(app_url('subject.php', ['slug' => $subject['slug']])) ?>"><?= h(t('Open', 'فتح')) ?></a>
</div>
<?php endforeach; ?>
</div>
<hr>
<div class="small text-secondary d-flex flex-column gap-2">
<div><strong class="text-dark"><?= h(t('Payment channel', 'قناة الدفع')) ?>:</strong> <?= h($subscription['payment_gateway']) ?></div>
<div><strong class="text-dark"><?= h(t('Reminder channel', 'قناة التذكير')) ?>:</strong> <?= h((int) $subscription['wablas_opt_in'] === 1 ? t('Enabled via Wablas', 'مفعلة عبر وابلاس') : t('Not enabled', 'غير مفعلة')) ?></div>
<div><strong class="text-dark"><?= h(t('Live classroom', 'الفصل المباشر')) ?>:</strong> Google Meet</div>
</div>
</aside>
</div>
</div>
</div>
</main>
<?php render_footer(); ?>

51
teacher.php Normal file
View File

@ -0,0 +1,51 @@
<?php
require_once __DIR__ . '/includes/app.php';
$subjects = subjects_catalog();
$metrics = subscription_metrics();
render_head(
t('Teacher workspace', 'مساحة المعلم'),
t('Preview the teacher-side classroom view with upcoming live rooms and student demand signals.', 'عاين واجهة المعلم مع الغرف المباشرة القادمة ومؤشرات طلب الطلاب.')
);
render_nav('teacher.php');
?>
<main class="py-5">
<div class="container">
<div class="section-header mb-4">
<div>
<span class="eyebrow"><?= h(t('Teacher workspace', 'مساحة المعلم')) ?></span>
<h1 class="section-title mb-2"><?= h(t('Operate subjects, cohorts, and live classrooms', 'شغّل المواد والمجموعات والفصول المباشرة')) ?></h1>
<p class="text-secondary mb-0"><?= h(t('This role-focused page previews how teachers can manage subject delivery, meeting cadence, and student demand.', 'تعرض هذه الصفحة الموجهة للدور كيف يمكن للمعلمين إدارة تقديم المواد وإيقاع الاجتماعات وطلب الطلاب.')) ?></p>
</div>
<span class="badge bg-dark-subtle text-dark-emphasis border"><?= h(t('Preview UI', 'واجهة معاينة')) ?></span>
</div>
<div class="row g-3 mb-4">
<div class="col-md-4"><div class="metric-card tall"><strong><?= h((string) count($subjects)) ?></strong><span><?= h(t('subjects assigned', 'مواد مخصصة')) ?></span></div></div>
<div class="col-md-4"><div class="metric-card tall"><strong><?= h((string) $metrics['active']) ?></strong><span><?= h(t('active subscribers', 'مشتركين نشطين')) ?></span></div></div>
<div class="col-md-4"><div class="metric-card tall"><strong>4</strong><span><?= h(t('Meet rooms this week', 'غرف Meet هذا الأسبوع')) ?></span></div></div>
</div>
<div class="row g-3">
<?php foreach ($subjects as $subject): ?>
<div class="col-lg-6">
<article class="panel-card h-100">
<div class="d-flex justify-content-between gap-3 align-items-start mb-3">
<div>
<h2 class="h5 mb-1"><?= h(subject_title($subject)) ?></h2>
<p class="small text-secondary mb-0"><?= h(subject_teacher($subject)) ?></p>
</div>
<span class="mini-tag"><?= h(t('Live ready', 'جاهزة للبث')) ?></span>
</div>
<div class="detail-meta-grid mb-3">
<div><strong><?= h(t('Next room', 'الغرفة القادمة')) ?></strong><span><?= h(subject_next_live($subject)) ?></span></div>
<div><strong><?= h(t('Delivery', 'التقديم')) ?></strong><span><?= h(t('Google Meet + async tasks', 'Google Meet + مهام غير متزامنة')) ?></span></div>
</div>
<div class="d-flex gap-2 flex-wrap">
<a class="btn btn-sm btn-dark" href="<?= h($subject['meet_url']) ?>" target="_blank" rel="noreferrer"><?= h(t('Open Meet', 'افتح Meet')) ?></a>
<a class="btn btn-sm btn-outline-dark" href="<?= h(app_url('subject.php', ['slug' => $subject['slug']])) ?>"><?= h(t('Review page', 'راجع الصفحة')) ?></a>
</div>
</article>
</div>
<?php endforeach; ?>
</div>
</div>
</main>
<?php render_footer(); ?>