Files
bel-sekolah-esp32/data/index.html

422 lines
9.8 KiB
HTML

<!-- data/index.html -->
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Bel Sekolah</title>
<style>
/* Inline Tailwind CSS for offline compatibility */
.bg-gradient-to-b {
background-image: linear-gradient(to bottom, var(--tw-gradient-stops));
}
.from-slate-50 {
--tw-gradient-from: #f8fafc;
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(248, 250, 252, 0));
}
.to-white {
--tw-gradient-to: #ffffff;
}
.min-h-screen {
min-height: 100vh;
}
.font-sans {
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
}
.text-slate-800 {
--tw-text-opacity: 1;
color: rgb(30 41 59 / var(--tw-text-opacity));
}
.max-w-2xl {
max-width: 42rem;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.p-6 {
padding: 1.5rem;
}
.text-center {
text-align: center;
}
.mb-6 {
margin-bottom: 1.5rem;
}
.text-5xl {
font-size: 3rem;
line-height: 1;
}
.text-3xl {
font-size: 1.875rem;
line-height: 2.25rem;
}
.font-bold {
font-weight: 700;
}
.text-blue-600 {
--tw-text-opacity: 1;
color: rgb(37 99 235 / var(--tw-text-opacity));
}
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.text-slate-600 {
--tw-text-opacity: 1;
color: rgb(71 85 105 / var(--tw-text-opacity));
}
.mt-1 {
margin-top: 0.25rem;
}
.bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.shadow {
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-width 0px), var(--tw-ring-shadow), var(--tw-shadow);
}
.rounded-xl {
border-radius: 0.75rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.mb-1 {
margin-bottom: 0.25rem;
}
.text-5xl {
font-size: 3rem;
line-height: 1;
}
.font-extrabold {
font-weight: 800;
}
.tracking-wider {
letter-spacing: 0.05em;
}
.text-slate-500 {
--tw-text-opacity: 1;
color: rgb(100 116 139 / var(--tw-text-opacity));
}
.mt-2 {
margin-top: 0.5rem;
}
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.gap-4 {
gap: 1rem;
}
.my-3 {
margin-top: 0.75rem;
margin-bottom: 0.75rem;
}
.w-3 {
width: 0.75rem;
}
.h-3 {
height: 0.75rem;
}
.rounded-full {
border-radius: 9999px;
}
.bg-gray-300 {
--tw-bg-opacity: 1;
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
}
.text-slate-700 {
--tw-text-opacity: 1;
color: rgb(51 65 85 / var(--tw-text-opacity));
}
.grid {
display: grid;
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.sm\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.gap-3 {
gap: 0.75rem;
}
.mt-6 {
margin-top: 1.5rem;
}
.max-w-md {
max-width: 28rem;
}
.bg-slate-50 {
--tw-bg-opacity: 1;
background-color: rgb(248 250 252 / var(--tw-bg-opacity));
}
.p-3 {
padding: 0.75rem;
}
.rounded {
border-radius: 0.25rem;
}
.h-40 {
height: 10rem;
}
.overflow-auto {
overflow: auto;
}
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.text-slate-500 {
--tw-text-opacity: 1;
color: rgb(100 116 139 / var(--tw-text-opacity));
}
.mt-6 {
margin-top: 1.5rem;
}
/* Button styles */
.btn {
display: inline-block;
padding: 0.5rem 0.9rem;
border-radius: 8px;
color: white;
font-weight: 600;
text-decoration: none;
cursor: pointer;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
}
.btn.small {
padding: 0.25rem 0.5rem;
font-size: 0.9rem;
}
.btn.btn-blue {
background: #2563EB;
}
.btn.btn-green {
background: #059669;
}
.btn.btn-red {
background: #DC2626;
}
.btn.btn-gray {
background: #6B7280;
}
.btn.btn-purple {
background: #7C3AED;
}
.btn:hover {
filter: brightness(0.95);
}
/* Responsive */
@media (max-width: 640px) {
.btn {
padding: 0.45rem 0.7rem;
font-size: 0.9rem;
}
}
</style>
</head>
<body class="bg-gradient-to-b from-slate-50 to-white min-h-screen font-sans text-slate-800">
<div class="max-w-2xl mx-auto p-6">
<header class="text-center mb-6">
<div class="text-5xl">🔔</div>
<h1 class="text-3xl font-bold text-blue-600">Bel Sekolah</h1>
<div id="schoolName" class="text-sm text-slate-600 mt-1">SMA Negeri</div>
</header>
<main class="bg-white shadow rounded-xl p-6">
<div class="text-center mb-4">
<div id="hari" class="text-lg text-slate-600 mb-1"></div>
<div id="clock" class="text-5xl font-extrabold tracking-wider">--.--.--</div>
<div id="tanggal" class="text-slate-500 mt-2"></div>
</div>
<div class="flex items-center justify-center gap-4 my-3">
<div id="statusDot" class="w-3 h-3 rounded-full bg-gray-300"></div>
<div id="statusText" class="text-slate-700">Memuat...</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 mt-6 max-w-md mx-auto">
<button id="btnTest" class="btn btn-green">Tes Bel</button>
<button id="btnStop" class="btn btn-red">Stop</button>
<a href="/setting.html" class="btn btn-blue">Pengaturan</a>
</div>
<div id="log" class="mt-6 bg-slate-50 p-3 rounded h-40 overflow-auto text-sm text-slate-700"></div>
</main>
<footer class="text-center text-slate-500 text-xs mt-6">
© Wartana 2025 — Bel Sekolah Otomatis
</footer>
</div>
<script>
// WebSocket connection (uses same host)
const protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://';
const ws = new WebSocket(protocol + location.host + '/ws');
const hariEl = document.getElementById('hari');
const clockEl = document.getElementById('clock');
const tanggalEl = document.getElementById('tanggal');
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
const schoolEl = document.getElementById('schoolName');
const logEl = document.getElementById('log');
function appendLog(s) {
const t = document.createElement('div');
t.textContent = (new Date()).toLocaleTimeString() + " — " + s;
logEl.prepend(t);
}
ws.onopen = () => {
appendLog("🔌 WebSocket tersambung.");
};
ws.onclose = () => {
appendLog("⚠️ WebSocket terputus.");
statusDot.style.background = 'gray';
statusText.textContent = 'Terputus';
};
ws.onmessage = (ev) => {
try {
const msg = JSON.parse(ev.data);
if (msg.type === 'time') {
const h = String(msg.jam).padStart(2, '0');
const m = String(msg.menit).padStart(2, '0');
const s = String(msg.detik).padStart(2, '0');
clockEl.textContent = `${h}.${m}.${s}`;
const weekday = ["Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"];
hariEl.textContent = weekday[msg.weekday || new Date(msg.epoch * 1000).getDay()];
const monthNames = ["Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember"];
tanggalEl.textContent = `${msg.hari} ${monthNames[(msg.bulan || (new Date(msg.epoch * 1000)).getMonth()) - 1]} ${msg.tahun}`;
} else if (msg.type === 'status') {
if (msg.playing) {
statusDot.style.background = 'green';
statusText.textContent = `Bel berbunyi (Track ${msg.track})`;
} else {
statusDot.style.background = 'gray';
statusText.textContent = 'Bel siaga';
}
if (msg.schoolName) schoolEl.textContent = msg.schoolName;
// Update clients info
const clientsInfoEl = document.getElementById('clientsInfo');
if (msg.clientList && msg.clientList.length > 0) {
let info = '';
msg.clientList.forEach((client, index) => {
if (index > 0) info += ', ';
info += `${client.mac}: ${client.percentage}%`;
});
clientsInfoEl.textContent = info;
} else {
clientsInfoEl.textContent = '';
}
} else if (msg.type === 'log') {
appendLog(msg.msg);
}
} catch (e) {
console.error("ws onmessage parse err", e);
}
};
// Buttons
document.getElementById('btnTest').addEventListener('click', async () => {
try {
// Fetch testTrack from config first
const configRes = await fetch('/api/config');
const config = await configRes.json();
const track = config.testTrack || 1000;
await fetch(`/api/play?track=${track}`);
} catch (e) { console.error(e); }
});
document.getElementById('btnStop').addEventListener('click', async () => {
try {
await fetch('/api/stop');
} catch (e) { console.error(e); }
});
</script>
</body>
</html>