diff --git a/admin.php b/admin.php new file mode 100644 index 0000000..f49ef43 --- /dev/null +++ b/admin.php @@ -0,0 +1,296 @@ + +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.', 'إدارة منصة التعلم الخاصة بك.') +); +?> + + +
+ + + + +
+
+ +
+
+ +

+

+
+
+
+
+
+
+
+
+
+

+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
#
+
+
+
+
+ +
+ + + +
+
+ +

+

+
+
+ +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+ Logo +
+ + +
+
+ + +
+ Favicon +
+ + +
+ +
+
+ + + + + + +
+
+

+
+
+
+
+ +
+
+ +
+ + +
+
+ + + + + + + diff --git a/admin_classes.php b/admin_classes.php new file mode 100644 index 0000000..02a025f --- /dev/null +++ b/admin_classes.php @@ -0,0 +1,162 @@ + 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; + } +?> +
+
+

+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+ +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); +?> +
+
+

+
+ + +
+ +
+
+ + + + + + +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + +
ID
+
+
+ + + +
+ +
+
+
+
+ + 1): ?> + + + + diff --git a/admin_subjects.php b/admin_subjects.php new file mode 100644 index 0000000..1c4edef --- /dev/null +++ b/admin_subjects.php @@ -0,0 +1,251 @@ +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; + } +?> +
+
+

+
+
+
+
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ + + +
+
+ + 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); +?> +
+
+

+
+ + +
+ +
+
+ + + + + + + + + + +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
ID
+
+
+
+ + + + + + - + + + + + +
+ +
+
+
+
+ + 1): ?> + + + + diff --git a/assets/css/custom.css b/assets/css/custom.css index 789132e..090e1b0 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -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; -} \ No newline at end of file +.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%; + } +} diff --git a/assets/js/main.js b/assets/js/main.js index d349598..42ae33e 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -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); + } }); diff --git a/catalog.php b/catalog.php new file mode 100644 index 0000000..16a3c8c --- /dev/null +++ b/catalog.php @@ -0,0 +1,53 @@ + +
+
+
+
+ +

+

+
+
+ + +
+
+
+ +
+
+
+
+ +

+

+
+ +
+

+
+
+
+
+ +
+
+ +
+
+
+

+

+
+
+
+
+ diff --git a/checkout.php b/checkout.php new file mode 100644 index 0000000..112b1c2 --- /dev/null +++ b/checkout.php @@ -0,0 +1,146 @@ + 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'); +?> +
+
+
+
+
+ +

+

+ +
+
    + +
  • + +
+
+ +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ > + +
+
+
+ + +
+
+
+
+
+ +
+
+
+
+ diff --git a/dashboard.php b/dashboard.php new file mode 100644 index 0000000..742b785 --- /dev/null +++ b/dashboard.php @@ -0,0 +1,121 @@ + +
+
+ +
+ +

+

+
+ + +
+
+ + +
+
+
+
+
+ +

+

+
+ +
+
+
+ = 999 ? 4 : $plan['subjects_limit']); ?> + + 0 ? round(($completed / $totalModules) * 100) : 0; + ?> +
+
+
+
+

+

+
+ +
+
+

%

+
+
+
+
+

+
+ + +
+
+
+ +
+
+
+

+ +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ +
+
+ +
+
+ \ No newline at end of file diff --git a/includes/app.php b/includes/app.php new file mode 100644 index 0000000..b7e316c --- /dev/null +++ b/includes/app.php @@ -0,0 +1,622 @@ +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'] ?? ''; + ?> + + + + + + <?= h(page_title($title)) ?> + + + + + + + + + + + + + + + + + + + + + + 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(); + ?> + + + + + + + + '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', + }; +} diff --git a/index.php b/index.php index 7205f3d..810dcad 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,150 @@ count($subjects), 'teachers' => 12, 'live' => 18]; ?> - - - - - - New Style - - - - - - - - - - - - - - - - - - - - - -
-
-

Analyzing your requirements and generating your website…

-
- Loading… +
+
+
+
+
+ +

+

+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +
+
+
+
+ +

+
+ Thawani +
+
+
+ +

+
+ Meet +
+
+
+ +

