39496-vm/includes/app.php
2026-04-07 07:11:44 +00:00

696 lines
28 KiB
PHP

<?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 get_landing_settings(): array {
static $settings = null;
if ($settings === null) {
$settings = [];
try {
$stmt = db()->query("SELECT setting_key, value_en, value_ar FROM landing_settings");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$settings[$row['setting_key']] = $row;
}
} catch (Exception $e) {
}
}
return $settings;
}
function landing_setting(string $key, string $default_en, string $default_ar = ""): string {
$settings = get_landing_settings();
$lang = current_lang();
if (isset($settings[$key])) {
$val = $lang === 'ar' ? $settings[$key]['value_ar'] : $settings[$key]['value_en'];
if (trim((string)$val) !== '') {
return $val;
}
}
return $lang === 'ar' ? $default_ar : $default_en;
}
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 s.* FROM subjects s LEFT JOIN classes c ON s.class_id = c.id WHERE s.status = 'active' AND (s.class_id IS NULL OR s.class_id = 0 OR c.status = 'active')");
$rows = $stmt->fetchAll();
$result = [];
foreach ($rows as $row) {
$row['modules_en'] = json_decode($row['modules_en'] ? (string)$row['modules_en'] : '[]', true) ?: [];
$row['modules_ar'] = json_decode($row['modules_ar'] ? (string)$row['modules_ar'] : '[]', true) ?: [];
$class_id = (int)$row['class_id'];
$subject_id = (int)$row['id'];
// Dynamically fetch active assigned teacher
$teacher_stmt = db()->prepare(
"SELECT t.name
FROM teacher_assignments ta
JOIN teachers t ON ta.teacher_id = t.id
WHERE t.status = 'active'
AND ta.class_id = ?
AND (ta.subject_id = ? OR ta.subject_id = 0)
LIMIT 1"
);
$teacher_stmt->execute([$class_id, $subject_id]);
$teacher_name = $teacher_stmt->fetchColumn();
if ($teacher_name) {
$row['teacher_en'] = $teacher_name;
$row['teacher_ar'] = $teacher_name;
} else {
// Check if there is an inactive teacher assigned. If so, override with empty or "No Teacher".
$inactive_stmt = db()->prepare(
"SELECT t.name
FROM teacher_assignments ta
JOIN teachers t ON ta.teacher_id = t.id
WHERE t.status = 'inactive'
AND ta.class_id = ?
AND (ta.subject_id = ? OR ta.subject_id = 0)
LIMIT 1"
);
$inactive_stmt->execute([$class_id, $subject_id]);
if ($inactive_stmt->fetchColumn()) {
$row['teacher_en'] = 'No Active Teacher';
$row['teacher_ar'] = 'لا يوجد معلم نشط';
}
}
$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 (string)(current_lang() === 'ar' ? ($subject['teacher_ar'] ?? '') : ($subject['teacher_en'] ?? ''));
}
function subject_level(array $subject): string
{
return (string)(current_lang() === 'ar' ? ($subject['level_ar'] ?? '') : ($subject['level_en'] ?? ''));
}
function subject_duration(array $subject): string
{
return (string)(current_lang() === 'ar' ? ($subject['duration_ar'] ?? '') : ($subject['duration_en'] ?? ''));
}
function subject_next_live(array $subject): string
{
return (string)(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 class="d-flex gap-2 ms-2">
<?php if (!empty($_SESSION['user_id'])): ?>
<a href="<?= h(app_url('profile.php')) ?>" class="btn btn-outline-primary btn-sm px-3"><?= h(t('Profile', 'حسابي')) ?></a>
<a href="<?= h(app_url('logout.php')) ?>" class="btn btn-outline-danger btn-sm px-3"><?= h(t('Logout', 'خروج')) ?></a>
<?php else: ?>
<a href="<?= h(app_url('login.php')) ?>" class="btn btn-outline-dark btn-sm px-3"><?= h(t('Login', 'دخول')) ?></a>
<?php endif; ?>
</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',
};
}