APP CODE
Weka hivi kwenye root ya project yako:
/assets/icons/
icon-72.png
icon-96.png
icon-128.png
icon-144.png
icon-152.png
icon-192.png
icon-384.png
icon-512.png
maskable-192.png
maskable-512.png
/manifest.webmanifest
/sw.js
/offline.html
Ukihitaji, naweza pia kukupa script ya ku-generate icons sizes zote ukitumia icon moja (PNG).
2) manifest.webmanifest (badilisha jina tu na colors)
Tengeneza file: manifest.webmanifest
{
"name": "EduScore",
"short_name": "EduScore",
"description": "Smart school results & performance system.",
"start_url": "/?source=pwa",
"scope": "/",
"display": "standalone",
"background_color": "#0b1b3a",
"theme_color": "#0b1b3a",
"lang": "sw",
"dir": "ltr",
"orientation": "portrait",
"icons": [
{ "src": "/assets/icons/icon-72.png", "sizes": "72x72", "type": "image/png" },
{ "src": "/assets/icons/icon-96.png", "sizes": "96x96", "type": "image/png" },
{ "src": "/assets/icons/icon-128.png", "sizes": "128x128", "type": "image/png" },
{ "src": "/assets/icons/icon-144.png", "sizes": "144x144", "type": "image/png" },
{ "src": "/assets/icons/icon-152.png", "sizes": "152x152", "type": "image/png" },
{ "src": "/assets/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/assets/icons/icon-384.png", "sizes": "384x384", "type": "image/png" },
{ "src": "/assets/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/assets/icons/maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" },
{ "src": "/assets/icons/maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
]
}
3) sw.js (Service Worker ya PRO: offline + update + runtime caching)
Tengeneza file: sw.js
/* sw.js - Production-ready PWA Service Worker (no libraries) */
'use strict';
const SW_VERSION = 'v1.0.0'; // BADILISHA uki-update ili force refresh
const CACHE_STATIC = `static-${SW_VERSION}`;
const CACHE_PAGES = `pages-${SW_VERSION}`;
const CACHE_ASSETS = `assets-${SW_VERSION}`;
const OFFLINE_URL = '/offline.html';
// Static muhimu za app (ongeza/ondoa kulingana na mfumo wako)
const STATIC_ASSETS = [
OFFLINE_URL,
'/manifest.webmanifest'
];
// --- Install: cache essentials
self.addEventListener('install', (event) => {
event.waitUntil((async () => {
const cache = await caches.open(CACHE_STATIC);
await cache.addAll(STATIC_ASSETS);
self.skipWaiting();
})());
});
// --- Activate: clean old caches
self.addEventListener('activate', (event) => {
event.waitUntil((async () => {
const keys = await caches.keys();
await Promise.all(
keys
.filter((k) => ![CACHE_STATIC, CACHE_PAGES, CACHE_ASSETS].includes(k))
.map((k) => caches.delete(k))
);
await self.clients.claim();
})());
});
// Helper: decide if request is navigation (HTML page)
const isNavigationRequest = (req) =>
req.mode === 'navigate' ||
(req.method === 'GET' && req.headers.get('accept') && req.headers.get('accept').includes('text/html'));
// Helper: cache-first for assets (css/js/images/fonts)
async function cacheFirst(req) {
const cache = await caches.open(CACHE_ASSETS);
const cached = await cache.match(req);
if (cached) return cached;
const res = await fetch(req);
// cache only OK
if (res && res.ok) cache.put(req, res.clone());
return res;
}
// Helper: network-first for pages (keeps content fresh, falls back offline)
async function networkFirst(req) {
const cache = await caches.open(CACHE_PAGES);
try {
const res = await fetch(req);
if (res && res.ok) cache.put(req, res.clone());
return res;
} catch (e) {
const cached = await cache.match(req);
return cached || caches.match(OFFLINE_URL);
}
}
// --- Fetch strategy
self.addEventListener('fetch', (event) => {
const req = event.request;
// Ignore non-GET
if (req.method !== 'GET') return;
const url = new URL(req.url);
// Only handle same-origin (avoid issues with CDNs unless unataka)
if (url.origin !== self.location.origin) return;
// Pages: network-first
if (isNavigationRequest(req)) {
event.respondWith(networkFirst(req));
return;
}
// Assets: cache-first
const isAsset =
url.pathname.startsWith('/assets/') ||
['.css', '.js', '.png', '.jpg', '.jpeg', '.webp', '.svg', '.ico', '.woff', '.woff2', '.ttf'].some(ext => url.pathname.endsWith(ext));
if (isAsset) {
event.respondWith(cacheFirst(req));
return;
}
// Default: try network, fallback cache
event.respondWith((async () => {
const cached = await caches.match(req);
try {
const res = await fetch(req);
return res;
} catch (e) {
return cached || caches.match(OFFLINE_URL);
}
})());
});
// Optional: Listen for skipWaiting message (instant update)
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
4) offline.html (ukurasa wa offline wa professional)
Tengeneza file: offline.html
<!doctype html>
<html lang="sw">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Upo Offline</title>
<meta name="theme-color" content="#0b1b3a" />
<style>
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial; margin:0; background:#0b1b3a; color:#fff;}
.wrap{min-height:100vh; display:flex; align-items:center; justify-content:center; padding:24px;}
.card{max-width:560px; width:100%; background:rgba(255,255,255,.08); border:1px solid rgba(255,255,255,.15);
border-radius:16px; padding:22px; box-shadow:0 10px 30px rgba(0,0,0,.35);}
h1{margin:0 0 10px; font-size:22px;}
p{margin:0 0 16px; opacity:.9; line-height:1.5;}
button{border:0; background:#ffffff; color:#0b1b3a; padding:10px 14px; border-radius:12px; cursor:pointer; font-weight:700;}
.small{margin-top:12px; font-size:13px; opacity:.75;}
</style>
</head>
<body>
<div class="wrap">
<div class="card">
<h1>Upo Offline</h1>
<p>Inaonekana hakuna intaneti kwa sasa. Ukirudi online, app itaji-refresh yenyewe.</p>
<button onclick="location.reload()">Jaribu Tena</button>
<div class="small">Tip: Hakikisha data muhimu zinahifadhiwa kabla ya kutoka.</div>
</div>
</div>
</body>
</html>
5) Code za kuweka kwenye index.php / index.html (ili PWA ifanye kazi 100%)
Weka hizi ndani ya <head>:
<link rel="manifest" href="/manifest.webmanifest">
<meta name="theme-color" content="#0b1b3a">
<!-- iOS support (optional but pro) -->
<link rel="apple-touch-icon" href="/assets/icons/icon-192.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="EduScore">
Kisha kabla ya </body> weka hii script (register SW + install prompt + update banner):
<script>
(function () {
'use strict';
// --- Register Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const reg = await navigator.serviceWorker.register('/sw.js', { scope: '/' });
// If there's an update waiting, you can show a UI
reg.addEventListener('updatefound', () => {
const newWorker = reg.installing;
if (!newWorker) return;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// New update available
showUpdateToast(() => {
newWorker.postMessage({ type: 'SKIP_WAITING' });
});
}
}
});
});
// Reload page when SW takes control after skipWaiting
navigator.serviceWorker.addEventListener('controllerchange', () => {
window.location.reload();
});
} catch (e) {
// Optional: console only
console.warn('SW registration failed:', e);
}
});
}
// --- Install prompt (Android/Chrome/Edge)
let deferredPrompt = null;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
showInstallToast(async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
await deferredPrompt.userChoice;
deferredPrompt = null;
});
});
// --- Minimal "toast" UI (no libs)
function toast(html, actionText, onAction) {
const id = 'pwa-toast';
const old = document.getElementById(id);
if (old) old.remove();
const el = document.createElement('div');
el.id = id;
el.style.cssText = `
position:fixed; left:16px; right:16px; bottom:16px; z-index:99999;
background:#0b1b3a; color:#fff; border:1px solid rgba(255,255,255,.2);
padding:12px 14px; border-radius:14px; display:flex; gap:12px; align-items:center;
box-shadow:0 10px 30px rgba(0,0,0,.35); font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;
`;
const msg = document.createElement('div');
msg.style.flex = '1';
msg.innerHTML = html;
const btn = document.createElement('button');
btn.textContent = actionText;
btn.style.cssText = `
border:0; background:#fff; color:#0b1b3a; padding:8px 12px;
border-radius:12px; cursor:pointer; font-weight:700;
`;
btn.onclick = () => { try { onAction && onAction(); } finally { el.remove(); } };
const close = document.createElement('button');
close.textContent = '×';
close.setAttribute('aria-label', 'Close');
close.style.cssText = `
border:0; background:transparent; color:#fff; font-size:22px; line-height:1;
cursor:pointer; opacity:.8;
`;
close.onclick = () => el.remove();
el.appendChild(msg);
el.appendChild(btn);
el.appendChild(close);
document.body.appendChild(el);
}
function showInstallToast(onInstall) {
toast('Weka <b>App</b> kwenye simu/PC yako ili ifunguke haraka.', 'Install', onInstall);
}
function showUpdateToast(onUpdate) {
toast('Kuna <b>update</b> mpya ya app. Bonyeza ku-refresh.', 'Update', onUpdate);
}
})();
</script>
6) Vitu vya “professional” vya kuzingatia (vinavyosababisha 100% experience)
Icons: hakikisha icon-192.png na icon-512.png zipo. (Maskable pia ni pro.)
start_url: tumia / au /index.php kulingana na mfumo wako.
Cache: kama una pages za admin zilizo sensitive (mfano /admin/), unaweza ku-avoid kuzicache.
Logout/Login: kwa mfumo wa sessions, “offline cache” inaweza kuonyesha page ya mwisho; hii ni normal. Ukihitaji stricter, tunaweka rules.
Unachobadili tu ili iwe ya “mfumo wowote”
manifest.webmanifest → name, short_name, theme_color, icons paths
index head meta → apple-mobile-web-app-title
SW_VERSION ndani ya sw.js kila ukitoa update