Autosave: 20260406-042125
This commit is contained in:
parent
3f8c63d1b3
commit
1c8373aa37
296
admin.php
Normal file
296
admin.php
Normal 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>
|
||||
© <?= 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
162
admin_classes.php
Normal 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
251
admin_subjects.php
Normal 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; ?>
|
||||
@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
53
catalog.php
Normal 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
146
checkout.php
Normal 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
121
dashboard.php
Normal 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
622
includes/app.php
Normal 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
290
index.php
@ -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
12
patch_admin_includes.php
Normal 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
20
patch_admin_nav.php
Normal 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
51
patch_app.php
Normal 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
27
patch_db2.php
Normal 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
22
patch_db_classes.php
Normal 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
16
patch_nav.php
Normal 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
54
pricing.php
Normal 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
93
subject.php
Normal 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')) ?>">← <?= 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
85
subscription.php
Normal 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
51
teacher.php
Normal 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(); ?>
|
||||
Loading…
x
Reference in New Issue
Block a user