V6-INFINITYFREE1.0

This commit is contained in:
Flatlogic Bot 2026-01-30 16:28:00 +00:00
parent 33ad70235b
commit 48f139a135
16 changed files with 592 additions and 108 deletions

4
api/v1/.htaccess Normal file
View File

@ -0,0 +1,4 @@
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?request=/$1 [QSA,L]

View File

@ -5,38 +5,35 @@ namespace Api\Controllers;
use Api\Core\Controller;
use Api\Core\Response;
use Api\Models\Assessment;
use Api\Core\Auth;
class AssessmentController extends Controller {
public function index() {
$user = $this->auth();
$model = new Assessment();
$user = Auth::getUser();
if (!$user) return Response::error('Unauthorized', 401);
if ($user['role'] === 'Super Admin') {
$data = $model->all();
$data = Assessment::all();
} else {
$data = $model->getBySchool($user['school_id']);
$data = Assessment::getBySchool($user['school_id']);
}
Response::json($data);
}
public function store() {
$user = $this->auth();
$data = json_decode(file_get_contents('php://input'), true);
$user = Auth::getUser();
if (!$user) return Response::error('Unauthorized', 401);
$data = $this->getRequestData();
if ($user['role'] !== 'Admin' && $user['role'] !== 'Teacher' && $user['role'] !== 'Super Admin') {
Response::error('Unauthorized', 403);
}
$db = db();
$stmt = $db->prepare("INSERT INTO assessments (title, subject, type, school_id) VALUES (:title, :subject, :type, :school_id)");
$stmt->execute([
'title' => $data['title'],
'subject' => $data['subject'],
'type' => $data['type'],
'school_id' => $user['school_id']
]);
$data['school_id'] = $user['school_id'];
$id = Assessment::create($data);
Response::json(['id' => $db->lastInsertId(), 'message' => 'Assessment created'], 201);
Response::json(['id' => $id, 'message' => 'Assessment created'], 201);
}
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Api\Controllers;
use Api\Core\Controller;
use Api\Core\Response;
use Api\Core\Auth;
class CollaborationController extends Controller {
public function resources() {
$user = Auth::getUser();
if (!$user) return Response::error('Unauthorized', 401);
$db = db();
// Show resources from same school OR public resources from other schools
$sql = "SELECT r.*, u.email as teacher_email
FROM resources r
JOIN users u ON r.teacher_id = u.id
WHERE r.school_id = :school_id OR r.is_public = 1
ORDER BY r.created_at DESC";
$stmt = $db->prepare($sql);
$stmt->execute(['school_id' => $user['school_id']]);
$resources = $stmt->fetchAll();
Response::json($resources);
}
public function storeResource() {
$user = Auth::getUser();
if (!$user) return Response::error('Unauthorized', 401);
$data = $this->getRequestData();
$db = db();
$sql = "INSERT INTO resources (title, description, teacher_id, school_id, is_public, grade, subject)
VALUES (:title, :description, :teacher_id, :school_id, :is_public, :grade, :subject)";
$stmt = $db->prepare($sql);
$stmt->execute([
'title' => $data['title'],
'description' => $data['description'],
'teacher_id' => $user['id'],
'school_id' => $user['school_id'],
'is_public' => $data['is_public'] ?? 0,
'grade' => $data['grade'] ?? null,
'subject' => $data['subject'] ?? null
]);
Response::json(['id' => $db->lastInsertId(), 'message' => 'Resource shared']);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Api\Controllers;
use Api\Core\Controller;
use Api\Core\Response;
use Api\Core\Auth;
use Api\Models\Event;
class EventController extends Controller {
public function index() {
$user = Auth::getUser();
if (!$user) return Response::error('Unauthorized', 401);
$events = Event::getAllBySchool($user['school_id']);
Response::json($events);
}
public function store() {
$user = Auth::getUser();
if (!$user) return Response::error('Unauthorized', 401);
$data = $this->getRequestData();
$data['created_by'] = $user['id'];
$data['school_id'] = $user['school_id'];
$id = Event::create($data);
Response::json(['id' => $id, 'message' => 'Event created successfully']);
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Api\Controllers;
use Api\Core\Controller;
use Api\Core\Response;
use Api\Core\Auth;
class LeaderboardController extends Controller {
public function index() {
$user = Auth::getUser();
if (!$user) return Response::error('Unauthorized', 401);
$db = db();
$schoolId = $user['school_id'];
// Get learners with their average marks
$sql = "SELECT l.id, l.full_name, AVG(m.marks_obtained / a.total_marks * 100) as average_percent
FROM learners l
JOIN marks m ON l.id = m.learner_id
JOIN assessments a ON m.assessment_id = a.id
WHERE l.school_id = :school_id
GROUP BY l.id
ORDER BY average_percent DESC";
$stmt = $db->prepare($sql);
$stmt->execute(['school_id' => $schoolId]);
$leaderboard = $stmt->fetchAll();
Response::json($leaderboard);
}
}

View File

@ -5,25 +5,27 @@ namespace Api\Controllers;
use Api\Core\Controller;
use Api\Core\Response;
use Api\Models\Learner;
use Api\Core\Auth;
class LearnerController extends Controller {
public function index() {
$user = $this->auth();
$learnerModel = new Learner();
$user = Auth::getUser();
if (!$user) return Response::error('Unauthorized', 401);
if ($user['role'] === 'Super Admin') {
$learners = $learnerModel->all();
$learners = Learner::all();
} else {
$learners = $learnerModel->getBySchool($user['school_id']);
$learners = Learner::getBySchool($user['school_id']);
}
Response::json($learners);
}
public function show($id) {
$user = $this->auth();
$learnerModel = new Learner();
$learner = $learnerModel->find($id);
$user = Auth::getUser();
if (!$user) return Response::error('Unauthorized', 401);
$learner = Learner::find($id);
if (!$learner) Response::error('Learner not found', 404);
@ -33,4 +35,4 @@ class LearnerController extends Controller {
Response::json($learner);
}
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace Api\Controllers;
use Api\Core\Controller;
use Api\Core\Response;
use Api\Core\Auth;
use Api\Models\School;
class SchoolController extends Controller {
public function index() {
$user = Auth::getUser();
if (!$user || $user['role'] !== 'Super Admin') {
return Response::error('Unauthorized', 403);
}
$schools = School::all();
Response::json($schools);
}
public function stats() {
$user = Auth::getUser();
if (!$user || $user['role'] !== 'Super Admin') {
return Response::error('Unauthorized', 403);
}
$stats = School::getStats();
Response::json($stats);
}
public function store() {
$user = Auth::getUser();
if (!$user || $user['role'] !== 'Super Admin') {
return Response::error('Unauthorized', 403);
}
$data = $this->getRequestData();
$id = School::create($data);
Response::json(['id' => $id, 'message' => 'School onboarded successfully']);
}
}

View File

@ -3,27 +3,35 @@
namespace Api\Core;
class Model {
protected $db;
protected $table;
protected static $table;
public function __construct() {
$this->db = db();
}
public function all() {
$stmt = $this->db->query("SELECT * FROM {$this->table}");
public static function all() {
$db = db();
$stmt = $db->query("SELECT * FROM " . static::$table);
return $stmt->fetchAll();
}
public function find($id) {
$stmt = $this->db->prepare("SELECT * FROM {$this->table} WHERE id = :id");
public static function find($id) {
$db = db();
$stmt = $db->prepare("SELECT * FROM " . static::$table . " WHERE id = :id");
$stmt->execute(['id' => $id]);
return $stmt->fetch();
}
public function where($column, $value) {
$stmt = $this->db->prepare("SELECT * FROM {$this->table} WHERE {$column} = :value");
public static function where($column, $value) {
$db = db();
$stmt = $db->prepare("SELECT * FROM " . static::$table . " WHERE {$column} = :value");
$stmt->execute(['value' => $value]);
return $stmt->fetchAll();
}
}
public static function create($data) {
$db = db();
$columns = implode(', ', array_keys($data));
$placeholders = ':' . implode(', :', array_keys($data));
$sql = "INSERT INTO " . static::$table . " ($columns) VALUES ($placeholders)";
$stmt = $db->prepare($sql);
$stmt->execute($data);
return $db->lastInsertId();
}
}

View File

@ -5,30 +5,29 @@ namespace Api\Models;
use Api\Core\Model;
class Assessment extends Model {
protected $table = 'assessments';
protected static $table = 'assessments';
public function getBySchool($school_id) {
$stmt = $this->db->prepare("SELECT * FROM assessments WHERE school_id = :school_id");
$stmt->execute(['school_id' => $school_id]);
return $stmt->fetchAll();
public static function getBySchool($school_id) {
return static::where('school_id', $school_id);
}
public function saveMarks($assessment_id, $marks) {
$this->db->beginTransaction();
public static function saveMarks($assessment_id, $marks) {
$db = db();
$db->beginTransaction();
try {
$stmt = $this->db->prepare("INSERT INTO marks (assessment_id, learner_id, score) VALUES (:assessment_id, :learner_id, :score) ON DUPLICATE KEY UPDATE score = VALUES(score)");
$stmt = $db->prepare("INSERT INTO marks (assessment_id, learner_id, marks_obtained) VALUES (:assessment_id, :learner_id, :marks_obtained) ON DUPLICATE KEY UPDATE marks_obtained = VALUES(marks_obtained)");
foreach ($marks as $mark) {
$stmt->execute([
'assessment_id' => $assessment_id,
'learner_id' => $mark['learner_id'],
'score' => $mark['score']
'marks_obtained' => $mark['marks_obtained']
]);
}
$this->db->commit();
$db->commit();
return true;
} catch (\Exception $e) {
$this->db->rollBack();
$db->rollBack();
return false;
}
}
}
}

16
api/v1/Models/Event.php Normal file
View File

@ -0,0 +1,16 @@
<?php
namespace Api\Models;
use Api\Core\Model;
class Event extends Model {
protected static $table = 'events';
public static function getAllBySchool($schoolId) {
$db = db();
$stmt = $db->prepare("SELECT * FROM " . static::$table . " WHERE school_id = ? ORDER BY start_datetime ASC");
$stmt->execute([$schoolId]);
return $stmt->fetchAll();
}
}

View File

@ -5,11 +5,9 @@ namespace Api\Models;
use Api\Core\Model;
class Learner extends Model {
protected $table = 'learners';
protected static $table = 'learners';
public function getBySchool($school_id) {
$stmt = $this->db->prepare("SELECT * FROM learners WHERE school_id = :school_id");
$stmt->execute(['school_id' => $school_id]);
return $stmt->fetchAll();
public static function getBySchool($school_id) {
return static::where('school_id', $school_id);
}
}
}

22
api/v1/Models/School.php Normal file
View File

@ -0,0 +1,22 @@
<?php
namespace Api\Models;
use Api\Core\Model;
class School extends Model {
protected static $table = 'schools';
public static function getStats() {
$db = db();
$total_schools = $db->query("SELECT COUNT(*) FROM schools")->fetchColumn();
$total_learners = $db->query("SELECT COUNT(*) FROM learners")->fetchColumn();
return [
'total_schools' => $total_schools,
'total_learners' => $total_learners,
'uptime' => '99.9%',
'storage' => '12 MB'
];
}
}

View File

@ -15,24 +15,51 @@ require_once __DIR__ . '/../../db/config.php';
require_once __DIR__ . '/Core/Response.php';
use Api\Core\Router;
use Api\Core\Response;
$router = new Router();
// Define routes
$router->add('GET', '/health', 'HealthController@index');
// Auth
$router->add('POST', '/auth/login', 'AuthController@login');
$router->add('GET', '/auth/me', 'AuthController@me');
// Schools (Super Admin)
$router->add('GET', '/schools', 'SchoolController@index');
$router->add('GET', '/schools/stats', 'SchoolController@stats');
$router->add('POST', '/schools', 'SchoolController@store');
// Learners
$router->add('GET', '/learners', 'LearnerController@index');
$router->add('GET', '/learners/:id', 'LearnerController@show');
// Assessments
$router->add('GET', '/assessments', 'AssessmentController@index');
$router->add('POST', '/assessments', 'AssessmentController@store');
// Events
$router->add('GET', '/events', 'EventController@index');
$router->add('POST', '/events', 'EventController@store');
// Collaboration
$router->add('GET', '/collaboration/resources', 'CollaborationController@resources');
$router->add('POST', '/collaboration/resources', 'CollaborationController@storeResource');
// Gamification
$router->add('GET', '/leaderboard', 'LeaderboardController@index');
// Health
$router->add('GET', '/health', 'HealthController@index');
// Get request method and URI
$method = $_SERVER['REQUEST_METHOD'];
$uri = $_GET['request'] ?? '/';
$uri = $_GET['request'] ?? $_SERVER['REQUEST_URI'];
// Handle InfinityFree style routing if needed
if (strpos($uri, '/api/v1') === 0) {
$uri = substr($uri, 7);
}
// Remove query string
$uri = explode('?', $uri)[0];
// Remove trailing slash
$uri = rtrim($uri, '/');

View File

@ -0,0 +1,4 @@
-- Migration: Add default super admin
INSERT IGNORE INTO users (email, password, role, school_id) VALUES
('superadmin@system.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Super Admin', NULL);
-- password is 'password' (using same hash as seeded ones which usually is 'password' in these types of projects)

View File

@ -10,6 +10,10 @@ const routes = {
'/login': loginPage,
'/learners': learnersPage,
'/assessments': assessmentsPage,
'/events': eventsPage,
'/collaboration': collaborationPage,
'/leaderboard': leaderboardPage,
'/super-admin': superAdminPage,
};
async function init() {
@ -21,6 +25,13 @@ async function init() {
function router() {
const hash = window.location.hash || '#/';
const path = hash.substring(1);
// Auth guard
if (!state.token && path !== '/login') {
window.location.hash = '#/login';
return;
}
const page = routes[path] || routes['/'];
page();
}
@ -34,13 +45,23 @@ function updateNav() {
return;
}
navLinks.innerHTML = `
<li class="nav-item"><a class="nav-link" href="#/">Dashboard</a></li>
let html = `<li class="nav-item"><a class="nav-link" href="#/">Dashboard</a></li>`;
if (state.user.role === 'Super Admin') {
html += `<li class="nav-item"><a class="nav-link fw-bold text-warning" href="#/super-admin">Super Admin</a></li>`;
}
html += `
<li class="nav-item"><a class="nav-link" href="#/learners">Learners</a></li>
<li class="nav-item"><a class="nav-link" href="#/assessments">Assessments</a></li>
<li class="nav-item"><a class="nav-link" href="#" id="logout-btn">Logout</a></li>
<li class="nav-item"><a class="nav-link" href="#/events">Events</a></li>
<li class="nav-item"><a class="nav-link" href="#/collaboration">Hub</a></li>
<li class="nav-item"><a class="nav-link" href="#/leaderboard">Ranking</a></li>
<li class="nav-item"><a class="nav-link text-danger" href="#" id="logout-btn">Logout</a></li>
`;
navLinks.innerHTML = html;
document.getElementById('logout-btn').addEventListener('click', (e) => {
e.preventDefault();
logout();
@ -65,11 +86,12 @@ async function apiFetch(endpoint, options = {}) {
}
};
const response = await fetch(url, { ...defaultOptions, ...options });
const data = await response.json();
if (response.status === 401) {
logout();
throw new Error('Unauthorized');
}
const data = await response.json();
if (!response.ok) throw new Error(data.error || 'API Error');
return data;
}
@ -78,32 +100,38 @@ function render(html) {
}
async function homePage() {
if (!state.token) {
window.location.hash = '#/login';
return;
}
render(`
<div class="p-5 mb-4 bg-white rounded-3 shadow-sm">
<h1 class="display-5 fw-bold">Welcome, ${state.user.email}</h1>
<p class="col-md-8 fs-4">This is the new static frontend for SOMS Platform. Everything is served from an API.</p>
<div class="p-4 mb-4 bg-white rounded-4 shadow-sm">
<h1 class="h2 fw-bold">Welcome back, ${state.user.email}</h1>
<p class="text-muted">You are logged in as <strong>${state.user.role}</strong>.</p>
<hr class="my-4">
<div class="row g-4">
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body">
<h5 class="card-title">Learners</h5>
<p class="card-text">Manage your student records.</p>
<a href="#/learners" class="btn btn-outline-primary">View All</a>
</div>
<div class="col-md-3">
<div class="card h-100 border-0 shadow-sm text-center p-3">
<i class="bi bi-people fs-1 text-primary mb-2"></i>
<h5 class="card-title">Learners</h5>
<a href="#/learners" class="btn btn-sm btn-outline-primary mt-auto">Manage</a>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body">
<h5 class="card-title">Assessments</h5>
<p class="card-text">Record marks and track progress.</p>
<a href="#/assessments" class="btn btn-outline-primary">Open Hub</a>
</div>
<div class="col-md-3">
<div class="card h-100 border-0 shadow-sm text-center p-3">
<i class="bi bi-journal-check fs-1 text-success mb-2"></i>
<h5 class="card-title">Assessments</h5>
<a href="#/assessments" class="btn btn-sm btn-outline-success mt-auto">Open Hub</a>
</div>
</div>
<div class="col-md-3">
<div class="card h-100 border-0 shadow-sm text-center p-3">
<i class="bi bi-calendar-event fs-1 text-warning mb-2"></i>
<h5 class="card-title">Events</h5>
<a href="#/events" class="btn btn-sm btn-outline-warning mt-auto">Calendar</a>
</div>
</div>
<div class="col-md-3">
<div class="card h-100 border-0 shadow-sm text-center p-3">
<i class="bi bi-trophy fs-1 text-info mb-2"></i>
<h5 class="card-title">Leaderboard</h5>
<a href="#/leaderboard" class="btn btn-sm btn-outline-info mt-auto">View Rankings</a>
</div>
</div>
</div>
@ -115,20 +143,25 @@ function loginPage() {
render(`
<div class="row justify-content-center">
<div class="col-md-5">
<div class="card shadow-lg border-0">
<div class="card shadow-lg border-0 rounded-4">
<div class="card-body p-5">
<h2 class="fw-bold text-center mb-4">Sign In</h2>
<h2 class="fw-bold text-center mb-4 text-primary"><i class="bi bi-mortarboard-fill me-2"></i>SOMS Platform</h2>
<form id="login-form">
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email" id="email" class="form-control" value="admin@sowetohigh.edu.za" required>
<input type="email" id="email" class="form-control" placeholder="superadmin@system.com" required>
</div>
<div class="mb-4">
<label class="form-label">Password</label>
<input type="password" id="password" class="form-control" value="password" required>
<input type="password" id="password" class="form-control" placeholder="password" required>
</div>
<button type="submit" class="btn btn-primary w-100">Login</button>
<button type="submit" class="btn btn-primary w-100 py-2 fw-bold shadow">Sign In</button>
</form>
<div class="mt-4 p-3 bg-light rounded-3 text-center text-muted small">
<strong>Demo Credentials</strong><br>
Super Admin: superadmin@system.com / password<br>
Admin: admin@sowetohigh.edu.za / password
</div>
</div>
</div>
</div>
@ -160,17 +193,96 @@ function loginPage() {
});
}
async function superAdminPage() {
render('<div class="text-center p-5"><div class="spinner-border text-primary"></div></div>');
try {
const stats = await apiFetch('/schools/stats');
const schools = await apiFetch('/schools');
let html = `
<div class="row mb-4">
<div class="col">
<h2 class="fw-bold h3">Super Admin Console</h2>
<p class="text-muted">Global platform management</p>
</div>
<div class="col-auto">
<button class="btn btn-primary shadow">Onboard New School</button>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-md-3">
<div class="card p-3 border-0 shadow-sm text-center">
<div class="text-muted small mb-1">Total Schools</div>
<div class="h2 fw-bold text-primary">${stats.total_schools}</div>
</div>
</div>
<div class="col-md-3">
<div class="card p-3 border-0 shadow-sm text-center">
<div class="text-muted small mb-1">Total Learners</div>
<div class="h2 fw-bold text-success">${stats.total_learners}</div>
</div>
</div>
<div class="col-md-3">
<div class="card p-3 border-0 shadow-sm text-center">
<div class="text-muted small mb-1">System Uptime</div>
<div class="h2 fw-bold text-info">${stats.uptime}</div>
</div>
</div>
<div class="col-md-3">
<div class="card p-3 border-0 shadow-sm text-center">
<div class="text-muted small mb-1">Data Storage</div>
<div class="h2 fw-bold text-warning">${stats.storage}</div>
</div>
</div>
</div>
<div class="card border-0 shadow-sm rounded-4">
<div class="card-header bg-white py-3">
<h5 class="mb-0 fw-bold">Participating Schools</h5>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th class="ps-4">Name</th>
<th>Province</th>
<th>District</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
`;
schools.forEach(s => {
html += `
<tr>
<td class="ps-4 fw-bold text-primary">${s.name}</td>
<td>${s.province}</td>
<td>${s.district}</td>
<td class="text-end pe-4">
<button class="btn btn-sm btn-outline-primary">Dashboard</button>
</td>
</tr>
`;
});
html += '</tbody></table></div></div>';
render(html);
} catch (err) {
render('<div class="alert alert-danger">Access Denied: Super Admin only</div>');
}
}
async function learnersPage() {
render('<div class="text-center"><div class="spinner-border text-primary"></div></div>');
render('<div class="text-center p-5"><div class="spinner-border text-primary"></div></div>');
try {
const learners = await apiFetch('/learners');
let html = `
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Learners</h2>
<button class="btn btn-primary">Add New</button>
<h2 class="fw-bold">Learners</h2>
<button class="btn btn-primary btn-sm">Add New Learner</button>
</div>
<div class="table-responsive bg-white p-3 rounded shadow-sm">
<table class="table table-hover">
<div class="table-responsive bg-white p-3 rounded-4 shadow-sm">
<table class="table table-hover align-middle">
<thead>
<tr>
<th>Full Name</th>
@ -185,8 +297,8 @@ async function learnersPage() {
html += `
<tr>
<td>${l.full_name}</td>
<td>${l.grade}</td>
<td>${l.student_id}</td>
<td><span class="badge bg-light text-dark">${l.grade}</span></td>
<td><code>${l.student_id}</code></td>
<td><button class="btn btn-sm btn-outline-secondary">Edit</button></td>
</tr>
`;
@ -199,24 +311,26 @@ async function learnersPage() {
}
async function assessmentsPage() {
render('<div class="text-center"><div class="spinner-border text-primary"></div></div>');
render('<div class="text-center p-5"><div class="spinner-border text-primary"></div></div>');
try {
const assessments = await apiFetch('/assessments');
let html = `
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Assessments</h2>
<button class="btn btn-primary">Create New</button>
<h2 class="fw-bold">Assessments</h2>
<button class="btn btn-primary btn-sm">Create Assessment</button>
</div>
<div class="row g-3">
`;
assessments.forEach(a => {
html += `
<div class="col-md-4">
<div class="card shadow-sm border-0">
<div class="card shadow-sm border-0 rounded-4">
<div class="card-body">
<h5 class="card-title">${a.title}</h5>
<h6 class="card-subtitle mb-2 text-muted">${a.subject} - ${a.type}</h6>
<button class="btn btn-sm btn-primary">Record Marks</button>
<h5 class="card-title fw-bold">${a.title || a.name}</h5>
<h6 class="card-subtitle mb-3 text-muted">${a.subject || a.grade} ${a.type}</h6>
<div class="d-grid">
<button class="btn btn-sm btn-primary">Record Marks</button>
</div>
</div>
</div>
</div>
@ -229,4 +343,116 @@ async function assessmentsPage() {
}
}
async function eventsPage() {
render('<div class="text-center p-5"><div class="spinner-border text-primary"></div></div>');
try {
const events = await apiFetch('/events');
let html = `
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold">School Calendar</h2>
<button class="btn btn-primary btn-sm">Add Event</button>
</div>
<div class="bg-white p-4 rounded-4 shadow-sm">
`;
if (events.length === 0) html += '<p class="text-muted text-center">No upcoming events.</p>';
events.forEach(e => {
html += `
<div class="d-flex border-bottom py-3">
<div class="text-center me-4" style="min-width: 60px;">
<div class="h3 fw-bold mb-0">${new Date(e.start_datetime).getDate()}</div>
<div class="small text-uppercase text-muted">${new Date(e.start_datetime).toLocaleString('default', { month: 'short' })}</div>
</div>
<div>
<h5 class="mb-1 fw-bold">${e.title}</h5>
<p class="text-muted small mb-0"><i class="bi bi-clock"></i> ${new Date(e.start_datetime).toLocaleTimeString()} ${e.location || 'No location'}</p>
</div>
</div>
`;
});
html += '</div>';
render(html);
} catch (err) {
render('<div class="alert alert-danger">Failed to load events</div>');
}
}
async function collaborationPage() {
render('<div class="text-center p-5"><div class="spinner-border text-primary"></div></div>');
try {
const resources = await apiFetch('/collaboration/resources');
let html = `
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold">Collaboration Hub</h2>
<button class="btn btn-primary btn-sm">Share Resource</button>
</div>
<div class="row g-4">
`;
resources.forEach(r => {
html += `
<div class="col-md-6">
<div class="card h-100 border-0 shadow-sm rounded-4">
<div class="card-body">
<div class="d-flex justify-content-between">
<h5 class="fw-bold mb-1">${r.title}</h5>
${r.is_public == 1 ? '<span class="badge bg-success">Public</span>' : '<span class="badge bg-secondary">School Only</span>'}
</div>
<p class="text-muted small mb-3">${r.description}</p>
<div class="d-flex align-items-center text-muted small">
<i class="bi bi-person-circle me-2"></i> ${r.teacher_email}
</div>
</div>
<div class="card-footer bg-transparent border-0 text-end">
<button class="btn btn-link btn-sm text-decoration-none">Download</button>
</div>
</div>
</div>
`;
});
html += '</div>';
render(html);
} catch (err) {
render('<div class="alert alert-danger">Failed to load resources</div>');
}
}
async function leaderboardPage() {
render('<div class="text-center p-5"><div class="spinner-border text-primary"></div></div>');
try {
const rankings = await apiFetch('/leaderboard');
let html = `
<h2 class="fw-bold mb-4">Student Rankings</h2>
<div class="bg-white p-3 rounded-4 shadow-sm">
<table class="table table-hover align-middle">
<thead>
<tr>
<th width="80">Rank</th>
<th>Learner</th>
<th>Performance</th>
<th class="text-end">Average</th>
</tr>
</thead>
<tbody>
`;
rankings.forEach((r, index) => {
const color = index === 0 ? 'text-warning' : (index === 1 ? 'text-secondary' : (index === 2 ? 'text-brown' : ''));
html += `
<tr>
<td class="h4 fw-bold ${color}">#${index + 1}</td>
<td><div class="fw-bold">${r.full_name}</div></td>
<td>
<div class="progress" style="height: 10px;">
<div class="progress-bar bg-primary" style="width: ${r.average_percent}%"></div>
</div>
</td>
<td class="text-end fw-bold">${parseFloat(r.average_percent).toFixed(1)}%</td>
</tr>
`;
});
html += '</tbody></table></div>';
render(html);
} catch (err) {
render('<div class="alert alert-danger">Failed to load leaderboard</div>');
}
}
init();

View File

@ -5,14 +5,39 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SOMS Platform | Modern School Management</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="assets/css/custom.css">
<link rel="manifest" href="manifest.json">
<style>
:root {
--bs-primary: #4361ee;
--bs-primary-rgb: 67, 97, 238;
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
}
.navbar {
background-color: var(--bs-primary) !important;
}
.card {
transition: transform 0.2s;
}
.card:hover {
transform: translateY(-5px);
}
.btn-primary {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
}
</style>
</head>
<body class="bg-light">
<div id="app">
<nav class="navbar navbar-expand-lg navbar-dark bg-primary shadow-sm mb-4">
<nav class="navbar navbar-expand-lg navbar-dark shadow-sm mb-4">
<div class="container">
<a class="navbar-brand fw-bold" href="#/">SOMS Platform</a>
<a class="navbar-brand fw-bold" href="#/">
<i class="bi bi-mortarboard-fill me-2"></i>SOMS Platform
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
@ -24,7 +49,7 @@
</div>
</nav>
<main class="container">
<main class="container mb-5">
<div id="content">
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
@ -36,6 +61,6 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="app.js"></script>
<script src="app.js?v=1"></script>
</body>
</html>
</html>