+
+ Wablas +
+
+ +
+
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

-
-
- Page updated: (UTC) -
- - + + +
+
+
+
+ +

+
+ +
+
+ +
+
+
+ + +
+

+

+
+ + +
+
+
+ +
+
+
+ +
+
+
+
+ +

+
+
+
+
01

+
02

+
03

+
+
+
+ +
+
+
+
+ +

+
+ +
+
+ +
+
+
+

+ +
+
+
    + +
  • + +
+ +
+
+ +
+
+
+
+ diff --git a/patch_admin_includes.php b/patch_admin_includes.php new file mode 100644 index 0000000..7a577c0 --- /dev/null +++ b/patch_admin_includes.php @@ -0,0 +1,12 @@ + + + + +HTML; +$content = str_replace('', $injection . "\n + > + + + +HTML; + +$content = str_replace( + '
  • \n \n 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 = '<?= h(page_title($title)) ?>'; +$render_head_new = <<<'EOD' +<?= h(page_title($title)) ?> + + + +EOD; +$content = str_replace($render_head_old, $render_head_new, $content); + +file_put_contents('includes/app.php', $content); +echo "Patched includes/app.php\n"; \ No newline at end of file diff --git a/patch_db2.php b/patch_db2.php new file mode 100644 index 0000000..ce12bd1 --- /dev/null +++ b/patch_db2.php @@ -0,0 +1,27 @@ +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(); +} + diff --git a/patch_db_classes.php b/patch_db_classes.php new file mode 100644 index 0000000..5099ed1 --- /dev/null +++ b/patch_db_classes.php @@ -0,0 +1,22 @@ +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"; + diff --git a/patch_nav.php b/patch_nav.php new file mode 100644 index 0000000..fc0ac4c --- /dev/null +++ b/patch_nav.php @@ -0,0 +1,16 @@ +">'; +$nav_new = <<<'EOD' + + + Logo + + + +EOD; + +$content = str_replace($nav_old, $nav_new, $content); +file_put_contents('includes/app.php', $content); +echo "Patched navbar\n"; + diff --git a/pricing.php b/pricing.php new file mode 100644 index 0000000..e0be559 --- /dev/null +++ b/pricing.php @@ -0,0 +1,54 @@ + +
    +
    +
    +
    + +

    +

    +
    +
    + Thawani + Wablas + Google Meet +
    +
    +
    + +
    +
    +
    +
    +

    +

    +
    + +
    +
    +
    +
    +
    +
      + +
    • + +
    +
    + + +
    +
    +
    + +
    +
    +
    + diff --git a/subject.php b/subject.php new file mode 100644 index 0000000..eaa4968 --- /dev/null +++ b/subject.php @@ -0,0 +1,93 @@ + $slug])); + exit; +} + +$completedModules = $hasAccess ? get_completed_modules((int) $subscription['id'], $slug) : []; + +render_head(subject_title($subject), subject_summary($subject)); +render_nav('catalog.php'); +?> +
    +
    +
    +
    +
    +
    + +
    +
    +

    +

    +
    +
    + +
    +
    +
    +
    + +
    + +
    +

    +
      + $module): ?> +
    • + + + +
      + + + + +
      + +
    • + +
    +
    +
    +
    + +
    +
    +
    +
    + \ No newline at end of file diff --git a/subscription.php b/subscription.php new file mode 100644 index 0000000..a5bf799 --- /dev/null +++ b/subscription.php @@ -0,0 +1,85 @@ + 0 ? fetch_subscription($id) : current_subscription(); +if (!$subscription) { + render_head(t('Subscription not found', 'الاشتراك غير موجود')); + render_nav('dashboard.php'); + ?> +
    + = 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'); +?> +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +

    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + +
    +
    +
    +
    + diff --git a/teacher.php b/teacher.php new file mode 100644 index 0000000..f75aa28 --- /dev/null +++ b/teacher.php @@ -0,0 +1,51 @@ + +
    +
    +
    +
    + +

    +

    +
    + +
    +
    +
    +
    +
    4
    +
    +
    + +
    +
    +
    +
    +

    +

    +
    + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    + +
    +
    +
    +