v5
This commit is contained in:
parent
7a517224e6
commit
33ad70235b
@ -135,6 +135,12 @@ include 'includes/header.php';
|
||||
<a href="learners.php" class="btn btn-outline-primary text-start">
|
||||
<i class="bi bi-person-plus me-2"></i> Manage Learners
|
||||
</a>
|
||||
<a href="leaderboard.php" class="btn btn-outline-warning text-dark text-start border-warning fw-bold">
|
||||
<i class="bi bi-trophy me-2"></i> Academic Leaderboard
|
||||
</a>
|
||||
<a href="collaboration.php" class="btn btn-outline-success text-start border-success fw-bold">
|
||||
<i class="bi bi-people me-2"></i> Collaboration Hub
|
||||
</a>
|
||||
<a href="bulk-upload.php" class="btn btn-outline-primary text-start">
|
||||
<i class="bi bi-upload me-2"></i> Bulk Upload Learners
|
||||
</a>
|
||||
@ -154,4 +160,4 @@ include 'includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include 'includes/footer.php'; ?>
|
||||
<?php include 'includes/header.php'; ?>
|
||||
42
api/v1/Controllers/AssessmentController.php
Normal file
42
api/v1/Controllers/AssessmentController.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Api\Controllers;
|
||||
|
||||
use Api\Core\Controller;
|
||||
use Api\Core\Response;
|
||||
use Api\Models\Assessment;
|
||||
|
||||
class AssessmentController extends Controller {
|
||||
public function index() {
|
||||
$user = $this->auth();
|
||||
$model = new Assessment();
|
||||
|
||||
if ($user['role'] === 'Super Admin') {
|
||||
$data = $model->all();
|
||||
} else {
|
||||
$data = $model->getBySchool($user['school_id']);
|
||||
}
|
||||
|
||||
Response::json($data);
|
||||
}
|
||||
|
||||
public function store() {
|
||||
$user = $this->auth();
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
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']
|
||||
]);
|
||||
|
||||
Response::json(['id' => $db->lastInsertId(), 'message' => 'Assessment created'], 201);
|
||||
}
|
||||
}
|
||||
54
api/v1/Controllers/AuthController.php
Normal file
54
api/v1/Controllers/AuthController.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Api\Controllers;
|
||||
|
||||
use Api\Core\Response;
|
||||
use Api\Core\Auth;
|
||||
|
||||
class AuthController {
|
||||
public function login() {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$email = $data['email'] ?? '';
|
||||
$password = $data['password'] ?? '';
|
||||
|
||||
if (!$email || !$password) {
|
||||
Response::error('Email and password required');
|
||||
}
|
||||
|
||||
$db = db();
|
||||
$stmt = $db->prepare("SELECT * FROM users WHERE email = :email");
|
||||
$stmt->execute(['email' => $email]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if ($user && password_verify($password, $user['password'])) {
|
||||
$token = Auth::generateToken([
|
||||
'user_id' => $user['id'],
|
||||
'role' => $user['role'],
|
||||
'school_id' => $user['school_id'],
|
||||
'email' => $user['email']
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'token' => $token,
|
||||
'user' => [
|
||||
'id' => $user['id'],
|
||||
'email' => $user['email'],
|
||||
'role' => $user['role'],
|
||||
'name' => $user['name'] ?? ''
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
Response::error('Invalid credentials', 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function me() {
|
||||
$token = Auth::getBearerToken();
|
||||
if (!$token) Response::error('No token provided', 401);
|
||||
|
||||
$payload = Auth::verifyToken($token);
|
||||
if (!$payload) Response::error('Invalid or expired token', 401);
|
||||
|
||||
Response::json(['user' => $payload]);
|
||||
}
|
||||
}
|
||||
15
api/v1/Controllers/HealthController.php
Normal file
15
api/v1/Controllers/HealthController.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Api\Controllers;
|
||||
|
||||
use Api\Core\Response;
|
||||
|
||||
class HealthController {
|
||||
public function index() {
|
||||
Response::json([
|
||||
'status' => 'ok',
|
||||
'timestamp' => date('c'),
|
||||
'php_version' => PHP_VERSION
|
||||
]);
|
||||
}
|
||||
}
|
||||
36
api/v1/Controllers/LearnerController.php
Normal file
36
api/v1/Controllers/LearnerController.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Api\Controllers;
|
||||
|
||||
use Api\Core\Controller;
|
||||
use Api\Core\Response;
|
||||
use Api\Models\Learner;
|
||||
|
||||
class LearnerController extends Controller {
|
||||
public function index() {
|
||||
$user = $this->auth();
|
||||
$learnerModel = new Learner();
|
||||
|
||||
if ($user['role'] === 'Super Admin') {
|
||||
$learners = $learnerModel->all();
|
||||
} else {
|
||||
$learners = $learnerModel->getBySchool($user['school_id']);
|
||||
}
|
||||
|
||||
Response::json($learners);
|
||||
}
|
||||
|
||||
public function show($id) {
|
||||
$user = $this->auth();
|
||||
$learnerModel = new Learner();
|
||||
$learner = $learnerModel->find($id);
|
||||
|
||||
if (!$learner) Response::error('Learner not found', 404);
|
||||
|
||||
if ($user['role'] !== 'Super Admin' && $learner['school_id'] != $user['school_id']) {
|
||||
Response::error('Unauthorized', 403);
|
||||
}
|
||||
|
||||
Response::json($learner);
|
||||
}
|
||||
}
|
||||
54
api/v1/Core/Auth.php
Normal file
54
api/v1/Core/Auth.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Api\Core;
|
||||
|
||||
class Auth {
|
||||
private static $secret = 'super-secret-key-change-me'; // Should be in an env/config file
|
||||
|
||||
public static function generateToken($payload) {
|
||||
$header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']);
|
||||
$payload['exp'] = time() + (60 * 60 * 24); // 24 hours
|
||||
$payload = json_encode($payload);
|
||||
|
||||
$base64UrlHeader = self::base64UrlEncode($header);
|
||||
$base64UrlPayload = self::base64UrlEncode($payload);
|
||||
|
||||
$signature = hash_hmac('sha256', $base64UrlHeader . "." . $base64UrlPayload, self::$secret, true);
|
||||
$base64UrlSignature = self::base64UrlEncode($signature);
|
||||
|
||||
return $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature;
|
||||
}
|
||||
|
||||
public static function verifyToken($token) {
|
||||
$parts = explode('.', $token);
|
||||
if (count($parts) !== 3) return false;
|
||||
|
||||
list($header, $payload, $signature) = $parts;
|
||||
|
||||
$validSignature = hash_hmac('sha256', $header . "." . $payload, self::$secret, true);
|
||||
if (self::base64UrlEncode($validSignature) !== $signature) return false;
|
||||
|
||||
$payloadData = json_decode(self::base64UrlDecode($payload), true);
|
||||
if (isset($payloadData['exp']) && $payloadData['exp'] < time()) return false;
|
||||
|
||||
return $payloadData;
|
||||
}
|
||||
|
||||
public static function getBearerToken() {
|
||||
$headers = getallheaders();
|
||||
if (isset($headers['Authorization'])) {
|
||||
if (preg_match('/Bearer\s(\S+)/', $headers['Authorization'], $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function base64UrlEncode($data) {
|
||||
return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($data));
|
||||
}
|
||||
|
||||
private static function base64UrlDecode($data) {
|
||||
return base64_decode(str_replace(['-', '_'], ['+', '/'], $data));
|
||||
}
|
||||
}
|
||||
27
api/v1/Core/Controller.php
Normal file
27
api/v1/Core/Controller.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Api\Core;
|
||||
|
||||
class Controller {
|
||||
protected function auth() {
|
||||
$token = Auth::getBearerToken();
|
||||
if (!$token) Response::error('No token provided', 401);
|
||||
|
||||
$payload = Auth::verifyToken($token);
|
||||
if (!$payload) Response::error('Invalid or expired token', 401);
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
protected function validate($data, $rules) {
|
||||
$errors = [];
|
||||
foreach ($rules as $field => $rule) {
|
||||
if ($rule === 'required' && (!isset($data[$field]) || empty($data[$field]))) {
|
||||
$errors[] = "Field '{$field}' is required";
|
||||
}
|
||||
}
|
||||
if (!empty($errors)) {
|
||||
Response::json(['errors' => $errors], 422);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
api/v1/Core/Model.php
Normal file
29
api/v1/Core/Model.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Api\Core;
|
||||
|
||||
class Model {
|
||||
protected $db;
|
||||
protected $table;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = db();
|
||||
}
|
||||
|
||||
public function all() {
|
||||
$stmt = $this->db->query("SELECT * FROM {$this->table}");
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public function find($id) {
|
||||
$stmt = $this->db->prepare("SELECT * FROM {$this->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");
|
||||
$stmt->execute(['value' => $value]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
}
|
||||
16
api/v1/Core/Response.php
Normal file
16
api/v1/Core/Response.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Api\Core;
|
||||
|
||||
class Response {
|
||||
public static function json($data, $status = 200) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code($status);
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function error($message, $status = 400) {
|
||||
self::json(['error' => $message], $status);
|
||||
}
|
||||
}
|
||||
40
api/v1/Core/Router.php
Normal file
40
api/v1/Core/Router.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Api\Core;
|
||||
|
||||
class Router {
|
||||
private $routes = [];
|
||||
|
||||
public function add($method, $path, $handler) {
|
||||
$this->routes[] = [
|
||||
'method' => $method,
|
||||
'path' => $path,
|
||||
'handler' => $handler
|
||||
];
|
||||
}
|
||||
|
||||
public function handle($method, $uri) {
|
||||
// Simple router logic: match path exactly or with :id
|
||||
foreach ($this->routes as $route) {
|
||||
$pattern = preg_replace('/\/:\w+/', '/(\d+)', $route['path']);
|
||||
if ($route['method'] === $method && preg_match('#^' . $pattern . '$#', $uri, $matches)) {
|
||||
array_shift($matches); // remove the full match
|
||||
return $this->executeHandler($route['handler'], $matches);
|
||||
}
|
||||
}
|
||||
Response::error('Route not found', 404);
|
||||
}
|
||||
|
||||
private function executeHandler($handler, $params) {
|
||||
list($controllerName, $methodName) = explode('@', $handler);
|
||||
$controllerClass = "Api\\Controllers\\" . $controllerName;
|
||||
|
||||
if (class_exists($controllerClass)) {
|
||||
$controller = new $controllerClass();
|
||||
if (method_exists($controller, $methodName)) {
|
||||
return call_user_func_array([$controller, $methodName], $params);
|
||||
}
|
||||
}
|
||||
Response::error('Controller or method not found', 500);
|
||||
}
|
||||
}
|
||||
34
api/v1/Models/Assessment.php
Normal file
34
api/v1/Models/Assessment.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Api\Models;
|
||||
|
||||
use Api\Core\Model;
|
||||
|
||||
class Assessment extends Model {
|
||||
protected $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 function saveMarks($assessment_id, $marks) {
|
||||
$this->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)");
|
||||
foreach ($marks as $mark) {
|
||||
$stmt->execute([
|
||||
'assessment_id' => $assessment_id,
|
||||
'learner_id' => $mark['learner_id'],
|
||||
'score' => $mark['score']
|
||||
]);
|
||||
}
|
||||
$this->db->commit();
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
$this->db->rollBack();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
api/v1/Models/Learner.php
Normal file
15
api/v1/Models/Learner.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Api\Models;
|
||||
|
||||
use Api\Core\Model;
|
||||
|
||||
class Learner extends Model {
|
||||
protected $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();
|
||||
}
|
||||
}
|
||||
41
api/v1/index.php
Normal file
41
api/v1/index.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
// Basic autoloader
|
||||
spl_autoload_register(function ($class) {
|
||||
$prefix = 'Api\\';
|
||||
$base_dir = __DIR__ . '/';
|
||||
$len = strlen($prefix);
|
||||
if (strncmp($prefix, $class, $len) !== 0) return;
|
||||
$relative_class = substr($class, $len);
|
||||
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
|
||||
if (file_exists($file)) require $file;
|
||||
});
|
||||
|
||||
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');
|
||||
$router->add('POST', '/auth/login', 'AuthController@login');
|
||||
$router->add('GET', '/auth/me', 'AuthController@me');
|
||||
|
||||
$router->add('GET', '/learners', 'LearnerController@index');
|
||||
$router->add('GET', '/learners/:id', 'LearnerController@show');
|
||||
|
||||
$router->add('GET', '/assessments', 'AssessmentController@index');
|
||||
$router->add('POST', '/assessments', 'AssessmentController@store');
|
||||
|
||||
// Get request method and URI
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$uri = $_GET['request'] ?? '/';
|
||||
|
||||
// Remove trailing slash
|
||||
$uri = rtrim($uri, '/');
|
||||
if (empty($uri)) $uri = '/';
|
||||
|
||||
$router->handle($method, $uri);
|
||||
@ -18,11 +18,11 @@ if (isset($_POST['upload_resource'])) {
|
||||
$description = $_POST['description'];
|
||||
$grade = $_POST['grade'];
|
||||
$subject = $_POST['subject'];
|
||||
$is_public = isset($_POST['is_public']) ? 1 : 0;
|
||||
|
||||
// In a real app, we'd handle file uploads. For this demo, we'll just store the metadata.
|
||||
try {
|
||||
$stmt = db()->prepare("INSERT INTO resources (title, description, teacher_id, school_id, grade, subject) VALUES (?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$title, $description, $user_id, $school_id, $grade, $subject]);
|
||||
$stmt = db()->prepare("INSERT INTO resources (title, description, teacher_id, school_id, grade, subject, is_public) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$title, $description, $user_id, $school_id, $grade, $subject, $is_public]);
|
||||
$success = "Resource shared successfully!";
|
||||
} catch (PDOException $e) {
|
||||
$error = "Failed to share resource: " . $e->getMessage();
|
||||
@ -33,34 +33,55 @@ if (isset($_POST['upload_resource'])) {
|
||||
if (isset($_POST['create_post'])) {
|
||||
$title = $_POST['title'];
|
||||
$content = $_POST['content'];
|
||||
$is_public = isset($_POST['is_public']) ? 1 : 0;
|
||||
|
||||
try {
|
||||
$stmt = db()->prepare("INSERT INTO forum_posts (title, content, author_id, school_id) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$title, $content, $user_id, $school_id]);
|
||||
$stmt = db()->prepare("INSERT INTO forum_posts (title, content, author_id, school_id, is_public) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$title, $content, $user_id, $school_id, $is_public]);
|
||||
$success = "Topic posted successfully!";
|
||||
} catch (PDOException $e) {
|
||||
$error = "Failed to post topic: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch Resources
|
||||
$resources = db()->prepare("SELECT r.*, u.email as teacher_email FROM resources r JOIN users u ON r.teacher_id = u.id WHERE r.school_id = ? ORDER BY r.created_at DESC");
|
||||
$resources->execute([$school_id]);
|
||||
$resources = $resources->fetchAll();
|
||||
// Fetch Resources (Own school or Public from other schools)
|
||||
$resources_query = "
|
||||
SELECT r.*, u.email as teacher_email, s.name as school_name
|
||||
FROM resources r
|
||||
JOIN users u ON r.teacher_id = u.id
|
||||
JOIN schools s ON r.school_id = s.id
|
||||
WHERE r.school_id = :school_id OR r.is_public = 1
|
||||
ORDER BY r.created_at DESC
|
||||
";
|
||||
$resources_stmt = db()->prepare($resources_query);
|
||||
$resources_stmt->execute(['school_id' => $school_id]);
|
||||
$resources = $resources_stmt->fetchAll();
|
||||
|
||||
// Fetch Forum Posts
|
||||
$posts = db()->prepare("SELECT p.*, u.email as author_email, (SELECT COUNT(*) FROM forum_comments WHERE post_id = p.id) as comment_count FROM forum_posts p JOIN users u ON p.author_id = u.id WHERE p.school_id = ? ORDER BY p.created_at DESC");
|
||||
$posts->execute([$school_id]);
|
||||
$posts = $posts->fetchAll();
|
||||
// Fetch Forum Posts (Own school or Public from other schools)
|
||||
$posts_query = "
|
||||
SELECT p.*, u.email as author_email, s.name as school_name,
|
||||
(SELECT COUNT(*) FROM forum_comments WHERE post_id = p.id) as comment_count
|
||||
FROM forum_posts p
|
||||
JOIN users u ON p.author_id = u.id
|
||||
JOIN schools s ON p.school_id = s.id
|
||||
WHERE p.school_id = :school_id OR p.is_public = 1
|
||||
ORDER BY p.created_at DESC
|
||||
";
|
||||
$posts_stmt = db()->prepare($posts_query);
|
||||
$posts_stmt->execute(['school_id' => $school_id]);
|
||||
$posts = $posts_stmt->fetchAll();
|
||||
|
||||
$pageTitle = "Teacher Collaboration Hub";
|
||||
$pageTitle = "School Collaboration Hub";
|
||||
include 'includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="container mt-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-people-fill me-2"></i>Collaboration Hub</h2>
|
||||
<div class="btn-group">
|
||||
<div>
|
||||
<h2><i class="bi bi-people-fill me-2 text-primary"></i>Collaboration Hub</h2>
|
||||
<p class="text-muted small mb-0">Share resources and ideas with your school and the wider community.</p>
|
||||
</div>
|
||||
<div class="btn-group shadow-sm">
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadModal">
|
||||
<i class="bi bi-upload me-2"></i>Share Resource
|
||||
</button>
|
||||
@ -71,18 +92,18 @@ include 'includes/header.php';
|
||||
</div>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<?php echo $success; ?>
|
||||
<div class="alert alert-success alert-dismissible fade show shadow-sm" role="alert">
|
||||
<i class="bi bi-check-circle-fill me-2"></i><?php echo $success; ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<ul class="nav nav-tabs mb-4" id="hubTabs" role="tablist">
|
||||
<ul class="nav nav-tabs mb-4 border-0" id="hubTabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active" id="resources-tab" data-bs-toggle="tab" data-bs-target="#resources" type="button" role="tab">Resources</button>
|
||||
<button class="nav-link active fw-bold border-0 bg-transparent px-4" id="resources-tab" data-bs-toggle="tab" data-bs-target="#resources" type="button" role="tab">Resources</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" id="forum-tab" data-bs-toggle="tab" data-bs-target="#forum" type="button" role="tab">Discussion Forum</button>
|
||||
<button class="nav-link fw-bold border-0 bg-transparent px-4" id="forum-tab" data-bs-toggle="tab" data-bs-target="#forum" type="button" role="tab">Discussion Forum</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@ -91,34 +112,42 @@ include 'includes/header.php';
|
||||
<div class="tab-pane fade show active" id="resources" role="tabpanel">
|
||||
<div class="row">
|
||||
<?php if (empty($resources)): ?>
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="bi bi-folder2-open display-1 text-light"></i>
|
||||
<div class="col-12 text-center py-5 bg-light rounded-4">
|
||||
<i class="bi bi-folder2-open display-1 text-muted opacity-25"></i>
|
||||
<p class="mt-3 text-muted">No resources shared yet. Be the first!</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($resources as $res): ?>
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card h-100 shadow-sm border-0 position-relative">
|
||||
<?php if ($res['is_public']): ?>
|
||||
<span class="badge bg-success position-absolute top-0 end-0 m-3 shadow-sm">Public</span>
|
||||
<?php endif; ?>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="badge bg-light text-dark"><?php echo htmlspecialchars($res['subject']); ?></span>
|
||||
<span class="badge bg-light text-dark">Grade <?php echo htmlspecialchars($res['grade']); ?></span>
|
||||
<span class="badge bg-light text-dark border"><?php echo htmlspecialchars($res['subject']); ?></span>
|
||||
<span class="badge bg-light text-dark border">Grade <?php echo htmlspecialchars($res['grade']); ?></span>
|
||||
</div>
|
||||
<h5 class="card-title"><?php echo htmlspecialchars($res['title']); ?></h5>
|
||||
<h5 class="card-title fw-bold"><?php echo htmlspecialchars($res['title']); ?></h5>
|
||||
<p class="card-text text-muted small"><?php echo htmlspecialchars($res['description']); ?></p>
|
||||
<div class="text-muted smaller mb-0">
|
||||
<i class="bi bi-building me-1"></i> <?php echo htmlspecialchars($res['school_name']); ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-white border-0 py-3">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="bi bi-person-circle fs-4 text-primary"></i>
|
||||
<div class="avatar-xs bg-soft-primary text-primary rounded-circle d-flex align-items-center justify-content-center" style="width: 32px; height: 32px; background: rgba(13, 110, 253, 0.1);">
|
||||
<?= strtoupper(substr($res['teacher_email'], 0, 1)) ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-2">
|
||||
<div class="small fw-bold"><?php echo explode('@', $res['teacher_email'])[0]; ?></div>
|
||||
<div class="small fw-bold text-dark"><?php echo explode('@', $res['teacher_email'])[0]; ?></div>
|
||||
<div class="text-muted smaller"><?php echo date('M d, Y', strtotime($res['created_at'])); ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-primary w-100">
|
||||
<i class="bi bi-download me-2"></i>Download Resource
|
||||
<button class="btn btn-sm btn-primary w-100 rounded-pill">
|
||||
<i class="bi bi-download me-2"></i>Download
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -130,32 +159,36 @@ include 'includes/header.php';
|
||||
|
||||
<!-- Forum Tab -->
|
||||
<div class="tab-pane fade" id="forum" role="tabpanel">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="list-group list-group-flush">
|
||||
<div class="card shadow-sm border-0 rounded-4">
|
||||
<div class="list-group list-group-flush rounded-4">
|
||||
<?php if (empty($posts)): ?>
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-chat-left-dots display-1 text-light"></i>
|
||||
<i class="bi bi-chat-left-dots display-1 text-muted opacity-25"></i>
|
||||
<p class="mt-3 text-muted">Start a conversation with your colleagues.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($posts as $post): ?>
|
||||
<a href="#" class="list-group-item list-group-item-action py-4 border-0 border-bottom">
|
||||
<div class="d-flex w-100 justify-content-between align-items-start">
|
||||
<div class="list-group-item list-group-item-action py-4 border-0 border-bottom position-relative">
|
||||
<?php if ($post['is_public']): ?>
|
||||
<span class="badge bg-success-soft text-success border border-success rounded-pill position-absolute top-0 end-0 m-3" style="background: rgba(25, 135, 84, 0.1);">Community</span>
|
||||
<?php endif; ?>
|
||||
<div class="d-flex w-100 justify-content-between align-items-start pe-5">
|
||||
<div class="me-3">
|
||||
<h5 class="mb-1"><?php echo htmlspecialchars($post['title']); ?></h5>
|
||||
<p class="mb-1 text-muted"><?php echo htmlspecialchars(substr($post['content'], 0, 150)) . '...'; ?></p>
|
||||
<div class="small mt-2">
|
||||
<h5 class="mb-1 fw-bold"><?php echo htmlspecialchars($post['title']); ?></h5>
|
||||
<p class="mb-2 text-muted"><?php echo htmlspecialchars(substr($post['content'], 0, 150)) . '...'; ?></p>
|
||||
<div class="small d-flex align-items-center gap-2">
|
||||
<span class="text-primary fw-bold"><?php echo explode('@', $post['author_email'])[0]; ?></span>
|
||||
<span class="text-muted ms-2">• <?php echo date('M d, Y', strtotime($post['created_at'])); ?></span>
|
||||
<span class="text-muted">• <?php echo htmlspecialchars($post['school_name']); ?></span>
|
||||
<span class="text-muted">• <?php echo date('M d, Y', strtotime($post['created_at'])); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<span class="badge rounded-pill bg-light text-primary px-3 py-2">
|
||||
<span class="badge rounded-pill bg-light text-primary px-3 py-2 border">
|
||||
<i class="bi bi-chat-fill me-1"></i> <?php echo $post['comment_count']; ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@ -167,21 +200,21 @@ include 'includes/header.php';
|
||||
<!-- Upload Modal -->
|
||||
<div class="modal fade" id="uploadModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<div class="modal-header border-0">
|
||||
<h5 class="modal-title">Share Learning Resource</h5>
|
||||
<div class="modal-content border-0 shadow-lg rounded-4">
|
||||
<div class="modal-header border-0 pb-0">
|
||||
<h5 class="modal-title fw-bold">Share Learning Resource</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form method="POST">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Resource Title</label>
|
||||
<input type="text" name="title" class="form-control" required placeholder="e.g. Grade 10 Math Exam Prep">
|
||||
<input type="text" name="title" class="form-control rounded-3" required placeholder="e.g. Grade 10 Math Exam Prep">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Grade</label>
|
||||
<select name="grade" class="form-select" required>
|
||||
<select name="grade" class="form-select rounded-3" required>
|
||||
<option value="8">Grade 8</option>
|
||||
<option value="9">Grade 9</option>
|
||||
<option value="10">Grade 10</option>
|
||||
@ -191,22 +224,24 @@ include 'includes/header.php';
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Subject</label>
|
||||
<input type="text" name="subject" class="form-control" required placeholder="e.g. Mathematics">
|
||||
<input type="text" name="subject" class="form-control rounded-3" required placeholder="e.g. Mathematics">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" class="form-control" rows="3" placeholder="Briefly describe the resource..."></textarea>
|
||||
<textarea name="description" class="form-control rounded-3" rows="3" placeholder="Briefly describe the resource..."></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">File (Placeholder)</label>
|
||||
<input type="file" class="form-control" disabled>
|
||||
<small class="text-muted">File upload disabled for prototype.</small>
|
||||
<div class="form-check form-switch p-3 bg-light rounded-3 border">
|
||||
<input class="form-check-input ms-0 me-2" type="checkbox" name="is_public" id="isPublicResource" checked>
|
||||
<label class="form-check-label fw-bold" for="isPublicResource">Make Public (School Collaboration)</label>
|
||||
<div class="form-text mt-0 ms-0">Allow teachers from other schools to see and download this.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" name="upload_resource" class="btn btn-primary">Upload & Share</button>
|
||||
<button type="button" class="btn btn-light rounded-pill px-4" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" name="upload_resource" class="btn btn-primary rounded-pill px-4">Upload & Share</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -216,29 +251,44 @@ include 'includes/header.php';
|
||||
<!-- Post Modal -->
|
||||
<div class="modal fade" id="postModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<div class="modal-header border-0">
|
||||
<h5 class="modal-title">Start Discussion Topic</h5>
|
||||
<div class="modal-content border-0 shadow-lg rounded-4">
|
||||
<div class="modal-header border-0 pb-0">
|
||||
<h5 class="modal-title fw-bold">Start Discussion Topic</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form method="POST">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Topic Title</label>
|
||||
<input type="text" name="title" class="form-control" required placeholder="What do you want to discuss?">
|
||||
<input type="text" name="title" class="form-control rounded-3" required placeholder="What do you want to discuss?">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Content</label>
|
||||
<textarea name="content" class="form-control" rows="5" required placeholder="Share your thoughts or ask a question..."></textarea>
|
||||
<textarea name="content" class="form-control rounded-3" rows="5" required placeholder="Share your thoughts or ask a question..."></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch p-3 bg-light rounded-3 border">
|
||||
<input class="form-check-input ms-0 me-2" type="checkbox" name="is_public" id="isPublicPost" checked>
|
||||
<label class="form-check-label fw-bold" for="isPublicPost">Make Public (School Collaboration)</label>
|
||||
<div class="form-text mt-0 ms-0">Allow the wider teaching community to see and reply.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" name="create_post" class="btn btn-primary">Post Topic</button>
|
||||
<button type="button" class="btn btn-light rounded-pill px-4" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" name="create_post" class="btn btn-primary rounded-pill px-4">Post Topic</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include 'includes/footer.php'; ?>
|
||||
<style>
|
||||
.nav-tabs .nav-link { color: #6c757d; transition: all 0.2s; }
|
||||
.nav-tabs .nav-link.active { color: #0d6efd; border-bottom: 3px solid #0d6efd !important; }
|
||||
.nav-tabs .nav-link:hover { color: #0d6efd; }
|
||||
.smaller { font-size: 0.8rem; }
|
||||
.bg-soft-primary { background-color: rgba(13, 110, 253, 0.1); }
|
||||
</style>
|
||||
|
||||
<?php include 'includes/footer.php'; ?>
|
||||
55
db/migrations/005_gamification_and_school_collab.sql
Normal file
55
db/migrations/005_gamification_and_school_collab.sql
Normal file
@ -0,0 +1,55 @@
|
||||
-- Migration: Add Gamification (Badges) and School Collaboration (Public resources)
|
||||
CREATE TABLE IF NOT EXISTS badges (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
icon VARCHAR(100) DEFAULT 'bi-award',
|
||||
threshold_percent DECIMAL(5,2) DEFAULT NULL, -- Optional mark threshold
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS learner_badges (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
learner_id INT NOT NULL,
|
||||
badge_id INT NOT NULL,
|
||||
awarded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (learner_id) REFERENCES learners(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (badge_id) REFERENCES badges(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Add is_public to resources and forum_posts for cross-school collaboration
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'resources';
|
||||
SET @columnname = 'is_public';
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @dbname
|
||||
AND TABLE_NAME = @tablename
|
||||
AND COLUMN_NAME = @columnname) > 0,
|
||||
'SELECT 1',
|
||||
'ALTER TABLE resources ADD COLUMN is_public BOOLEAN DEFAULT 0'
|
||||
));
|
||||
PREPARE stmt FROM @preparedStatement;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @tablename = 'forum_posts';
|
||||
SET @columnname = 'is_public';
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @dbname
|
||||
AND TABLE_NAME = @tablename
|
||||
AND COLUMN_NAME = @columnname) > 0,
|
||||
'SELECT 1',
|
||||
'ALTER TABLE forum_posts ADD COLUMN is_public BOOLEAN DEFAULT 0'
|
||||
));
|
||||
PREPARE stmt FROM @preparedStatement;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Seed some initial badges
|
||||
INSERT IGNORE INTO badges (name, description, icon, threshold_percent) VALUES
|
||||
('Academic Star', 'Achieved an average of over 80% in assessments.', 'bi-star-fill', 80.00),
|
||||
('Consistent Performer', 'Achieved an average of over 60% in assessments.', 'bi-check-circle-fill', 60.00),
|
||||
('Math Whiz', 'Top performer in Mathematics assessments.', 'bi-calculator', NULL),
|
||||
('Science Explorer', 'Excellence in Science projects.', 'bi-microscope', NULL);
|
||||
@ -64,6 +64,9 @@ $is_logged_in = isset($_SESSION['user_id']);
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= basename($_SERVER['PHP_SELF']) == 'analytics.php' ? 'active' : '' ?>" href="analytics.php">Analytics</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= basename($_SERVER['PHP_SELF']) == 'leaderboard.php' ? 'active' : '' ?>" href="leaderboard.php">Leaderboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= basename($_SERVER['PHP_SELF']) == 'collaboration.php' ? 'active' : '' ?>" href="collaboration.php">Hub</a>
|
||||
</li>
|
||||
@ -82,6 +85,9 @@ $is_logged_in = isset($_SESSION['user_id']);
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= basename($_SERVER['PHP_SELF']) == 'analytics.php' ? 'active' : '' ?>" href="analytics.php">Analytics</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= basename($_SERVER['PHP_SELF']) == 'leaderboard.php' ? 'active' : '' ?>" href="leaderboard.php">Leaderboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= basename($_SERVER['PHP_SELF']) == 'collaboration.php' ? 'active' : '' ?>" href="collaboration.php">Hub</a>
|
||||
</li>
|
||||
@ -100,6 +106,9 @@ $is_logged_in = isset($_SESSION['user_id']);
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= basename($_SERVER['PHP_SELF']) == 'parent.php' ? 'active' : '' ?>" href="parent.php">Parent Portal</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= basename($_SERVER['PHP_SELF']) == 'leaderboard.php' ? 'active' : '' ?>" href="leaderboard.php">Leaderboard</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($is_logged_in): ?>
|
||||
@ -121,4 +130,4 @@ $is_logged_in = isset($_SESSION['user_id']);
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</nav>
|
||||
206
leaderboard.php
Normal file
206
leaderboard.php
Normal file
@ -0,0 +1,206 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once 'db/config.php';
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$db = db();
|
||||
$school_id = $_SESSION['school_id'];
|
||||
|
||||
// Award badges automatically based on performance (Logic)
|
||||
// Academic Star: Average >= 80%
|
||||
// Consistent Performer: Average >= 60%
|
||||
try {
|
||||
// This is a simple logic to auto-award badges for demonstration
|
||||
$stmt = $db->prepare("
|
||||
SELECT l.id, AVG((m.marks_obtained / a.total_marks) * 100) as avg_score
|
||||
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 = ?
|
||||
GROUP BY l.id
|
||||
");
|
||||
$stmt->execute([$school_id]);
|
||||
$performances = $stmt->fetchAll();
|
||||
|
||||
foreach ($performances as $perf) {
|
||||
if ($perf['avg_score'] >= 80) {
|
||||
// Award Academic Star (Badge ID 1)
|
||||
$db->prepare("INSERT IGNORE INTO learner_badges (learner_id, badge_id) VALUES (?, 1)")->execute([$perf['id']]);
|
||||
} elseif ($perf['avg_score'] >= 60) {
|
||||
// Award Consistent Performer (Badge ID 2)
|
||||
$db->prepare("INSERT IGNORE INTO learner_badges (learner_id, badge_id) VALUES (?, 2)")->execute([$perf['id']]);
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Silently fail if something goes wrong with auto-awarding
|
||||
}
|
||||
|
||||
// Fetch Leaderboard (Top learners by average score)
|
||||
$query = "
|
||||
SELECT
|
||||
l.id, l.full_name, l.grade, l.student_id,
|
||||
AVG((m.marks_obtained / a.total_marks) * 100) as average_score,
|
||||
COUNT(m.id) as assessments_count
|
||||
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_score DESC
|
||||
LIMIT 20
|
||||
";
|
||||
$stmt = $db->prepare($query);
|
||||
$stmt->execute(['school_id' => $school_id]);
|
||||
$leaderboard = $stmt->fetchAll();
|
||||
|
||||
// Fetch Badges for these learners
|
||||
$badges_query = "
|
||||
SELECT lb.learner_id, b.name, b.icon, b.description
|
||||
FROM learner_badges lb
|
||||
JOIN badges b ON lb.badge_id = b.id
|
||||
WHERE lb.learner_id IN (SELECT id FROM learners WHERE school_id = ?)
|
||||
";
|
||||
$stmt = $db->prepare($badges_query);
|
||||
$stmt->execute([$school_id]);
|
||||
$all_learner_badges = $stmt->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_ASSOC);
|
||||
|
||||
$pageTitle = "Learner Leaderboard | SOMS";
|
||||
include 'includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="container py-5">
|
||||
<div class="text-center mb-5">
|
||||
<h1 class="display-5 fw-bold mb-2"><i class="bi bi-trophy text-warning me-2"></i>Academic Leaderboard</h1>
|
||||
<p class="lead text-muted">Celebrating excellence and consistent growth in our school.</p>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10">
|
||||
<div class="card shadow border-0 overflow-hidden">
|
||||
<div class="card-header bg-primary text-white py-3">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
<span class="badge bg-white text-primary">Top 20</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h5 class="mb-0">Current Rankings</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="ps-4" style="width: 80px;">Rank</th>
|
||||
<th>Learner</th>
|
||||
<th>Grade</th>
|
||||
<th>Avg. Score</th>
|
||||
<th>Achievements</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($leaderboard)): ?>
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-5 text-muted">
|
||||
No assessment data available yet to generate rankings.
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php foreach ($leaderboard as $index => $row): ?>
|
||||
<?php
|
||||
$rank = $index + 1;
|
||||
$score = round($row['average_score'], 1);
|
||||
$row_badges = $all_learner_badges[$row['id']] ?? [];
|
||||
|
||||
$rankClass = '';
|
||||
if ($rank === 1) $rankClass = 'rank-1';
|
||||
elseif ($rank === 2) $rankClass = 'rank-2';
|
||||
elseif ($rank === 3) $rankClass = 'rank-3';
|
||||
?>
|
||||
<tr class="<?= $rank <= 3 ? 'table-light' : '' ?>">
|
||||
<td class="ps-4">
|
||||
<?php if ($rank === 1): ?>
|
||||
<div class="rank-circle bg-warning text-dark"><i class="bi bi-award-fill"></i></div>
|
||||
<?php elseif ($rank === 2): ?>
|
||||
<div class="rank-circle bg-secondary text-white">2</div>
|
||||
<?php elseif ($rank === 3): ?>
|
||||
<div class="rank-circle bg-bronze text-white">3</div>
|
||||
<?php else: ?>
|
||||
<span class="text-muted fw-bold"><?= $rank ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="avatar-sm bg-primary text-white rounded-circle me-3 d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
|
||||
<?= strtoupper(substr($row['full_name'], 0, 1)) ?>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-bold"><?= htmlspecialchars($row['full_name']) ?></div>
|
||||
<div class="text-muted smaller"><?= htmlspecialchars($row['student_id']) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="badge bg-light text-dark border">Grade <?= htmlspecialchars($row['grade']) ?></span></td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="progress flex-grow-1 me-3" style="height: 6px; min-width: 60px;">
|
||||
<div class="progress-bar bg-primary" role="progressbar" style="width: <?= $score ?>%"></div>
|
||||
</div>
|
||||
<span class="fw-bold"><?= $score ?>%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<?php if (empty($row_badges)): ?>
|
||||
<span class="text-muted smaller">No badges yet</span>
|
||||
<?php else: ?>
|
||||
<?php foreach ($row_badges as $badge): ?>
|
||||
<span class="badge bg-soft-warning text-warning border border-warning" data-bs-toggle="tooltip" title="<?= htmlspecialchars($badge['description']) ?>">
|
||||
<i class="bi <?= htmlspecialchars($badge['icon']) ?> me-1"></i>
|
||||
<?= htmlspecialchars($badge['name']) ?>
|
||||
</span>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.rank-circle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.bg-bronze { background-color: #cd7f32; }
|
||||
.bg-soft-warning { background-color: rgba(255, 193, 7, 0.1); }
|
||||
.smaller { font-size: 0.75rem; }
|
||||
|
||||
.rank-1 { background-color: rgba(255, 193, 7, 0.05); }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl)
|
||||
})
|
||||
</script>
|
||||
|
||||
<?php include 'includes/footer.php'; ?>
|
||||
232
public/app.js
Normal file
232
public/app.js
Normal file
@ -0,0 +1,232 @@
|
||||
const API_URL = '/api/v1/index.php?request=';
|
||||
|
||||
const state = {
|
||||
user: JSON.parse(localStorage.getItem('user')) || null,
|
||||
token: localStorage.getItem('token') || null,
|
||||
};
|
||||
|
||||
const routes = {
|
||||
'/': homePage,
|
||||
'/login': loginPage,
|
||||
'/learners': learnersPage,
|
||||
'/assessments': assessmentsPage,
|
||||
};
|
||||
|
||||
async function init() {
|
||||
window.addEventListener('hashchange', router);
|
||||
router();
|
||||
updateNav();
|
||||
}
|
||||
|
||||
function router() {
|
||||
const hash = window.location.hash || '#/';
|
||||
const path = hash.substring(1);
|
||||
const page = routes[path] || routes['/'];
|
||||
page();
|
||||
}
|
||||
|
||||
function updateNav() {
|
||||
const navLinks = document.getElementById('nav-links');
|
||||
if (!state.token) {
|
||||
navLinks.innerHTML = `
|
||||
<li class="nav-item"><a class="nav-link" href="#/login">Login</a></li>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
navLinks.innerHTML = `
|
||||
<li class="nav-item"><a class="nav-link" href="#/">Dashboard</a></li>
|
||||
<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>
|
||||
`;
|
||||
|
||||
document.getElementById('logout-btn').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
logout();
|
||||
});
|
||||
}
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
state.token = null;
|
||||
state.user = null;
|
||||
window.location.hash = '#/login';
|
||||
updateNav();
|
||||
}
|
||||
|
||||
async function apiFetch(endpoint, options = {}) {
|
||||
const url = API_URL + endpoint;
|
||||
const defaultOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(state.token ? { 'Authorization': `Bearer ${state.token}` } : {})
|
||||
}
|
||||
};
|
||||
const response = await fetch(url, { ...defaultOptions, ...options });
|
||||
const data = await response.json();
|
||||
if (response.status === 401) {
|
||||
logout();
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function render(html) {
|
||||
document.getElementById('content').innerHTML = 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>
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
function loginPage() {
|
||||
render(`
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-5">
|
||||
<div class="card shadow-lg border-0">
|
||||
<div class="card-body p-5">
|
||||
<h2 class="fw-bold text-center mb-4">Sign In</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>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Password</label>
|
||||
<input type="password" id="password" class="form-control" value="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
document.getElementById('login-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const email = document.getElementById('email').value;
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
try {
|
||||
const data = await apiFetch('/auth/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
|
||||
if (data.token) {
|
||||
localStorage.setItem('token', data.token);
|
||||
localStorage.setItem('user', JSON.stringify(data.user));
|
||||
state.token = data.token;
|
||||
state.user = data.user;
|
||||
updateNav();
|
||||
window.location.hash = '#/';
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Login failed: ' + err.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function learnersPage() {
|
||||
render('<div class="text-center"><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>
|
||||
</div>
|
||||
<div class="table-responsive bg-white p-3 rounded shadow-sm">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Full Name</th>
|
||||
<th>Grade</th>
|
||||
<th>Student ID</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
learners.forEach(l => {
|
||||
html += `
|
||||
<tr>
|
||||
<td>${l.full_name}</td>
|
||||
<td>${l.grade}</td>
|
||||
<td>${l.student_id}</td>
|
||||
<td><button class="btn btn-sm btn-outline-secondary">Edit</button></td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
html += '</tbody></table></div>';
|
||||
render(html);
|
||||
} catch (err) {
|
||||
render('<div class="alert alert-danger">Failed to load learners</div>');
|
||||
}
|
||||
}
|
||||
|
||||
async function assessmentsPage() {
|
||||
render('<div class="text-center"><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>
|
||||
</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-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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
render(html);
|
||||
} catch (err) {
|
||||
render('<div class="alert alert-danger">Failed to load assessments</div>');
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
100
public/assets/css/custom.css
Normal file
100
public/assets/css/custom.css
Normal file
@ -0,0 +1,100 @@
|
||||
:root {
|
||||
--primary-color: #004D40;
|
||||
--primary-light: #00695C;
|
||||
--secondary-color: #FFC107;
|
||||
--bg-color: #F8F9FA;
|
||||
--surface-color: #FFFFFF;
|
||||
--text-main: #212529;
|
||||
--text-muted: #6C757D;
|
||||
--border-color: #DEE2E6;
|
||||
--border-radius: 4px;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-main);
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background-color: var(--primary-color) !important;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
border-radius: var(--border-radius);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-light);
|
||||
border-color: var(--primary-light);
|
||||
}
|
||||
|
||||
.table {
|
||||
background-color: var(--surface-color);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background-color: #F1F3F5;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.05em;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.attendance-toggle {
|
||||
cursor: pointer;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.attendance-toggle.present {
|
||||
background-color: #E8F5E9;
|
||||
color: #2E7D32;
|
||||
border: 1px solid #A5D6A7;
|
||||
}
|
||||
|
||||
.attendance-toggle.absent {
|
||||
background-color: #FFEBEE;
|
||||
color: #C62828;
|
||||
border: 1px solid #EF9A9A;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
border-left: 4px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.stats-card h3 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stats-card p {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
87
public/assets/js/main.js
Normal file
87
public/assets/js/main.js
Normal file
@ -0,0 +1,87 @@
|
||||
// Service Worker Registration for Offline Capability
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
.then(reg => {
|
||||
console.log('Service Worker registered');
|
||||
// Check for updates
|
||||
reg.onupdatefound = () => {
|
||||
const installingWorker = reg.installing;
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||
console.log('New content is available; please refresh.');
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch(err => console.log('Service Worker registration failed', err));
|
||||
});
|
||||
}
|
||||
|
||||
// Online/Offline Status
|
||||
window.addEventListener('online', updateOnlineStatus);
|
||||
window.addEventListener('offline', updateOnlineStatus);
|
||||
|
||||
function updateOnlineStatus() {
|
||||
const status = navigator.onLine ? 'online' : 'offline';
|
||||
console.log('Status changed to:', status);
|
||||
|
||||
// Create/Update UI indicator if it doesn't exist
|
||||
let indicator = document.getElementById('offline-indicator');
|
||||
if (!navigator.onLine) {
|
||||
if (!indicator) {
|
||||
indicator = document.createElement('div');
|
||||
indicator.id = 'offline-indicator';
|
||||
indicator.className = 'alert alert-warning fixed-bottom m-0 text-center rounded-0';
|
||||
indicator.style.zIndex = '9999';
|
||||
indicator.innerHTML = '<i class="bi bi-wifi-off me-2"></i> You are currently offline. Changes will be synced when you reconnect.';
|
||||
document.body.appendChild(indicator);
|
||||
}
|
||||
} else {
|
||||
if (indicator) {
|
||||
indicator.remove();
|
||||
// Show brief "Back online" message
|
||||
const onlineToast = document.createElement('div');
|
||||
onlineToast.className = 'alert alert-success fixed-bottom m-0 text-center rounded-0';
|
||||
onlineToast.style.zIndex = '9999';
|
||||
onlineToast.innerHTML = '<i class="bi bi-wifi me-2"></i> Back online! Syncing data...';
|
||||
document.body.appendChild(onlineToast);
|
||||
setTimeout(() => onlineToast.remove(), 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initial check
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
updateOnlineStatus();
|
||||
|
||||
// Attendance Table Search (Re-initialize if present)
|
||||
const learnerSearch = document.getElementById('learnerSearch');
|
||||
if (learnerSearch) {
|
||||
learnerSearch.addEventListener('keyup', function() {
|
||||
let filter = this.value.toUpperCase();
|
||||
let rows = document.querySelector("#learnersTable tbody").rows;
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
let nameCol = rows[i].cells[0].textContent.toUpperCase();
|
||||
let idCol = rows[i].cells[2].textContent.toUpperCase();
|
||||
if (nameCol.indexOf(filter) > -1 || idCol.indexOf(filter) > -1) {
|
||||
rows[i].style.display = "";
|
||||
} else {
|
||||
rows[i].style.display = "none";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Attendance Toggle Feedback
|
||||
const tableRows = document.querySelectorAll('#learnersTable tbody tr');
|
||||
tableRows.forEach(row => {
|
||||
const inputs = row.querySelectorAll('input[type="radio"]');
|
||||
inputs.forEach(input => {
|
||||
input.addEventListener('change', () => {
|
||||
row.classList.add('table-light');
|
||||
setTimeout(() => row.classList.remove('table-light'), 500);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
41
public/index.html
Normal file
41
public/index.html
Normal file
@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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="assets/css/custom.css">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<div id="app">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary shadow-sm mb-4">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-bold" href="#/">SOMS Platform</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto" id="nav-links">
|
||||
<!-- Links will be injected here -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container">
|
||||
<div id="content">
|
||||
<div class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
21
public/manifest.json
Normal file
21
public/manifest.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "SOMS Platform",
|
||||
"short_name": "SOMS",
|
||||
"start_url": "./",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#0d6efd",
|
||||
"description": "Modern School Management for Township Schools",
|
||||
"icons": [
|
||||
{
|
||||
"src": "https://placehold.co/192x192/0d6efd/ffffff?text=SOMS",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "https://placehold.co/512x512/0d6efd/ffffff?text=SOMS",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
39
public/sw.js
Normal file
39
public/sw.js
Normal file
@ -0,0 +1,39 @@
|
||||
const CACHE_NAME = 'soms-v1-static';
|
||||
const API_CACHE_NAME = 'soms-v1-api';
|
||||
|
||||
const STATIC_ASSETS = [
|
||||
'./',
|
||||
'./index.html',
|
||||
'./app.js',
|
||||
'./assets/css/custom.css',
|
||||
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css'
|
||||
];
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS))
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
// API requests strategy: Network first, then cache
|
||||
if (url.searchParams.has('request')) {
|
||||
event.respondWith(
|
||||
fetch(event.request)
|
||||
.then((response) => {
|
||||
const clonedResponse = response.clone();
|
||||
caches.open(API_CACHE_NAME).then((cache) => cache.put(event.request, clonedResponse));
|
||||
return response;
|
||||
})
|
||||
.catch(() => caches.match(event.request))
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Static assets strategy: Cache first, then network
|
||||
event.respondWith(
|
||||
caches.match(event.request).then((response) => response || fetch(event.request))
|
||||
);
|
||||
});
|
||||
5
sw.js
5
sw.js
@ -1,4 +1,4 @@
|
||||
const CACHE_NAME = 'township-schools-v7';
|
||||
const CACHE_NAME = 'township-schools-v8';
|
||||
const STATIC_ASSETS = [
|
||||
'/',
|
||||
'/index.php',
|
||||
@ -13,6 +13,7 @@ const STATIC_ASSETS = [
|
||||
'/notifications.php',
|
||||
'/analytics.php',
|
||||
'/collaboration.php',
|
||||
'/leaderboard.php',
|
||||
'/events.php',
|
||||
'/parent.php',
|
||||
'/assets/css/custom.css',
|
||||
@ -68,4 +69,4 @@ self.addEventListener('fetch', (event) => {
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user