Initial commit projek TA Aquascape

This commit is contained in:
developer 2026-07-28 13:29:36 +07:00
commit 83af86cde4
19 changed files with 2883 additions and 0 deletions

0
aquascape/dashboard.php Normal file
View File

950
assets/css/custom.css Normal file
View File

@ -0,0 +1,950 @@
/* Reset Basic */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background-color: #f4f7f6;
color: #333;
}
/* Warna Utama */
.bg-tosca {
background-color: #5FBDBD;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Navbar */
.navbar {
display: flex;
justify-content: space-between;
padding: 15px 30px;
background: #e0f2f1; /* Sedikit terang menyesuaikan gambar */
font-weight: bold;
}
.navbar .logo {
font-weight: 900;
font-size: 1.2rem;
color: #333;
}
.navbar .nav-links a {
text-decoration: none;
color: #333;
margin-left: 20px;
font-size: 14px;
transition: color 0.3s;
}
.navbar .nav-links a:hover {
color: #5FBDBD;
}
/* Hero Section */
.hero {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-size: 2.5rem;
font-style: italic;
font-weight: 600;
}
/* Cards (Umum) */
.card {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
/* Auth Pages (Login/Register) */
.auth-container {
display: flex;
justify-content: center;
align-items: center;
height: calc(100vh - 60px); /* Kurangi tinggi navbar */
background-color: transparent;
}
.auth-card {
width: 320px;
text-align: center;
border-radius: 20px;
}
.auth-card h2 {
margin-bottom: 25px;
font-weight: 800;
}
.auth-card input {
width: 100%;
padding: 12px;
margin-bottom: 15px;
border: none;
background: #e0e0e0;
border-radius: 20px;
outline: none;
text-align: center;
font-weight: 500;
}
.auth-card button {
padding: 10px 40px;
border: none;
background: #ddd;
border-radius: 20px;
cursor: pointer;
font-weight: bold;
transition: background 0.3s;
}
.auth-card button:hover {
background: #ccc;
}
.message {
font-size: 12px;
margin-bottom: 15px;
}
.msg-error { color: red; }
.msg-success { color: green; }
/* Dashboard Layout */
.dashboard-layout {
display: flex;
height: 100vh;
}
/* Sidebar */
.sidebar {
width: 250px;
background: #e0f2f1;
padding: 20px;
}
.sidebar h3 {
margin-bottom: 30px;
font-size: 1.1rem;
font-weight: 800;
letter-spacing: 1px;
}
.sidebar ul {
list-style: none;
}
.sidebar ul li {
margin-bottom: 15px;
}
.sidebar ul li a {
text-decoration: none;
color: #555;
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
}
.sidebar ul li.active a {
color: #333;
font-weight: bold;
/* Indikator aktif mirip gambar */
position: relative;
}
/* Main Content */
.main-content {
flex: 1;
background: #5FBDBD;
padding: 30px;
overflow-y: auto;
}
/* Sensor Cards */
.cards-grid {
display: flex;
gap: 20px;
margin-bottom: 20px;
flex-wrap: wrap;
justify-content: center;
}
.sensor-card {
background: #e0f2f1;
border-radius: 15px;
padding: 20px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 150px;
height: 150px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.sensor-val {
font-size: 1.2rem;
font-weight: bold;
background: white;
border-radius: 50%;
width: 70px;
height: 70px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10px;
}
/* Chart Container */
.chart-container {
background: white;
border-radius: 15px;
padding: 20px;
height: 300px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin: 20px auto;
max-width: 600px;
}
/* Control Settings */
.controls-container {
background: #e0f2f1;
border-radius: 15px;
padding: 20px;
margin-top: 20px;
max-width: 400px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
/* Toggle Switch */
.switch {
position: relative;
display: inline-block;
width: 40px;
height: 20px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 2px;
bottom: 2px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #333;
}
input:checked + .slider:before {
transform: translateX(20px);
}
/* ==== TAMBAHAN CSS SESUAI GAMBAR UI ==== */
/* Dashboard Menu Cards (Gambar 1) */
.menu-cards-grid {
display: flex;
gap: 20px;
justify-content: center;
margin-bottom: 30px;
}
.menu-card-link {
background: #e0f2f1;
border-radius: 15px;
padding: 20px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 180px;
height: 140px;
text-decoration: none;
color: #333;
font-weight: bold;
font-size: 14px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
transition: transform 0.2s;
}
.menu-card-link:hover {
transform: scale(1.05);
}
.menu-card-link .icon-box {
font-size: 40px;
margin-bottom: 10px;
}
/* Monitoring Parameter (Gambar 2) */
.mon-top-grid {
display: flex;
justify-content: center;
gap: 30px;
margin-bottom: 30px;
}
.mon-circle-card {
background: #9bdcd5;
border-radius: 20px;
padding: 20px;
width: 200px;
text-align: center;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.mon-circle-card .circle-val {
background: white;
border-radius: 50%;
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 15px auto;
font-weight: bold;
font-size: 1.1rem;
color: #333;
}
.mon-circle-card .title {
font-weight: 800;
font-size: 1.1rem;
color: #222;
}
.mon-bot-grid {
display: flex;
flex-direction: column;
grid-template-columns:repeat(auto-fit,minmax(220px,1fr));
align-items: center;
gap: 20px;
}
.mon-pill-card {
background: #9bdcd5;
border-radius: 20px;
padding: 15px 30px;
width: 350px;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: bold;
font-size: 1.2rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
/* Sistem Kontrol (Gambar 3) */
.ctrl-main-grid {
display: flex;
justify-content: center;
gap: 30px;
margin-top: 20px;
}
.ctrl-box {
background: #e0f2f1;
border-radius: 20px;
padding: 25px;
width: 450px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.ctrl-box-title {
background: white;
border-radius: 20px;
padding: 10px;
text-align: center;
font-weight: 800;
margin-bottom: 25px;
}
.ctrl-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
font-weight: bold;
font-size: 14px;
}
.ctrl-item input[type="text"] {
width: 60px;
padding: 5px;
border-radius: 5px;
border: none;
text-align: center;
font-weight: bold;
}
.ctrl-time {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
margin-bottom: 20px;
}
.ctrl-time input {
background: white;
border: none;
padding: 10px 15px;
border-radius: 20px;
font-weight: bold;
width: 80px;
text-align: center;
}
.btn-mulai {
background: #e8d5d5;
border: none;
padding: 8px 30px;
border-radius: 15px;
font-weight: bold;
cursor: pointer;
display: block;
margin: 0 auto;
transition: 0.2s;
}
.btn-mulai:hover {
background: #d4baba;
}
/* Catatan Table (Gambar 4) */
.table-container {
background: #9bdcd5;
border-radius: 15px;
padding: 20px;
margin-top: 20px;
}
.catatan-table {
width: 100%;
border-collapse: collapse;
background: white;
border-radius: 10px;
overflow: hidden;
}
.catatan-table th, .catatan-table td {
padding: 12px;
text-align: center;
border-bottom: 1px solid #eee;
font-size: 14px;
}
.catatan-table th {
background: #fdfdfd;
font-weight: bold;
}
.pagination {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 15px;
padding: 0 10px;
font-size: 14px;
font-weight: bold;
}
.pagination-controls button {
background: #333;
color: white;
border: none;
padding: 5px 10px;
border-radius: 5px;
cursor: pointer;
margin-left: 5px;
}
.dashboard-layout {
display: flex;
}
.dashboard-layout {
display: flex;
}
/* Sidebar collapse */
.sidebar.hide {
width: 80px;
padding: 20px 10px;
}
/* Hilangkan teks menu */
.sidebar.hide .text {
display: none;
}
/* Hilangkan tulisan DASHBOARD */
.sidebar.hide .sidebar-title {
display: none;
}
/* Biar tombol tetap keliatan & ke tengah */
.sidebar.hide .sidebar-header {
justify-content: center;
}
/* ICON tetap center */
.sidebar.hide ul li a {
justify-content: center;
}
/* Background aquarium */
.aquarium {
position: relative;
height: 100vh;
overflow: hidden;
background: linear-gradient(to bottom, #0099cc, #003366);
}
/* Hero */
.hero {
position: relative;
z-index: 2;
text-align: center;
padding-top: 120px;
color: white;
font-size: 2rem;
}
/* IKAN */
.fish {
position: absolute;
font-size: 40px;
animation: swim linear infinite;
}
.fish1 { top: 30%; animation-duration: 12s; }
.fish2 { top: 50%; animation-duration: 18s; }
.fish3 { top: 70%; animation-duration: 20s; }
@keyframes swim {
0% {
left: -100px;
transform: scaleX(-1); /* dibalik jadi hadap kanan */
}
100% {
left: 110%;
transform: scaleX(-1); /* tetap hadap kanan */
}
}
.shark {
position: absolute;
top: 40%;
font-size: 80px;
animation: swimShark 25s linear infinite;
}
@keyframes swimShark {
0% {
right: -150px;
transform: scaleX(1); /* hadap kiri (default emoji) */
}
100% {
right: 110%;
transform: scaleX(1);
}
}
.stingray {
position: absolute;
bottom: 20%;
font-size: 70px;
animation: swimRay 30s linear infinite;
opacity: 0.9;
}
@keyframes swimRay {
0% {
right: -150px;
transform: scaleX(1); /* hadap kiri (default emoji) */
}
100% {
right: 110%;
transform: scaleX(1);
}
}
.jellyfish {
position: absolute;
font-size: 50px;
animation: floatUpDown ease-in-out infinite;
}
.j1 {
left: 20%;
animation-duration: 6s;
}
.j2 {
left: 50%;
animation-duration: 8s;
}
.j3 {
left: 75%;
animation-duration: 7s;
}
@keyframes floatUpDown {
0% {
top: 20%;
}
50% {
top: 60%;
}
100% {
top: 20%;
}
}
/* GELEMBUNG */
.bubble {
position: absolute;
bottom: -50px;
width: 12px;
height: 12px;
background: rgba(255, 255, 255, 0.8); /* lebih terang */
border-radius: 50%;
box-shadow: 0 0 10px rgba(255,255,255,0.8); /* glow biar keliatan */
animation: rise linear infinite;
}
.bubble1 { left: 20%; animation-duration: 5s; }
.bubble2 { left: 40%; animation-duration: 3s; }
.bubble3 { left: 20%; animation-duration: 5s; }
.bubble4 { left: 80%; animation-duration: 4s; }
.bubble5 { left: 20%; animation-duration: 5s; }
.bubble6 { left: 40%; animation-duration: 3s; }
.bubble7 { left: 20%; animation-duration: 5s; }
.bubble8 { left: 80%; animation-duration: 4s; }
.bubble9 { left: 20%; animation-duration: 5s; }
.bubble10 { left: 80%; animation-duration: 4s; }
@keyframes rise {
0% {
transform: translateY(0) scale(1);
opacity: 0.7;
}
100% {
transform: translateY(-100vh) scale(1.5);
opacity: 0;
}
}
/* TERUMBU KARANG */
.coral {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
font-size: 150px; /* dibesarin */
text-align: left;
padding-left: 20px;
}
.coral-right {
position: absolute;
bottom: 0;
right: 10px;
font-size: 150px;
transform: rotate(-5deg); /* sedikit miring biar natural */
}
.hero h1 {
font-family: 'Baloo 2', cursive;
font-size: 60px;
color: #aefeff; /* warna aqua */
}
.bg-login {
min-height: 100vh;
display: flex;
flex-direction: column;
background: linear-gradient(to bottom, #0099cc, #003366);
}
.auth-container {
display: flex;
justify-content: center;
align-items: center;
flex: 1;
}
.auth-card {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(12px);
border-radius: 20px;
padding: 35px;
width: 320px;
color: white;
border: 1px solid rgba(255,255,255,0.2);
box-shadow:
0 10px 30px rgba(0,0,0,0.3);
animation: fadeIn 0.8s ease;
}
.auth-card h2 {
text-align: center;
margin-bottom: 20px;
font-size: 28px;
color: #ffffff;
}
.auth-card input {
width: 100%;
padding: 12px;
margin-bottom: 15px;
border-radius: 12px;
border: none;
outline: none;
background: rgba(255,255,255,0.2);
color: white;
}
.auth-card input::placeholder {
color: #eee;
}
.auth-card button {
width: 100%;
padding: 12px;
border-radius: 12px;
border: none;
background: linear-gradient(45deg, #00c9ff, #92fe9d);
color: black;
font-weight: bold;
cursor: pointer;
transition: 0.3s;
}
.auth-card button:hover {
transform: scale(1.05);
}
.forgot-password {
display: block;
text-align: center;
font-size: 12px;
color: white;
text-decoration: none;
margin-bottom: 15px;
opacity: 0.8;
}
.forgot-password:hover {
opacity: 1;
text-decoration: underline;
}
/* item dasar */
.sidebar ul li a {
padding: 10px 12px;
border-radius: 10px;
transition: all 0.3s ease;
}
/* hover */
.sidebar ul li a:hover {
background: rgba(0, 0, 0, 0.08);
}
/* ACTIVE MENU */
.sidebar ul li.active a {
background: #5FBDBD;
color: white;
font-weight: bold;
border-radius: 10px;
}
/* ICON ikut berubah */
.sidebar ul li.active i {
color: #000;
}
.sidebar ul li {
position: relative;
}
.sidebar ul li.active::before {
content: "";
position: absolute;
left: -20px;
top: 0;
height: 100%;
width: 5px;
background: #00c9ff;
border-radius: 0 5px 5px 0;
}
.sidebar ul li a {
padding: 10px;
transition: 0.3s;
}
.sidebar ul li a:hover {
background: rgba(0,0,0,0.1);
}
.sidebar ul li a {
padding: 10px;
border-radius: 10px;
transition: 0.3s;
}
.sidebar ul li a:hover {
background: #cceeee;
}
/* Header sidebar saat collapse */
.sidebar.hide .sidebar-header {
display: flex;
justify-content: center;
align-items: center;
}
/* Biar tombol sama posisi icon menu */
#toggleSidebar {
display: flex;
justify-content: center;
align-items: center;
width: 50px;
height: 50px;
}
/* Biar icon menu konsisten */
.sidebar ul li i {
width: 40px;
text-align: center;
font-size: 18px;
}
.sidebar {
width: 250px;
transition: width 0.3s ease;
}
/* teks fade out */
.sidebar .text {
transition: opacity 0.2s ease;
}
.sidebar.hide .text {
opacity: 0;
}
/* judul juga fade */
.sidebar-title {
transition: opacity 0.2s ease;
}
.sidebar.hide .sidebar-title {
opacity: 0;
}
.sidebar ul li a {
transition: all 0.2s ease;
}
.sidebar ul li a:hover {
transform: scale(1.05);
}
/* Animasi masuk halaman */
.page-enter {
opacity: 0;
transform: translateY(20px);
animation: fadeSlideIn 0.6s ease forwards;
}
@keyframes fadeSlideIn {
to {
opacity: 1;
transform: translateY(0);
}
}
.menu-card-link {
opacity: 0;
transform: translateY(20px);
animation: fadeSlideIn 0.5s ease forwards;
}
/* delay tiap card */
.menu-card-link:nth-child(1) { animation-delay: 0.1s; }
.menu-card-link:nth-child(2) { animation-delay: 0.2s; }
.menu-card-link:nth-child(3) { animation-delay: 0.3s; }
.topbar-icons {
display: flex;
gap: 20px;
align-items: center;
color: white;
font-size: 18px;
}
.notif, .user {
position: relative;
cursor: pointer;
}
.notif-dropdown, .user-dropdown {
display: none;
position: absolute;
top: 45px;
right: 0;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(15px);
color: white;
width: 240px;
border-radius: 16px;
padding: 15px;
box-shadow:
0 10px 25px rgba(0,0,0,0.3),
inset 0 0 10px rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
animation: dropdown3D 0.3s ease;
z-index: 999;
}
.user-dropdown {
width: 260px; /* diperlebar */
}
/* aktif */
.show {
display: block;
}
/* animasi */
@keyframes dropdown3D {
from {
opacity: 0;
transform: translateY(-10px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* KHUSUS NOTIF */
.notif-dropdown {
width: 280px; /* diperlebar */
}
/* KHUSUS USER */
.user-dropdown {
width: 260px; /* biar email muat */
}
.notif-dropdown ul {
list-style: none; /* INI KUNCINYA */
padding: 0;
margin: 0;
}
.notif-dropdown li {
padding: 8px 12px;
text-align: left;
}
.notif-dropdown li::before {
content: "🔔";
margin-right: 8px;
}

147
assets/js/chart.js Normal file
View File

@ -0,0 +1,147 @@
document.addEventListener('DOMContentLoaded', async function () {
const ctx = document.getElementById('phChart');
if (!ctx) return;
const NODERED_URL = (window.NODERED_URL || 'https://node-red.aquamonn.online').replace(/\/$/, '');
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: 'pH',
data: [],
borderColor: '#2980b9',
backgroundColor: 'rgba(41, 128, 185, 0.15)',
tension: 0.3,
fill: true,
pointRadius: 3,
yAxisID: 'yPh'
},
{
label: 'Suhu (°C)',
data: [],
borderColor: '#e67e22',
backgroundColor: 'rgba(230, 126, 34, 0.10)',
tension: 0.3,
fill: false,
pointRadius: 3,
yAxisID: 'ySuhu'
},
{
label: 'Amonia (ppm)',
data: [],
borderColor: '#27ae60',
backgroundColor: 'rgba(39, 174, 96, 0.10)',
tension: 0.3,
fill: false,
pointRadius: 3,
yAxisID: 'yAmonia'
},
{
label: 'Distance (cm)',
data: [],
borderColor: '#3b27ae',
backgroundColor: 'rgba(59, 39, 174, 0.10)',
tension: 0.3,
fill: false,
pointRadius: 3,
yAxisID: 'ydistance'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { display: true, labels: { color: '#000000', font: { size: 12 } } },
title: {
display: true,
text: 'Grafik Sensor Realtime',
color: '#0b0b0b',
font: { size: 14, weight: 'bold' }
}
},
scales: {
x: {
ticks: { color: '#ccc', font: { size: 10 }, maxTicksLimit: 10 },
grid: { color: 'rgba(255,255,255,0.08)' }
},
yPh: {
type: 'linear',
position: 'left',
title: { display: true, text: 'pH', color: '#2980b9' },
ticks: { color: '#2980b9' },
grid: { color: 'rgba(255,255,255,0.08)' },
min: 0, max: 14
},
ySuhu: {
type: 'linear',
position: 'right',
title: { display: true, text: '°C', color: '#e67e22' },
ticks: { color: '#e67e22' },
grid: { drawOnChartArea: false },
min: 20, max: 40
},
yAmonia: {
type: 'linear',
position: 'right',
title: { display: true, text: 'ppm', color: '#27ae60' },
ticks: { color: '#27ae60' },
grid: { drawOnChartArea: false },
min: 0, max: 10,
offset: true
},
ydistance: {
type: 'linear',
position: 'right',
title: { display: true, text: 'cm', color: '#3b27ae' },
ticks: { color: '#2927ae' },
grid: { drawOnChartArea: false },
min: 0, max: 30,
offset: true
}
}
}
});
async function loadChartData() {
try {
const res = await fetch(NODERED_URL + '/get_history', { cache: 'no-store' });
if (!res.ok) throw new Error('HTTP ' + res.status);
const arr = await res.json();
if (!Array.isArray(arr) || arr.length === 0) {
chart.options.plugins.title.text = 'Grafik Sensor — Menunggu data…';
chart.update();
return;
}
const slice = arr.slice(0, 20).reverse();
chart.data.labels = slice.map(r => {
if (r.jam) return r.jam.substring(0, 5);
const d = new Date(r.timestamp ?? 0);
return d.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' });
});
chart.data.datasets[0].data = slice.map(r => parseFloat(r.ph ?? r.pH ?? 0));
chart.data.datasets[1].data = slice.map(r => parseFloat(r.suhu ?? r.temperature ?? 0));
chart.data.datasets[2].data = slice.map(r => parseFloat(r.amonia ?? 0));
chart.data.datasets[3].data = slice.map(r => parseFloat(r.distance ?? 0));
chart.options.plugins.title.text = 'Grafik Sensor Realtime ';
chart.update();
} catch(e) {
chart.options.plugins.title.text = 'Grafik — Gagal: ' + e.message;
chart.update();
console.warn('Chart error:', e.message);
}
}
await loadChartData();
setInterval(loadChartData, 30000);
});

110
assets/js/node-red.js Normal file
View File

@ -0,0 +1,110 @@
(function () {
"use strict";
const POLL_MS = 3000;
const DATA_URL = (window.NODERED_URL || "").replace(/\/$/, "") + "/get-data";
const SENSORS = {
suhu: { valId:"val-suhu", barId:"bar-suhu", unit:"", decimals:1, max:45, color: v=>(v>32||v<18)?"#f59e0b":"#22c55e" },
ph: { valId:"val-ph", barId:"bar-ph", unit:" pH", decimals:2, max:14, color: v=>(v<6.5||v>8.5)?"#ef4444":"#22c55e" },
amonia: { valId:"val-amonia", barId:"bar-amonia", unit:" ppm", decimals:2, max:10, color: v=>v>2?"#ef4444":v>1?"#f59e0b":"#22c55e" },
kekeruhan: { valId:"val-kekeruhan", barId:"bar-kekeruhan", unit:" NTU", decimals:1, max:150, color: v=>v>80?"#ef4444":v>40?"#f59e0b":"#22c55e" },
distance: { valId:"val-distance", barId:"bar-distance", unit:" cm", decimals:1, max:100, color: v=>v>70?"#ef4444":v>50?"#f59e0b":"#22c55e" },
};
function updateSensor(key, raw) {
const cfg = SENSORS[key];
if (!cfg || raw===null || raw===undefined) return;
const v = parseFloat(raw);
if (isNaN(v)) return;
const valEl = document.getElementById(cfg.valId);
const barEl = document.getElementById(cfg.barId);
if (valEl) valEl.textContent = v.toFixed(cfg.decimals) + cfg.unit;
if (barEl) {
barEl.style.width = Math.min(100, Math.max(0, v/cfg.max*100)) + "%";
barEl.style.background = cfg.color(v);
}
}
function updateDevice(badgeId, valId, raw) {
const valEl = document.getElementById(valId);
const badgeEl = document.getElementById(badgeId);
if (!valEl || !badgeEl || raw===null || raw===undefined) return;
const on = (raw==1 || String(raw).toUpperCase()==="ON");
valEl.textContent = on ? "ON" : "OFF";
badgeEl.classList.toggle("on", on);
badgeEl.classList.toggle("off", !on);
}
const RULES = [
{key:"amonia", label:"Amonia", dangerHi:2, warnHi:1 },
{key:"suhu", label:"Suhu", dangerHi:33, warnHi:30, dangerLo:17, warnLo:20 },
{key:"ph", label:"pH", dangerLo:6.0, warnLo:6.5,dangerHi:9.0,warnHi:8.5},
{key:"kekeruhan", label:"Kekeruhan", dangerHi:80, warnHi:40 },
{key:"distance", label:"Distance", dangerHi:70, warnHi:50 }
];
function checkThresholds(data) {
const notifs = [];
RULES.forEach(r => {
const v = parseFloat(data[r.key]);
if (isNaN(v)) return;
if ((r.dangerHi!==undefined&&v>=r.dangerHi)||(r.dangerLo!==undefined&&v<=r.dangerLo))
notifs.push({level:"danger", msg:`⚠️ ${r.label} KRITIS: ${v.toFixed(2)}`});
else if ((r.warnHi!==undefined&&v>=r.warnHi)||(r.warnLo!==undefined&&v<=r.warnLo))
notifs.push({level:"warn", msg:`🔔 ${r.label} Abnormal: ${v.toFixed(2)}`});
});
const badge = document.querySelector(".notif-badge");
const listEl = document.querySelector("#notifDropdown ul");
if (!listEl) return;
if (!notifs.length) {
if (badge) badge.textContent = "0";
listEl.innerHTML = "<li style='color:rgba(255,255,255,.5)'>Semua normal ✔</li>";
} else {
if (badge) badge.textContent = notifs.length;
listEl.innerHTML = notifs.map(n=>
`<li style="color:${n.level==='danger'?'#ef4444':'#f59e0b'}">${n.msg}</li>`
).join("");
}
}
async function fetchData() {
try {
const res = await fetch(DATA_URL, {cache:"no-store"});
if (!res.ok) throw new Error("HTTP "+res.status);
const data = await res.json();
// normalize: turbidity → kekeruhan, pH → ph
const d = {
suhu: data.suhu ?? data.temperature ?? null,
ph: data.ph ?? data.pH ?? null,
amonia: data.amonia ?? null,
kekeruhan: data.kekeruhan ?? data.turbidity ?? null,
relay: data.relay ?? null,
lampu: data.lampu ?? null,
distance: data.distance ?? null
};
Object.keys(SENSORS).forEach(k => updateSensor(k, d[k]));
updateDevice("badge-relay", "val-relay", d.relay);
updateDevice("badge-lampu", "val-lampu", d.lampu);
checkThresholds(d);
if (typeof window.onSensorUpdate==="function") window.onSensorUpdate(true);
} catch(err) {
console.warn("[node-red.js]", err.message);
if (typeof window.onSensorUpdate==="function") window.onSensorUpdate(false);
}
}
if (!window.NODERED_URL) {
console.error("[node-red.js] window.NODERED_URL belum diset di monitoring.php!");
return;
}
fetchData();
setInterval(fetchData, POLL_MS);
})();

346
catatan.php Normal file
View File

@ -0,0 +1,346 @@
<?php
require_once 'functions/auth.php';
if(!isLoggedIn()) { header("Location: login.php"); exit; }
require_once 'partials/header.php';
if (!defined('NODERED_URL')) {
define('NODERED_URL', 'https://node-red.aquamonn.online');
}
$noderedUrl = rtrim(NODERED_URL, '/');
?>
<div class="dashboard-layout">
<?php require_once 'partials/sidebar.php'; ?>
<div class="main-content page-enter">
<div style="display:flex; justify-content:flex-end; margin-bottom:10px;">
<div class="topbar-icons">
<div class="notif" id="notifBtn">
<i class="fas fa-bell"></i>
<span class="notif-badge">0</span>
<div class="notif-dropdown" id="notifDropdown">
<p><b>Notifikasi</b></p>
<ul><li>Memuat data…</li></ul>
</div>
</div>
<div class="user" id="userBtn">
<i class="fas fa-user"></i>
<div class="user-dropdown" id="userDropdown">
<div class="user-info">
<p class="user-email"><?= htmlspecialchars($_SESSION['email'] ?? 'User') ?></p>
</div>
<a href="logout.php">Logout</a>
</div>
</div>
</div>
</div>
<!-- Filter tanggal -->
<div style="margin-bottom:14px; display:flex; gap:10px; align-items:center; flex-wrap:wrap;">
<input type="date" id="filter-date"
style="padding:6px 12px; border-radius:8px; border:1px solid rgba(255,255,255,.2);
background:rgba(255,255,255,.1); color:#fff; font-size:13px; cursor:pointer;">
<button onclick="applyFilter()"
style="padding:6px 16px; border-radius:8px; background:#22c55e; color:#fff;
border:none; font-size:13px; cursor:pointer; font-weight:600;">
Filter
</button>
<button onclick="clearFilter()"
style="padding:6px 16px; border-radius:8px; background:rgba(255,255,255,.15); color:#fff;
border:none; font-size:13px; cursor:pointer;">
Reset
</button>
<span id="status-label" style="font-size:11px; color:rgba(255,255,255,.4); margin-left:4px;"></span>
</div>
<div class="table-container">
<div style="background:white; display:inline-block; padding:5px 15px;
border-radius:10px 10px 0 0; font-weight:bold; font-size:12px;">Laporan</div>
<table class="catatan-table">
<thead>
<tr>
<th>Tanggal</th>
<th>Waktu</th>
<th>Amonia</th>
<th>Kekeruhan</th>
<th>Suhu</th>
<th>Tinggi Air</th>
<th>Jarak sensor</th>
<th>pH</th>
</tr>
</thead>
<tbody id="tbody-catatan">
<tr>
<td colspan="6" style="text-align:center;padding:20px;color:#888;"> Memuat…</td>
</tr>
</tbody>
</table>
<div class="pagination">
<span id="pagination-info"></span>
<div class="pagination-controls">
<button id="btn-prev" onclick="changePage(-1)">Previous</button>
<span id="page-indicator" style="padding:0 10px;font-weight:bold;">1</span>
<button id="btn-next" onclick="changePage(1)">Next</button>
</div>
</div>
</div>
</div>
</div>
<?php require_once 'partials/footer.php'; ?>
<script>
const NODE_RED_URL = "<?= $noderedUrl ?>";
let currentPage = 1;
const rowsPerPage = 10;
let historyData = [];
let pollingTimer = null;
// ===============================
// Stop Polling
// ===============================
function stopPolling() {
console.log("Menghentikan polling Node-RED");
if (pollingTimer) {
clearInterval(pollingTimer);
pollingTimer = null;
}
}
// ===============================
// Start Polling
// ===============================
function startPolling() {
stopPolling();
loadHistory();
pollingTimer = setInterval(() => {
loadHistory();
}, 5000); // refresh setiap 5 detik
}
// ===============================
// Ambil History dari Node-RED
// ===============================
async function fetchHistory(limit = 100, date = null) {
try {
const params = new URLSearchParams();
params.append("limit", limit);
if (date) {
params.append("date", date);
}
const response = await fetch(
`${NODE_RED_URL}/get_history?${params.toString()}`,
{
method: "GET",
headers: {
"Accept":"application/json"
}
}
);
if (!response.ok) {
throw new Error("HTTP " + response.status);
}
const decoded = await response.json();
let list = [];
if (Array.isArray(decoded)) {
list = decoded;
}
else if (decoded.items) {
list = decoded.items;
}
else if (decoded.data) {
list = decoded.data;
}
else if (decoded.history) {
list = decoded.history;
}
return list;
} catch(err) {
console.error("Fetch History Error :", err);
return [];
}
}
// ===============================
// Load Data
// ===============================
async function loadHistory() {
document.getElementById("status-label").innerHTML = "Sinkronisasi...";
const date = document.getElementById("filter-date").value;
historyData = await fetchHistory(
100,
date !== "" ? date : null
);
currentPage = 1;
renderTable();
document.getElementById("status-label").innerHTML =
"Update : " + new Date().toLocaleTimeString();
}
// ===============================
// Render Table
// ===============================
function renderTable() {
const tbody = document.getElementById("tbody-catatan");
tbody.innerHTML = "";
if(historyData.length===0){
tbody.innerHTML=`
<tr>
<td colspan="6" style="text-align:center;padding:20px;color:#999">
Tidak ada data
</td>
</tr>`;
document.getElementById("pagination-info").innerHTML="0 Data";
return;
}
const start=(currentPage-1)*rowsPerPage;
const end=start+rowsPerPage;
const pageData=historyData.slice(start,end);
pageData.forEach(item=>{
let tanggal = "-";
let waktu = "-";
if (item.time) {
const arr = item.time.split("T");
tanggal = arr[0];
waktu = arr[1].replace(".000Z", "");
}
// Data sensor
const ammonia = Number(item.ammonia ?? item.amonia ?? 0).toFixed(1);
const turbidity = Number(item.turbidity ?? item.kekeruhan ?? 0).toFixed(1);
// suhu & ph (sesuaikan jika nanti sudah dikirim dari Node-RED)
const suhu = Number(item.suhu ?? 0).toFixed(1);
const tinggi_air = Number(item.tinggiAir ?? item.tinggi_air ?? 0).toFixed(1);
const jarak_sensor = Number(item.distance ?? item.jarak_Sensor ?? 0).toFixed(1);
const ph = Number(item.ph ?? 0).toFixed(1);
tbody.innerHTML+=`
<tr>
<td>${tanggal}</td>
<td>${waktu}</td>
<td>${ammonia}</td>
<td>${turbidity}</td>
<td>${suhu}</td>
<td>${tinggi_air}</td>
<td>${jarak_sensor}</td>
<td>${ph}</td>
</tr>`;
});
document.getElementById("page-indicator").innerHTML=currentPage;
document.getElementById("pagination-info").innerHTML=
`${start+1}-${Math.min(end,historyData.length)} dari ${historyData.length}`;
document.getElementById("btn-prev").disabled=currentPage===1;
document.getElementById("btn-next").disabled=end>=historyData.length;
}
// ===============================
// Pagination
// ===============================
function changePage(dir){
const maxPage=Math.ceil(historyData.length/rowsPerPage);
currentPage+=dir;
if(currentPage<1)
currentPage=1;
if(currentPage>maxPage)
currentPage=maxPage;
renderTable();
}
// ===============================
// Filter
// ===============================
function applyFilter(){
loadHistory();
}
function clearFilter(){
document.getElementById("filter-date").value="";
loadHistory();
}
// ===============================
// Dropdown User
// ===============================
document.getElementById("userBtn").onclick=function(){
document.getElementById("userDropdown")
.classList.toggle("show");
};
document.getElementById("notifBtn").onclick=function(){
document.getElementById("notifDropdown")
.classList.toggle("show");
};
// ===============================
// Start
// ===============================
window.onload=()=>{
startPolling();
};
window.onbeforeunload=()=>{
stopPolling();
};
</script>

10
config/firebase.php Normal file
View File

@ -0,0 +1,10 @@
<?php
// Mencegah error session already started
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Konfigurasi Firebase (Ubah sesuai dengan detail project Firebase kamu)
define("FIREBASE_API_KEY", "AIzaSyAGt3x8fgM27kL8Q4naNEDBE1ij5CbbFr0");
define("FIREBASE_DB_URL", "https://aquascape-67707-default-rtdb.firebaseio.com/");
?>

138
dashboard.php Normal file
View File

@ -0,0 +1,138 @@
<?php
require_once 'functions/auth.php';
$currentPage = basename($_SERVER['PHP_SELF']);
if(!isLoggedIn()) {
header("Location: login.php");
exit;
}
if (!defined('NODERED_URL')) {
define('NODERED_URL', 'https://node-red.aquamonn.online');
}
require_once 'partials/header.php';
?>
<div class="dashboard-layout">
<?php require_once 'partials/sidebar.php'; ?>
<div class="main-content page-enter">
<!-- Header -->
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 30px;">
<h2 style="color: white; font-weight:800; text-transform: uppercase;">DASHBOARD</h2>
<div class="topbar-icons">
<!-- NOTIFIKASI -->
<div class="notif" id="notifBtn">
<i class="fas fa-bell"></i>
<span class="notif-badge" id="notifCount">0</span>
<div class="notif-dropdown" id="notifDropdown">
<p><b>Notifikasi</b></p>
<ul id="notifList"><li>Memuat…</li></ul>
</div>
</div>
<!-- USER -->
<div class="user" id="userBtn">
<i class="fas fa-user"></i>
<div class="user-dropdown" id="userDropdown">
<div class="user-info">
<p class="user-email"><?= htmlspecialchars($_SESSION['email'] ?? 'User') ?></p>
</div>
<a href="logout.php">Logout</a>
</div>
</div>
</div>
</div>
<!-- Menu Cards -->
<div class="menu-cards-grid">
<a href="monitoring.php" class="menu-card-link">
<div class="icon-box">🌡️</div>
<span>Monitoring<br>Parameter</span>
</a>
<a href="catatan.php" class="menu-card-link">
<div class="icon-box">📝</div>
<span>Catatan</span>
</a>
<a href="kontrol.php" class="menu-card-link">
<div class="icon-box">⚙️</div>
<span>Sistem Kontrol</span>
</a>
</div>
<!-- Grafik -->
<div class="chart-container" style="max-width:600px; margin-top:30px; position:relative; height:280px;">
<canvas id="phChart"></canvas>
</div>
</div>
</div>
<!-- Kirim NODERED_URL ke JS -->
<script>
window.NODERED_URL = "<?= rtrim(NODERED_URL, '/') ?>";
</script>
<script src="assets/js/chart.js"></script>
<script>
// Sidebar toggle
const toggleBtn = document.getElementById("toggleSidebar");
if (toggleBtn) {
toggleBtn.onclick = function() {
document.querySelector(".sidebar").classList.toggle("hide");
};
}
// Dropdown notif & user
const notifBtn = document.getElementById("notifBtn");
const notifDropdown = document.getElementById("notifDropdown");
const userBtn = document.getElementById("userBtn");
const userDropdown = document.getElementById("userDropdown");
notifBtn.onclick = e => { e.stopPropagation(); notifDropdown.classList.toggle("show"); userDropdown.classList.remove("show"); };
userBtn.onclick = e => { e.stopPropagation(); userDropdown.classList.toggle("show"); notifDropdown.classList.remove("show"); };
document.onclick = () => { notifDropdown.classList.remove("show"); userDropdown.classList.remove("show"); };
// ── Notifikasi otomatis dari data sensor ─────────────────
async function cekNotifikasi() {
try {
const res = await fetch(window.NODERED_URL + '/get_history', { cache: 'no-store' });
if (!res.ok) return;
const arr = await res.json();
if (!Array.isArray(arr) || arr.length === 0) return;
const latest = arr[0]; // data terbaru
const notifs = [];
const suhu = parseFloat(latest.suhu ?? 0);
const ph = parseFloat(latest.ph ?? latest.pH ?? 0);
const amonia = parseFloat(latest.amonia ?? 0);
const distance = parseFloat(latest.distance ?? 0);
if (suhu > 30) notifs.push(`🌡️ Suhu tinggi: ${suhu.toFixed(1)}°C`);
if (ph < 6.5) notifs.push(`🔬 pH terlalu rendah: ${ph.toFixed(2)}`);
if (ph > 8.0) notifs.push(`🔬 pH terlalu tinggi: ${ph.toFixed(2)}`);
if (amonia > 2) notifs.push(`⚠️ Amonia tinggi: ${amonia.toFixed(2)} ppm`);
if (distance > 70) notifs.push(`📏 Jarak air tinggi: ${distance.toFixed(1)} cm`);
if (latest.relay == 1) notifs.push(`💧 Pompa sedang aktif`);
const badge = document.getElementById("notifCount");
const list = document.getElementById("notifList");
badge.textContent = notifs.length;
list.innerHTML = notifs.length
? notifs.map(n => `<li>${n}</li>`).join('')
: '<li>Semua parameter normal </li>';
} catch(e) {}
}
cekNotifikasi();
setInterval(cekNotifikasi, 30000);
</script>
<?php require_once 'partials/footer.php'; ?>

50
forgot.php Normal file
View File

@ -0,0 +1,50 @@
<?php require_once 'partials/header.php'; ?>
<div class="bg-login">
<div class="auth-container">
<div class="card auth-card">
<h2>Lupa Password</h2>
<form id="resetForm">
<input type="email" id="email" placeholder="Masukkan email kamu" required>
<button type="submit">Reset Password</button>
</form>
<a href="login.php" class="forgot-password" style="text-align:center; margin-top:15px;">
Kembali ke Login
</a>
</div>
</div>
</div>
<?php require_once 'partials/footer.php'; ?>
<!-- Firebase (VERSI BENAR - modular) -->
<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/9.6.1/firebase-app.js";
import { getAuth, sendPasswordResetEmail } from "https://www.gstatic.com/firebasejs/9.6.1/firebase-auth.js";
const firebaseConfig = {
apiKey: "AIzaSyAGt3x8fgM27kL8Q4naNEDBE1ij5CbbFr0",
authDomain: "aquascape-67707.firebaseapp.com",
};
// Init Firebase
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
// Event Reset Password
document.getElementById("resetForm").addEventListener("submit", function(e) {
e.preventDefault();
const email = document.getElementById("email").value;
sendPasswordResetEmail(auth, email)
.then(() => {
alert("Link reset password sudah dikirim ke email!");
})
.catch((error) => {
alert("Error: " + error.message);
});
});
</script>

93
functions/auth.php Normal file
View File

@ -0,0 +1,93 @@
<?php
session_start();
/**
* PERBAIKAN PATH:
* Menggunakan __DIR__ agar path selalu absolut dan akurat
* dari folder 'functions' ke 'config'.
*/
$configPath = __DIR__ . '/../config/firebase.php';
if (file_exists($configPath)) {
require_once $configPath;
} else {
// Jika masih error, tampilkan pesan bantuan
echo "<div style='background:#ffeded; border:1px solid red; padding:10px; border-radius:5px; font-family:sans-serif;'>";
echo "<b>Error:</b> File firebase.php tidak ditemukan!<br>";
echo "Dicari di: <code style='color:blue'>$configPath</code>";
echo "</div>";
die();
}
function isLoggedIn() {
return isset($_SESSION['user_token']);
}
function registerUser($username, $email, $password) {
// PROTEKSI: Cek apakah cURL dimuat oleh web server
if (!function_exists('curl_init')) {
return [
'success' => false,
'error' => 'Modul cURL tidak aktif di browser. Buka XAMPP Control Panel, klik Stop pada Apache, lalu Start kembali. Pastikan php.ini yang diedit benar.'
];
}
$url = "https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=" . FIREBASE_API_KEY;
$data = json_encode(['email' => $email, 'password' => $password, 'returnSecureToken' => true]);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);
if ($err) return ['success' => false, 'error' => 'Gagal koneksi: ' . $err];
$result = json_decode($response, true);
if (isset($result['idToken'])) {
return ['success' => true];
} else {
$message = $result['error']['message'] ?? 'Gagal mendaftar';
if ($message == 'EMAIL_EXISTS') $message = 'Email sudah terdaftar.';
if ($message == 'WEAK_PASSWORD') $message = 'Password terlalu lemah.';
return ['success' => false, 'error' => $message];
}
}
function loginUser($email, $password) {
// PROTEKSI: Cek apakah cURL dimuat oleh web server
if (!function_exists('curl_init')) {
return [
'success' => false,
'error' => 'Fitur cURL tidak terbaca oleh Apache. Silakan RESTART Apache di XAMPP Control Panel.'
];
}
$url = "https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=" . FIREBASE_API_KEY;
$data = json_encode(['email' => $email, 'password' => $password, 'returnSecureToken' => true]);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$result = json_decode($response, true);
curl_close($ch);
if (isset($result['idToken'])) {
$_SESSION['user_token'] = $result['idToken'];
$_SESSION['email'] = $result['email'];
return ['success' => true];
} else {
$message = $result['error']['message'] ?? 'Login Gagal';
if ($message == 'INVALID_LOGIN_CREDENTIALS') $message = 'Email atau password salah.';
return ['success' => false, 'error' => $message];
}
}

54
index.php Normal file
View File

@ -0,0 +1,54 @@
<?php
require_once 'config/firebase.php';
require_once 'partials/header.php';
?>
<div class="aquarium">
<?php require_once 'partials/navbar.php'; ?>
<div class="aquarium">
<?php require_once 'partials/navbar.php'; ?>
<div class="hero">
<h1>Welcome to My Aquascape</h1>
</div>
<!-- Ikan -->
<div class="fish fish1">🐟</div>
<div class="fish fish2">🐠</div>
<div class="fish fish3">🐡</div>
<div class="shark">🦈</div>
<div class="stingray">🐋</div>
<div class="jellyfish j1">🪼</div>
<div class="jellyfish j2">🪼</div>
<div class="jellyfish j3">🪼</div>
<!-- Gelembung -->
<div class="bubble bubble1"></div>
<div class="bubble bubble2"></div>
<div class="bubble bubble3"></div>
<div class="bubble bubble4"></div>
<div class="bubble bubble5"></div>
<div class="bubble bubble6"></div>
<div class="bubble bubble7"></div>
<div class="bubble bubble8"></div>
<div class="bubble bubble9"></div>
<div class="bubble bubble10"></div>
<!-- Terumbu Karang -->
<div class="coral">🪸🪸🪸</div>
<div class="coral-right">🌿🪸🐚</div>
</div>
<?php require_once 'partials/footer.php'; ?>
<?php require_once 'partials/footer.php'; ?>
<?php require_once 'partials/footer.php'; ?>

583
kontrol.php Normal file
View File

@ -0,0 +1,583 @@
<?php
require_once 'functions/auth.php';
if(!isLoggedIn()) {
header("Location: login.php");
exit;
}
if (!defined('NODERED_URL')) {
define('NODERED_URL', 'https://node-red.aquamonn.online');
}
require_once 'partials/header.php';
$noderedUrl = rtrim(NODERED_URL, '/');
?>
<div class="dashboard-layout">
<?php require_once 'partials/sidebar.php'; ?>
<div class="main-content page-enter">
<style>
.sensor-bar-wrap {
width: 100%;
height: 5px;
background: rgba(255,255,255,.15);
border-radius: 99px;
margin-top: 8px;
overflow: hidden;
}
.sensor-bar-fill {
height: 100%;
border-radius: 99px;
background: #22c55e;
width: 0%;
transition: width .6s ease, background .3s;
}
#conn-status {
display: inline-block;
width: 9px; height: 9px;
border-radius: 50%;
background: #6b7280;
margin-right: 6px;
transition: background .3s;
}
#conn-status.online { background: #22c55e; box-shadow: 0 0 6px #22c55e; }
#conn-status.offline { background: #ef4444; box-shadow: 0 0 6px #ef4444; }
#last-update { font-size: 11px; color: rgba(255,255,255,.55); margin-left: 6px; }
.device-badge {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px;
border-radius: 99px;
font-size: 11px;
font-weight: 700;
background: rgba(255,255,255,.1);
color: rgba(255,255,255,.5);
transition: background .3s, color .3s;
}
.device-badge.on { background: rgba(34,197,94,.2); color: #22c55e; }
.device-badge.off { background: rgba(239,68,68,.15); color: #ef4444; }
</style>
<div class="dashboard-layout">
<?php require_once 'partials/sidebar.php'; ?>
<div class="main-content page-enter">
<!-- TOPBAR -->
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px; flex-wrap:wrap; gap:10px;">
<div style="display:flex; align-items:center; gap:10px; flex-wrap:wrap;">
<div style="display:flex; align-items:center; color:rgba(25, 22, 22, 0.7); font-size:13px;">
<span id="conn-status"></span>
<span id="conn-label">Menghubungkan…</span>
<span id="last-update"></span>
</div>
<span class="device-badge" id="badge-relay">
</span>
<span class="device-badge" id="badge-lampu">
</span>
</div>
<div class="topbar-icons">
<div class="notif" id="notifBtn">
<i class="fas fa-bell"></i>
<span class="notif-badge">0</span>
<div class="notif-dropdown" id="notifDropdown">
<p><b>Notifikasi</b></p>
<ul><li>Memuat data…</li></ul>
</div>
</div>
<div class="user" id="userBtn">
<i class="fas fa-user"></i>
<div class="user-dropdown" id="userDropdown">
<p><?= htmlspecialchars($_SESSION['email'] ?? 'User') ?></p>
<a href="logout.php">Logout</a>
</div>
</div>
</div>
</div>
<!-- GRID -->
<div class="ctrl-main-grid">
<!-- BOX SISTEM OTOMATIS -->
<div class="ctrl-box">
<div class="ctrl-box-title">Sistem Otomatis</div>
<!-- AMONIA -->
<div class="ctrl-item">
<span>Batas Amonia</span>
<input type="number" id="input-amonia" step="0.1">
<button class="btn-mulai" onclick="setAmonia()">Simpan</button>
</div>
<!-- KEKERUHAN -->
<div class="ctrl-item">
<span>Batas Kekeruhan</span>
<input type="number" id="input-kekeruhan" step="0.1" >
<button class="btn-mulai" onclick="setBatasKekeruhan()">Simpan</button>
</div>
<!-- TINGGI AIR -->
<div class="ctrl-item">
<span>Batas Tinggi Air</span>
<input type="number" id="input-tinggi-air" step="1">
<button class="btn-mulai" onclick="setBatasTinggiAir()">Simpan</button>
</div>
<!-- KONTROL TINGGI AQUARIUM -->
<div class="ctrl-item">
<span>Tinggi Aquarium</span>
<input type="number" id="input-tinggi-aquarium" step="1">
<button class="btn-mulai" onclick="setTinggiAquarium()">Simpan</button>
</div>
<!-- MODE POMPA -->
<div class="ctrl-item">
</div>
</div>
<!-- BOX LAMPU -->
<div class="ctrl-box">
<div class="ctrl-box-title">Set Lampu</div>
<div class="ctrl-time">
<input type="text" id="jam-mulai">
<span style="font-weight:bold;"></span>
<input type="text" id="jam-selesai">
</div>
<div style="display:flex; gap:10px; margin-top:10px; flex-wrap:wrap;">
<button class="btn-mulai" style="background:#e222;" onclick="setJadwalLampu()">Simpan Jadwal</button>
</div>
<!-- MODE LAMPU -->
<div class="ctrl-item" style="margin-top:12px;">
<span>Lampu</span>
<label class="switch">
<input type="checkbox" id="toggle-lampu" onchange="kontrolLampu(this.checked)">
<span class="slider"></span>
</label>
<span id="label-lampu" style="font-size:12px; color:#aaa;">OFF</span>
</div>
</div>
</div>
</div>
</div>
<script>
const NR = "<?= $noderedUrl ?>";
/* ===================== JADWAL LAMPU ===================== */
async function setJadwalLampu() {
const onTime = document.getElementById("jam-mulai").value.trim();
const offTime = document.getElementById("jam-selesai").value.trim();
if (!onTime || !offTime) {
showToast("Isi jam nyala dan jam mati terlebih dahulu", "error");
return;
}
const re = /^([01]\d|2[0-3]):([0-5]\d)$/;
if (!re.test(onTime) || !re.test(offTime)) {
showToast("Format jam harus HH:MM", "error");
return;
}
// Menggunakan toggle yang ada di HTML
const isAuto = document.getElementById("toggle-lampu")?.checked ?? false;
try {
const response = await fetch(NR + "/api-schedule", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
on_time: onTime,
off_time: offTime,
enabled: isAuto
})
});
if (!response.ok) {
throw new Error("HTTP " + response.status);
}
await response.text();
showToast("Jadwal lampu berhasil disimpan");
} catch (err) {
console.error(err);
showToast("Gagal menyimpan jadwal lampu", "error");
}
}
/* ===================== KONTROL LAMPU ===================== */
async function kontrolLampu(isOn) {
try {
const response = await fetch(NR + "/api-lampu", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
lampu: isOn ? "on" : "off"
})
});
if (!response.ok) {
throw new Error("HTTP " + response.status);
}
await response.text();
document.getElementById("label-lampu").textContent = isOn ? "ON" : "OFF";
showToast("Lampu " + (isOn ? "ON" : "OFF"));
} catch (err) {
console.error(err);
showToast("Gagal mengontrol lampu", "error");
}
}
/* ===================== BATAS AMONIA ===================== */
async function setAmonia() {
const nilai = parseFloat(document.getElementById("input-amonia").value);
if (isNaN(nilai)) {
showToast("Masukkan nilai amonia", "error");
return;
}
try {
const response = await fetch(NR + "/set-pmaks", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
maks: nilai
})
});
if (!response.ok) {
throw new Error("HTTP " + response.status);
}
showToast("Batas amonia berhasil disimpan", "success");
} catch (error) {
console.error(error);
showToast("Gagal menyimpan batas amonia", "error");
}
}
/* ===================== BATAS KEKERUHAN ===================== */
async function setBatasKekeruhan() {
const nilai = parseFloat(document.getElementById("input-kekeruhan").value);
if (isNaN(nilai)) {
showToast("Masukkan nilai kekeruhan", "error");
return;
}
try {
const response = await fetch(NR + "/set-batas_kekeruhan", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
max_kekeruhan: nilai
})
});
if (!response.ok) {
throw new Error("HTTP " + response.status);
}
await response.text();
showToast("Batas kekeruhan berhasil disimpan");
} catch (err) {
console.error(err);
showToast("Gagal menyimpan batas kekeruhan", "error");
}
}
/* ===================== BATAS TINGGI AIR ===================== */
async function setBatasTinggiAir() {
const nilai = parseFloat(document.getElementById("input-tinggi-air").value);
if (isNaN(nilai)) {
showToast("Masukkan tinggi air", "error");
return;
}
try {
const response = await fetch(NR + "/set-batas_distance", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
min_jarak: nilai
})
});
if (!response.ok) {
throw new Error("HTTP " + response.status);
}
await response.text();
showToast("Batas tinggi air berhasil disimpan");
} catch (err) {
console.error(err);
showToast("Gagal menyimpan batas tinggi air", "error");
}
}
/* ===================== TINGGI AQUARIUM ===================== */
async function setTinggiAquarium() {
const nilai = parseFloat(document.getElementById("input-tinggi-aquarium").value);
if (isNaN(nilai)) {
showToast("Masukkan tinggi aquarium", "error");
return;
}
try {
const response = await fetch(NR + "/set-tinggi_aquarium", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
tinggi_aquarium: nilai
})
});
if (!response.ok) {
throw new Error("HTTP " + response.status);
}
await response.text();
showToast("Tinggi aquarium berhasil disimpan");
} catch (err) {
console.error(err);
showToast("Gagal menyimpan tinggi aquarium", "error");
}
}
/* ── Load status terkini dari Node-RED /get-status ────── */
async function loadStatus() {
try {
const res = await fetch(NR + '/get-status', { cache: 'no-store' });
if (!res.ok) return;
const d = await res.json();
if (!d) return;
// Pompa
if (d.relay !== undefined) {
const on = d.relay == 1;
const statusPompa = document.getElementById('status-pompa');
if (statusPompa) {
statusPompa.textContent = on ? 'ON' : 'OFF';
statusPompa.style.color = on ? '#22c55e' : '#ef4444';
}
}
if (d.pompa_mode !== undefined) {
const isAuto = d.pompa_mode === 'AUTO';
const togglePompa = document.getElementById('toggle-pompa-auto');
if (togglePompa) togglePompa.checked = isAuto;
const labelPompa = document.getElementById('label-pompa-mode');
if (labelPompa) labelPompa.textContent = isAuto ? 'Auto' : 'Manual';
}
if (d.batas_amonia !== undefined) {
document.getElementById('input-amonia').value = d.batas_amonia;
}
if (d.batas_turbidity !== undefined) {
document.getElementById('input-kekeruhan').value = d.batas_turbidity;
}
if (d.batas_distance !== undefined) {
document.getElementById('input-tinggi-air').value = d.batas_distance;
}
if (d.tinggi_aquarium !== undefined) {
document.getElementById('input-tinggi-aquarium').value = d.tinggi_aquarium;
}
// Lampu
if (d.lampu !== undefined) {
const on = d.lampu == 1;
const labelLampu = document.getElementById('label-lampu');
if (labelLampu) {
labelLampu.textContent = on ? 'ON' : 'OFF';
}
const toggleLampu = document.getElementById('toggle-lampu');
if (toggleLampu) {
toggleLampu.checked = on;
}
}
const jamMulai = document.getElementById("jam-mulai");
if (jamMulai && !jamMulai.value && d.lampu_on_time) {
jamMulai.value = d.lampu_on_time.substring(0,5);
}
const jamSelesai = document.getElementById("jam-selesai");
if (jamSelesai && !jamSelesai.value && d.lampu_off_time) {
jamSelesai.value = d.lampu_off_time.substring(0,5);
}
} catch(e) {
console.warn('[kontrol] Gagal load status:', e.message);
}
}
/* ── Toast ────────────────────────────────────────────── */
function showToast(message, type = "success") {
let toast = document.getElementById("nr-toast");
if (!toast) {
toast = document.createElement("div");
toast.id = "nr-toast";
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
min-width: 320px;
padding: 15px 20px;
border-radius: 8px;
color: #fff;
font-size: 14px;
font-weight: 600;
z-index: 99999;
opacity: 0;
transform: translateX(100%);
transition: all .35s ease;
box-shadow: 0 8px 20px rgba(0,0,0,.25);
`;
document.body.appendChild(toast);
}
if (type === "success") {
toast.style.background = "#22c55e";
toast.innerHTML = "" + message;
} else {
toast.style.background = "#ef4444";
toast.innerHTML = "" + message;
}
toast.style.opacity = "1";
toast.style.transform = "translateX(0)";
clearTimeout(toast.timer);
toast.timer = setTimeout(() => {
toast.style.opacity = "0";
toast.style.transform = "translateX(100%)";
}, 3000);
}
/* ── Dropdown ─────────────────────────────────────────── */
const notifBtn = document.getElementById('notifBtn');
const notifDropdown = document.getElementById('notifDropdown');
const userBtn = document.getElementById('userBtn');
const userDropdown = document.getElementById('userDropdown');
notifBtn.onclick = e => { e.stopPropagation(); notifDropdown.classList.toggle('show'); userDropdown.classList.remove('show'); };
userBtn.onclick = e => { e.stopPropagation(); userDropdown.classList.toggle('show'); notifDropdown.classList.remove('show'); };
document.onclick = () => { notifDropdown.classList.remove('show'); userDropdown.classList.remove('show'); };
document.addEventListener('DOMContentLoaded', () => {
loadStatus();
setInterval(loadStatus, 10000); // refresh status tiap 10 detik
});
</script>
<script>
/**
* NODERED_URL = URL server Node-RED (misal http://192.168.1.x:1880)
* node-red.js akan panggil NODERED_URL + "/get-sensor"
*
* FIREBASE_DB_URL dipakai sebagai fallback jika NODERED_URL tidak diset.
*
* Ganti IP sesuai server Node-RED kamu.
*/
window.NODERED_URL = "https://node-red.aquamonn.online";
window.FIREBASE_DB_URL = "<?= rtrim(FIREBASE_DB_URL, '/') ?>";
// Jika NODERED_URL tidak dikonfigurasi di PHP, kosongkan agar
// node-red.js fallback ke Firebase langsung.
if (!window.NODERED_URL) {
// Firebase path sesuai flow:
// https://aquascape-67707-default-rtdb.firebaseio.com/sensor.json
window.FIREBASE_DB_URL = window.FIREBASE_DB_URL || "https://aquascape-67707-default-rtdb.firebaseio.com";
}
window.onSensorUpdate = function(ok) {
const dot = document.getElementById("conn-status");
const label = document.getElementById("conn-label");
const ts = document.getElementById("last-update");
if (!dot) return;
dot.className = ok ? "online" : "offline";
label.textContent = ok ? "Live" : "Offline";
if (ok) ts.textContent = " · " + new Date().toLocaleTimeString("id-ID");
};
</script>
<script src="assets/js/node-red.js"></script>
<?php require_once 'partials/footer.php'; ?>

48
login.php Normal file
View File

@ -0,0 +1,48 @@
<?php
require_once 'functions/auth.php';
// Redirect ke dashboard jika sudah login
if(isLoggedIn()) {
header("Location: dashboard.php");
exit;
}
$error = '';
if($_SERVER['REQUEST_METHOD'] == 'POST') {
$email = $_POST['email'];
$password = $_POST['password'];
$res = loginUser($email, $password);
if($res['success']) {
header("Location: dashboard.php");
exit;
} else {
$error = $res['error'];
}
}
require_once 'partials/header.php';
?>
<div class="bg-login">
<?php require_once 'partials/navbar.php'; ?>
<div class="auth-container">
<div class="card auth-card">
<h2>Login</h2>
<?php if($error): ?>
<p class="message msg-error"><?= htmlspecialchars($error) ?></p>
<?php endif; ?>
<form method="POST">
<input type="email" name="email" placeholder="user@gmail.com" required>
<input type="password" name="password" placeholder="********" required>
<a href="forgot.php" class="forgot-password">
Lupa password?
</a>
<button type="submit">Start</button>
</form>
</div>
</div>
</div>
<?php require_once 'partials/footer.php'; ?>

8
logout.php Normal file
View File

@ -0,0 +1,8 @@
<?php
// Mulai session, hancurkan, dan redirect ke index (Home)
session_start();
session_unset();
session_destroy();
header("Location: index.php");
exit();
?>

202
monitoring.php Normal file
View File

@ -0,0 +1,202 @@
<?php
require_once 'functions/auth.php';
$currentPage = basename($_SERVER['PHP_SELF']);
if(!isLoggedIn()) { header("Location: login.php"); exit; }
require_once 'partials/header.php';
?>
<style>
.sensor-bar-wrap {
width: 100%;
height: 5px;
background: rgba(255,255,255,.15);
border-radius: 99px;
margin-top: 8px;
overflow: hidden;
}
.sensor-bar-fill {
height: 100%;
border-radius: 99px;
background: #22c55e;
width: 0%;
transition: width .6s ease, background .3s;
}
#conn-status {
display: inline-block;
width: 9px; height: 9px;
border-radius: 50%;
background: #6b7280;
margin-right: 6px;
transition: background .3s;
}
#conn-status.online { background: #22c55e; box-shadow: 0 0 6px #22c55e; }
#conn-status.offline { background: #ef4444; box-shadow: 0 0 6px #ef4444; }
#last-update { font-size: 11px; color: rgba(255,255,255,.55); margin-left: 6px; }
.device-badge {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px;
border-radius: 99px;
font-size: 11px;
font-weight: 700;
background: rgba(255,255,255,.1);
color: rgba(255,255,255,.5);
transition: background .3s, color .3s;
}
.device-badge.on { background: rgba(34,197,94,.2); color: #22c55e; }
.device-badge.off { background: rgba(239,68,68,.15); color: #ef4444; }
</style>
<div class="dashboard-layout">
<?php require_once 'partials/sidebar.php'; ?>
<div class="main-content page-enter">
<!-- TOPBAR -->
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px; flex-wrap:wrap; gap:10px;">
<div style="display:flex; align-items:center; gap:10px; flex-wrap:wrap;">
<div style="display:flex; align-items:center; color:rgba(255,255,255,.7); font-size:13px;">
<span id="conn-status"></span>
<span id="conn-label">Menghubungkan…</span>
<span id="last-update"></span>
</div>
<span class="device-badge" id="badge-relay">
</span>
<span class="device-badge" id="badge-lampu">
</span>
</div>
<div class="topbar-icons">
<div class="notif" id="notifBtn">
<i class="fas fa-bell"></i>
<span class="notif-badge">0</span>
<div class="notif-dropdown" id="notifDropdown">
<p><b>Notifikasi</b></p>
<ul><li>Memuat data…</li></ul>
</div>
</div>
<div class="user" id="userBtn">
<i class="fas fa-user"></i>
<div class="user-dropdown" id="userDropdown">
<p><?= htmlspecialchars($_SESSION['email'] ?? 'User') ?></p>
<a href="logout.php">Logout</a>
</div>
</div>
</div>
</div>
<!-- KARTU ATAS: Amonia & Kekeruhan -->
<div class="mon-top-grid">
<div class="mon-circle-card">
<div class="circle-val" id="val-amonia"> mg/L<mg>
<L></L></div>
<div class="title">Amonia</div>
<div class="sensor-bar-wrap">
<div class="sensor-bar-fill" id="bar-amonia"></div>
</div>
</div>
<div class="mon-circle-card">
<div class="circle-val" id="val-distance"> cm</div>
<div class="title">Distance</div>
<div class="sensor-bar-wrap">
<div class="sensor-bar-fill" id="bar-distance"></div>
</div>
</div>
<div class="mon-circle-card">
<!-- kekeruhan = turbidity dari topic aquamonn/kekeruhan -->
<div class="circle-val" id="val-kekeruhan"> NTU</div>
<div class="title">Kekeruhan</div>
<div class="sensor-bar-wrap">
<div class="sensor-bar-fill" id="bar-kekeruhan"></div>
</div>
</div>
</div>
<!-- KARTU BAWAH: Suhu & pH -->
<div class="mon-bot-grid">
<!-- Suhu field "suhu" -->
<div class="mon-pill-card">
<span style="font-size:1.5rem;">☁️</span>
<div style="text-align:right; line-height:1; flex:1;">
<span id="val-suhu" style="font-size:1.8rem; font-weight:800;"></span><br>
<span style="font-size:0.8rem; font-weight:normal;">Celcius</span>
<div class="sensor-bar-wrap" style="margin-top:6px;">
<div class="sensor-bar-fill" id="bar-suhu"></div>
</div>
</div>
</div>
<!-- pH field "ph" (Node-RED) / "pH" (ESP32) keduanya ditangani -->
<div class="mon-pill-card">
<div style="flex:1; text-align:left;">
<span id="val-ph" style="font-size:1.5rem;"> pH</span>
<div class="sensor-bar-wrap" style="margin-top:6px;">
<div class="sensor-bar-fill" id="bar-ph"></div>
</div>
</div>
<span style="font-size:1.5rem;">💧</span>
</div>
</div>
</div>
<?php require_once 'partials/footer.php'; ?>
<script>
/**
* NODERED_URL = URL server Node-RED (misal http://192.168.1.x:1880)
* node-red.js akan panggil NODERED_URL + "/get-sensor"
*
* FIREBASE_DB_URL dipakai sebagai fallback jika NODERED_URL tidak diset.
*
* Ganti IP sesuai server Node-RED kamu.
*/
window.NODERED_URL = "https://node-red.aquamonn.online";
window.FIREBASE_DB_URL = "<?= rtrim(FIREBASE_DB_URL, '/') ?>";
// Jika NODERED_URL tidak dikonfigurasi di PHP, kosongkan agar
// node-red.js fallback ke Firebase langsung.
if (!window.NODERED_URL) {
// Firebase path sesuai flow:
// https://aquascape-67707-default-rtdb.firebaseio.com/sensor.json
window.FIREBASE_DB_URL = window.FIREBASE_DB_URL || "https://aquascape-67707-default-rtdb.firebaseio.com";
}
window.onSensorUpdate = function(ok) {
const dot = document.getElementById("conn-status");
const label = document.getElementById("conn-label");
const ts = document.getElementById("last-update");
if (!dot) return;
dot.className = ok ? "online" : "offline";
label.textContent = ok ? "Live" : "Offline";
if (ok) ts.textContent = " · " + new Date().toLocaleTimeString("id-ID");
};
</script>
<script src="assets/js/node-red.js"></script>
<script>
const notifBtn = document.getElementById("notifBtn");
const notifDropdown = document.getElementById("notifDropdown");
const userBtn = document.getElementById("userBtn");
const userDropdown = document.getElementById("userDropdown");
notifBtn.onclick = e => {
e.stopPropagation();
notifDropdown.classList.toggle("show");
userDropdown.classList.remove("show");
};
userBtn.onclick = e => {
e.stopPropagation();
userDropdown.classList.toggle("show");
notifDropdown.classList.remove("show");
};
document.onclick = () => {
notifDropdown.classList.remove("show");
userDropdown.classList.remove("show");
};
</script>

7
partials/footer.php Normal file
View File

@ -0,0 +1,7 @@
</body>
<script>
document.getElementById("toggleSidebar").onclick = function() {
document.querySelector(".sidebar").classList.toggle("hide");
};
</script>
</html>

14
partials/header.php Normal file
View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aquascape IoT Dashboard</title>
<!-- Include Custom CSS -->
<link rel="stylesheet" href="assets/css/custom.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@600&display=swap" rel="stylesheet">
<!-- Include Chart.js Library -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>

13
partials/navbar.php Normal file
View File

@ -0,0 +1,13 @@
<nav class="navbar">
<div class="logo">Aquascape</div>
<div class="nav-links">
<a href="index.php">HOME</a>
<?php if(isset($_SESSION['user_token'])): ?>
<a href="dashboard.php">DASHBOARD</a>
<a href="logout.php">LOGOUT</a>
<?php else: ?>
<a href="login.php">LOGIN</a>
<a href="register.php">REGISTER</a>
<?php endif; ?>
</div>
</nav>

59
partials/sidebar.php Normal file
View File

@ -0,0 +1,59 @@
<?php
$currentPage = basename($_SERVER['PHP_SELF']);
?>
<div class="sidebar" id="sidebar">
<div class="sidebar-header" style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h3 class="sidebar-title" style="margin:0; font-size:18px; line-height:1;">
DASHBOARD
</h3>
<button id="toggleSidebar"
style="background:none; border:none; color:black; font-size:18px; cursor:pointer;">
<i class="fas fa-bars"></i>
</button>
</div>
<ul>
<li class="<?= ($currentPage == 'dashboard.php') ? 'active' : '' ?>">
<a href="dashboard.php">
<i class="fas fa-home"></i>
<span class="text">Dashboard</span>
</a>
</li>
<li class="<?= ($currentPage == 'monitoring.php') ? 'active' : '' ?>">
<a href="monitoring.php">
<i class="fas fa-chart-line"></i>
<span class="text">Monitoring Parameter</span>
</a>
</li>
<li class="<?= ($currentPage == 'kontrol.php') ? 'active' : '' ?>">
<a href="kontrol.php">
<i class="fas fa-sliders-h"></i>
<span class="text">Sistem Kontrol</span>
</a>
</li>
<li class="<?= ($currentPage == 'catatan.php') ? 'active' : '' ?>">
<a href="catatan.php">
<i class="fas fa-note-sticky"></i>
<span class="text">Catatan</span>
</a>
</li>
<br><br>
<li>
<a href="logout.php">
<i class="fas fa-sign-out-alt"></i>
<span class="text">Logout</span>
</a>
</li>
</ul>
</div>

51
register.php Normal file
View File

@ -0,0 +1,51 @@
<?php
require_once 'functions/auth.php';
// Redirect ke dashboard jika sudah login
if(isLoggedIn()) {
header("Location: dashboard.php");
exit;
}
$error = '';
$success = '';
if($_SERVER['REQUEST_METHOD'] == 'POST') {
$username = $_POST['username'];
$email = $_POST['email'];
$password = $_POST['password'];
$res = registerUser($username, $email, $password);
if($res['success']) {
$success = 'Registrasi berhasil! Silahkan Login.';
} else {
$error = $res['error'];
}
}
require_once 'partials/header.php';
?>
<div class="bg-login">
<?php require_once 'partials/navbar.php'; ?>
<div class="auth-container">
<div class="card auth-card">
<h2>Register</h2>
<?php if($error): ?>
<p class="message msg-error"><?= htmlspecialchars($error) ?></p>
<?php endif; ?>
<?php if($success): ?>
<p class="message msg-success"><?= htmlspecialchars($success) ?></p>
<?php endif; ?>
<form method="POST">
<input type="text" name="username" placeholder="Username" required>
<input type="email" name="email" placeholder="Email" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Register</button>
</form>
</div>
</div>
</div>
<?php require_once 'partials/footer.php'; ?>