Initial commit

This commit is contained in:
Abil 2026-07-09 12:17:17 +07:00 committed by Abil
commit 9f29342857
156 changed files with 90065 additions and 0 deletions

306
DEMO_SIDEBAR.html Normal file
View File

@ -0,0 +1,306 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Smart Pot - Sidebar Demo</title>
<link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* Demo specific styles */
.demo-card {
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.demo-card h2 {
color: #1f2937;
font-size: 24px;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.demo-card p {
color: #6b7280;
font-size: 16px;
line-height: 1.6;
margin-bottom: 10px;
}
.feature-list {
list-style: none;
padding: 0;
}
.feature-list li {
padding: 10px 0;
border-bottom: 1px solid #e5e7eb;
color: #374151;
display: flex;
align-items: center;
gap: 10px;
}
.feature-list li:last-child {
border-bottom: none;
}
.feature-list i {
color: #059669;
font-size: 18px;
}
.alert-info {
background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
border-left: 4px solid #3b82f6;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
}
.alert-info strong {
color: #1e40af;
display: block;
margin-bottom: 8px;
font-size: 18px;
}
.alert-info p {
color: #1e3a8a;
margin: 0;
}
</style>
<script src="assets/js/sidebar.js"></script>
</head>
<body>
<!-- Sidebar Overlay for Mobile -->
<div class="sidebar-overlay" id="sidebarOverlay" onclick="toggleSidebar()"></div>
<!-- Dashboard Page -->
<div class="dashboard-page">
<!-- Sidebar -->
<aside class="sidebar" id="sidebar">
<!-- Sidebar Toggle Button -->
<button class="sidebar-toggle" onclick="toggleSidebarCollapse()">
<i class="fas fa-chevron-left"></i>
</button>
<!-- Sidebar Brand -->
<div class="sidebar-brand">
<img src="assets/img/logo.png" alt="Smart Pot Logo" class="sidebar-logo">
<div class="sidebar-title">SMART POT</div>
<div class="sidebar-subtitle">IoT Monitoring</div>
</div>
<!-- Sidebar Menu -->
<nav class="sidebar-menu">
<a href="dashboard/" class="menu-item active">
<i class="fas fa-home"></i>
<span>Dashboard</span>
</a>
<a href="controller/" class="menu-item">
<i class="fas fa-sliders-h"></i>
<span>Controller</span>
</a>
<a href="histori/" class="menu-item">
<i class="fas fa-history"></i>
<span>Histori</span>
</a>
<a href="kelola-user/" class="menu-item">
<i class="fas fa-users"></i>
<span>Kelola User</span>
</a>
</nav>
<!-- Sidebar Footer -->
<div class="sidebar-footer">
<div class="sidebar-user">
<div class="user-avatar">
<i class="fas fa-user"></i>
</div>
<div class="user-info">
<div class="user-name">Administrator</div>
<div class="user-email">admin@smartpot.com</div>
</div>
</div>
<button id="signOutBtn" class="btn-logout">
<i class="fas fa-sign-out-alt"></i>
<span>Logout</span>
</button>
</div>
</aside>
<!-- Main Wrapper -->
<div class="main-wrapper">
<!-- Top Header -->
<header class="top-header">
<div class="header-title">
<button class="mobile-menu-btn" onclick="toggleSidebar()" style="display: none;">
<i class="fas fa-bars"></i>
</button>
<i class="fas fa-rocket"></i>
<h1>Sidebar Layout Demo</h1>
</div>
<div class="header-actions">
<div class="header-time" id="currentTime"></div>
</div>
</header>
<!-- Main Content -->
<div class="main-content">
<!-- Welcome Alert -->
<div class="alert-info">
<strong>🎉 Selamat! Dashboard Smart_Pot Telah Berhasil Di-Update!</strong>
<p>Layout sidebar modern telah berhasil diterapkan. Coba klik tombol toggle di sidebar untuk collapse/expand, atau resize browser untuk melihat mode responsive.</p>
</div>
<!-- Demo Cards -->
<div class="demo-card">
<h2><i class="fas fa-magic"></i> Fitur Utama Sidebar</h2>
<ul class="feature-list">
<li>
<i class="fas fa-check-circle"></i>
<span><strong>Fixed Sidebar</strong> - Sidebar tetap di kiri dan tidak scroll bersama konten</span>
</li>
<li>
<i class="fas fa-check-circle"></i>
<span><strong>Collapsible</strong> - Klik tombol toggle untuk minimize/maximize sidebar</span>
</li>
<li>
<i class="fas fa-check-circle"></i>
<span><strong>Active Highlighting</strong> - Menu yang aktif memiliki highlight warna dan indicator bar</span>
</li>
<li>
<i class="fas fa-check-circle"></i>
<span><strong>Responsive</strong> - Di mobile, sidebar jadi slide-in menu dengan overlay</span>
</li>
<li>
<i class="fas fa-check-circle"></i>
<span><strong>Smooth Animation</strong> - Semua transisi menggunakan smooth CSS animations</span>
</li>
<li>
<i class="fas fa-check-circle"></i>
<span><strong>User Info</strong> - Info user dan tombol logout di bagian bawah sidebar</span>
</li>
</ul>
</div>
<div class="demo-card">
<h2><i class="fas fa-palette"></i> Design Modern</h2>
<p>
Dashboard menggunakan design modern dengan:
</p>
<ul class="feature-list">
<li>
<i class="fas fa-paint-brush"></i>
<span>Gradient hijau untuk sidebar sesuai branding Smart_Pot</span>
</li>
<li>
<i class="fas fa-image"></i>
<span>Logo circular dengan background putih di bagian atas sidebar</span>
</li>
<li>
<i class="fas fa-square-full"></i>
<span>Border radius dan shadow untuk efek depth</span>
</li>
<li>
<i class="fas fa-icons"></i>
<span>Font Awesome icons untuk visual yang jelas</span>
</li>
<li>
<i class="fas fa-mobile-alt"></i>
<span>Responsive layout untuk semua ukuran device</span>
</li>
</ul>
</div>
<div class="demo-card">
<h2><i class="fas fa-keyboard"></i> Cara Menggunakan</h2>
<p><strong>Desktop Mode:</strong></p>
<ul class="feature-list">
<li>
<i class="fas fa-arrow-pointer"></i>
<span>Klik menu di sidebar untuk navigasi antar halaman</span>
</li>
<li>
<i class="fas fa-compress"></i>
<span>Klik tombol chevron (◄/►) untuk collapse/expand sidebar</span>
</li>
<li>
<i class="fas fa-mouse-pointer"></i>
<span>Hover menu untuk melihat efek highlight</span>
</li>
</ul>
<p style="margin-top: 20px;"><strong>Mobile Mode:</strong></p>
<ul class="feature-list">
<li>
<i class="fas fa-bars"></i>
<span>Tap tombol hamburger (☰) di header untuk buka sidebar</span>
</li>
<li>
<i class="fas fa-times-circle"></i>
<span>Tap di luar sidebar (overlay) untuk tutup</span>
</li>
</ul>
</div>
<div class="demo-card">
<h2><i class="fas fa-folder-open"></i> File yang Dimodifikasi</h2>
<ul class="feature-list">
<li>
<i class="fas fa-file-code"></i>
<span><strong>assets/css/style.css</strong> - Styling sidebar dan layout baru</span>
</li>
<li>
<i class="fas fa-file-code"></i>
<span><strong>assets/js/sidebar.js</strong> - JavaScript untuk sidebar functionality</span>
</li>
<li>
<i class="fas fa-file-code"></i>
<span><strong>dashboard/index.html</strong> - Halaman monitoring dengan sidebar</span>
</li>
<li>
<i class="fas fa-file-code"></i>
<span><strong>controller/index.html</strong> - Halaman controller dengan sidebar</span>
</li>
<li>
<i class="fas fa-file-code"></i>
<span><strong>histori/index.html</strong> - Halaman histori dengan sidebar</span>
</li>
<li>
<i class="fas fa-file-code"></i>
<span><strong>kelola-user/index.html</strong> - Halaman kelola user dengan sidebar</span>
</li>
</ul>
</div>
<div class="demo-card">
<h2><i class="fas fa-info-circle"></i> Dokumentasi Lengkap</h2>
<p>
Untuk dokumentasi lengkap mengenai sidebar layout, silakan baca file:
</p>
<p style="background: #f3f4f6; padding: 15px; border-radius: 8px; font-family: monospace; margin-top: 10px;">
📄 <strong>SIDEBAR_LAYOUT_GUIDE.md</strong>
</p>
<p style="margin-top: 15px;">
File tersebut berisi penjelasan detail tentang:
</p>
<ul class="feature-list">
<li><i class="fas fa-check"></i> <span>Fitur-fitur sidebar</span></li>
<li><i class="fas fa-check"></i> <span>Design elements dan color scheme</span></li>
<li><i class="fas fa-check"></i> <span>Cara customization</span></li>
<li><i class="fas fa-check"></i> <span>Responsive breakpoints</span></li>
<li><i class="fas fa-check"></i> <span>Tips dan best practices</span></li>
</ul>
</div>
</div>
</div>
</div>
</body>
</html>

45
app.js Normal file
View File

@ -0,0 +1,45 @@
import { initializeApp } from "https://www.gstatic.com/firebasejs/12.7.0/firebase-app.js";
import { getAnalytics } from "https://www.gstatic.com/firebasejs/12.7.0/firebase-analytics.js";
import { getAuth, signOut, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/12.7.0/firebase-auth.js";
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "AIzaSyCQWvoDxDyVCuLEDiwammjUIVYxVARzJig",
authDomain: "project-ta-951b4.firebaseapp.com",
projectId: "project-ta-951b4",
storageBucket: "project-ta-951b4.firebasestorage.app",
messagingSenderId: "217854138058",
appId: "1:217854138058:web:50a5bcd5a61ac1820c4633",
measurementId: "G-6ML8QQEGNZ"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
// Check authentication
onAuthStateChanged(auth, (user) => {
if (user) {
document.getElementById('dashboardPage').hidden = false;
document.getElementById('loginMessage').hidden = true;
document.getElementById('userEmail').textContent = user.email;
document.getElementById('welcomeMessage').textContent = `Welcome back, ${user.email}!`;
window.scrollTo(0, 0);
} else {
document.getElementById('dashboardPage').hidden = true;
document.getElementById('loginMessage').hidden = false;
setTimeout(() => {
window.location.href = '/';
}, 2000);
}
});
// Sign out function
document.getElementById('signOutBtn').addEventListener('click', () => {
signOut(auth).then(() => {
console.log('User signed out');
window.location.href = '/';
}).catch((error) => {
console.error(error);
});
});

3577
assets/css/main.css Normal file

File diff suppressed because it is too large Load Diff

5184
assets/css/style.css Normal file

File diff suppressed because it is too large Load Diff

BIN
assets/img/ahmad.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
assets/img/bg/bg-8.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
assets/img/cta/cta-1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
assets/img/hero-img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

1
assets/img/home.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

128
assets/img/logo.jti.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 70 KiB

BIN
assets/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 KiB

BIN
assets/img/logo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
assets/img/logo_kampus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 KiB

BIN
assets/img/logo_mitra.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

BIN
assets/img/mitra.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

BIN
assets/img/mitra1.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

BIN
assets/img/mitra2.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

BIN
assets/img/mitra3.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

BIN
assets/img/mitra4.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

BIN
assets/img/mitra5.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

BIN
assets/img/mitra6.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

BIN
assets/img/mitra7.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

BIN
assets/img/mitra8.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 KiB

BIN
assets/img/mitra9.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
assets/img/why-us.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
assets/img/wisnu.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 MiB

BIN
assets/img/zamira.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 MiB

720
assets/js/app.js Normal file
View File

@ -0,0 +1,720 @@
import { initializeApp } from "https://www.gstatic.com/firebasejs/12.7.0/firebase-app.js";
import { getAnalytics } from "https://www.gstatic.com/firebasejs/12.7.0/firebase-analytics.js";
import { getAuth, signOut, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/12.7.0/firebase-auth.js";
import { getDatabase, ref, set, get, onValue, update } from "https://www.gstatic.com/firebasejs/12.7.0/firebase-database.js";
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "AIzaSyCQWvoDxDyVCuLEDiwammjUIVYxVARzJig",
authDomain: "project-ta-951b4.firebaseapp.com",
databaseURL: "https://project-ta-951b4-default-rtdb.firebaseio.com",
projectId: "project-ta-951b4",
storageBucket: "project-ta-951b4.firebasestorage.app",
messagingSenderId: "217854138058",
appId: "1:217854138058:web:50a5bcd5a61ac1820c4633",
measurementId: "G-6ML8QQEGNZ"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const database = getDatabase(app);
const aktuatorRef = ref(database, 'aktuator');
// Sensor readings RTDB node (auto-detect)
// If you already know the exact RTDB path for sensor readings, set it here (example: 'monitoring' or 'monitoring/sensor').
const SENSOR_PATH_OVERRIDE = '';
const SENSOR_PATH_CANDIDATES = [
'sensor',
'sensors',
'monitoring',
'monitor',
'data_sensor',
'sensor_data',
// common alternatives
'realtime',
'data',
'nilai_sensor',
'smartpot'
];
const ACTUATOR_KEYS = {
water: 'mosvet_1',
fertilizer: 'mosvet_2',
mixer: 'mosvet_8'
};
// Pot states (all ON by default)
const potStates = {
1: true,
2: true,
3: true,
4: true,
5: true
};
// Water/Fertilizer states
const actuatorStates = {
water: true,
fertilizer: true,
mixer: true
};
// Normalize toggle values from RTDB (supports boolean, number, and string forms)
function normalizeToggleValue(value, fallback = false) {
if (typeof value === 'boolean') return value;
if (typeof value === 'number') return value !== 0;
if (typeof value === 'string') {
const normalized = value.trim().toLowerCase();
if (['1', 'true', 'on', 'aktif', 'yes', 'y'].includes(normalized)) return true;
if (['0', 'false', 'off', 'nonaktif', 'no', 'n', ''].includes(normalized)) return false;
const numeric = Number(normalized);
if (!Number.isNaN(numeric)) return numeric !== 0;
}
return fallback;
}
// Check if all pots are OFF
function areAllPotsOff() {
return !potStates[1] && !potStates[2] && !potStates[3] && !potStates[4] && !potStates[5];
}
// Show notification/alert to user
function showNotification(message) {
alert(message);
}
function renderToggleUI(statusEl, indicatorEl, isOn) {
if (!statusEl || !indicatorEl) return;
statusEl.textContent = isOn ? 'ON' : 'OFF';
indicatorEl.classList.toggle('active', isOn);
indicatorEl.classList.toggle('inactive', !isOn);
}
// Toggle pot function
window.togglePot = function(potNumber) {
potStates[potNumber] = !potStates[potNumber];
const statusEl = document.getElementById(`status${potNumber}`);
const indicatorEl = document.getElementById(`indicator${potNumber}`);
renderToggleUI(statusEl, indicatorEl, potStates[potNumber]);
// Update Firebase - Pot 1-5 maps to mosvet_3-7
const mosvetNumber = potNumber + 2;
const mosvetKey = `mosvet_${mosvetNumber}`;
update(aktuatorRef, {
[mosvetKey]: potStates[potNumber]
}).then(() => {
console.log(`POT ${potNumber} updated to ${potStates[potNumber]} (${mosvetKey})`);
}).catch((error) => {
console.error('Error updating Firebase:', error);
});
};
// Toggle water actuator (mosvet_1 - Pompa air)
window.toggleWater = function() {
const newWaterState = !actuatorStates.water;
// Check if all pots are OFF and user is trying to turn water ON
if (newWaterState && areAllPotsOff()) {
showNotification('Semua pot dalam status OFF. Aktifkan minimal satu pot sebelum mengaktifkan pompa air.');
return;
}
// If turning water ON, turn fertilizer OFF (but mixer can stay ON)
if (newWaterState) {
actuatorStates.water = true;
actuatorStates.fertilizer = false;
// Keep mixer state as is
// Update water UI
const waterStatusEl = document.getElementById('waterStatus');
const waterIndicatorEl = document.getElementById('waterIndicator');
waterStatusEl.textContent = 'ON';
waterIndicatorEl.classList.add('active');
waterIndicatorEl.classList.remove('inactive');
// Update fertilizer UI to OFF
const fertilizerStatusEl = document.getElementById('fertilizerStatus');
const fertilizerIndicatorEl = document.getElementById('fertilizerIndicator');
fertilizerStatusEl.textContent = 'OFF';
fertilizerIndicatorEl.classList.add('inactive');
fertilizerIndicatorEl.classList.remove('active');
// Update Firebase
update(aktuatorRef, {
[ACTUATOR_KEYS.water]: true,
[ACTUATOR_KEYS.fertilizer]: false
}).then(() => {
console.log('Water ON, Fertilizer OFF');
}).catch((error) => {
console.error('Error updating Firebase:', error);
});
} else {
// Turning water OFF
actuatorStates.water = false;
const waterStatusEl = document.getElementById('waterStatus');
const waterIndicatorEl = document.getElementById('waterIndicator');
waterStatusEl.textContent = 'OFF';
waterIndicatorEl.classList.add('inactive');
waterIndicatorEl.classList.remove('active');
update(aktuatorRef, {
[ACTUATOR_KEYS.water]: false
}).then(() => {
console.log('Water OFF');
}).catch((error) => {
console.error('Error updating Firebase:', error);
});
}
};
// Toggle fertilizer actuator (mosvet_2 - Pupuk)
window.toggleFertilizer = function() {
const newFertilizerState = !actuatorStates.fertilizer;
// Check if all pots are OFF and user is trying to turn fertilizer ON
if (newFertilizerState && areAllPotsOff()) {
showNotification('Semua pot dalam status OFF. Aktifkan minimal satu pot sebelum mengaktifkan penyiraman pupuk.');
return;
}
// If turning fertilizer ON, turn water OFF (but mixer can stay ON)
if (newFertilizerState) {
actuatorStates.fertilizer = true;
actuatorStates.water = false;
// Keep mixer state as is
// Update fertilizer UI
const fertilizerStatusEl = document.getElementById('fertilizerStatus');
const fertilizerIndicatorEl = document.getElementById('fertilizerIndicator');
fertilizerStatusEl.textContent = 'ON';
fertilizerIndicatorEl.classList.add('active');
fertilizerIndicatorEl.classList.remove('inactive');
// Update water UI to OFF
const waterStatusEl = document.getElementById('waterStatus');
const waterIndicatorEl = document.getElementById('waterIndicator');
waterStatusEl.textContent = 'OFF';
waterIndicatorEl.classList.add('inactive');
waterIndicatorEl.classList.remove('active');
// Update Firebase
update(aktuatorRef, {
[ACTUATOR_KEYS.water]: false,
[ACTUATOR_KEYS.fertilizer]: true
}).then(() => {
console.log('Fertilizer ON, Water OFF');
}).catch((error) => {
console.error('Error updating Firebase:', error);
});
} else {
// Turning fertilizer OFF
actuatorStates.fertilizer = false;
const fertilizerStatusEl = document.getElementById('fertilizerStatus');
const fertilizerIndicatorEl = document.getElementById('fertilizerIndicator');
fertilizerStatusEl.textContent = 'OFF';
fertilizerIndicatorEl.classList.add('inactive');
fertilizerIndicatorEl.classList.remove('active');
update(aktuatorRef, {
[ACTUATOR_KEYS.fertilizer]: false
}).then(() => {
console.log('Fertilizer OFF');
}).catch((error) => {
console.error('Error updating Firebase:', error);
});
}
};
// Toggle mixer actuator (mosvet_8 - Pompa pengaduk)
window.toggleMixer = function() {
const newMixerState = !actuatorStates.mixer;
const mixerStatusEl = document.getElementById('mixerStatus');
const mixerIndicatorEl = document.getElementById('mixerIndicator');
if (!mixerStatusEl || !mixerIndicatorEl) {
return;
}
// Check if all pots are OFF and user is trying to turn mixer ON
if (newMixerState && areAllPotsOff()) {
showNotification('Semua pot dalam status OFF. Aktifkan minimal satu pot sebelum mengaktifkan pengaduk larutan nutrisi.');
return;
}
if (newMixerState) {
actuatorStates.mixer = true;
// Keep water and fertilizer states as is
mixerStatusEl.textContent = 'ON';
mixerIndicatorEl.classList.add('active');
mixerIndicatorEl.classList.remove('inactive');
update(aktuatorRef, {
[ACTUATOR_KEYS.mixer]: true
}).then(() => {
console.log('Mixer ON');
}).catch((error) => {
console.error('Error updating Firebase:', error);
});
} else {
actuatorStates.mixer = false;
mixerStatusEl.textContent = 'OFF';
mixerIndicatorEl.classList.add('inactive');
mixerIndicatorEl.classList.remove('active');
update(aktuatorRef, {
[ACTUATOR_KEYS.mixer]: false
}).then(() => {
console.log('Mixer OFF');
}).catch((error) => {
console.error('Error updating Firebase:', error);
});
}
};
// Check authentication
onAuthStateChanged(auth, (user) => {
const dashboardPageEl = document.getElementById('dashboardPage');
const loginMessageEl = document.getElementById('loginMessage');
if (user) {
if (dashboardPageEl) dashboardPageEl.hidden = false;
if (loginMessageEl) loginMessageEl.hidden = true;
const userEmailEl = document.getElementById('userEmail');
if (userEmailEl) userEmailEl.textContent = user.email;
window.scrollTo(0, 0);
// Load sensor readings from Firebase (RTDB)
startSensorSubscription();
// Load actuator states from Firebase
loadActuatorStates();
} else {
if (dashboardPageEl) dashboardPageEl.hidden = true;
if (loginMessageEl) loginMessageEl.hidden = false;
setTimeout(() => {
window.location.href = '../index.html';
}, 2000);
}
});
// Load actuator states from Firebase
function loadActuatorStates() {
onValue(aktuatorRef, (snapshot) => {
if (snapshot.exists()) {
const data = snapshot.val();
console.log('Actuator data loaded:', data);
// Update pot states (Pot 1-5 maps to mosvet_3-7)
for (let i = 1; i <= 5; i++) {
const mosvetNumber = i + 2;
const mosvetKey = `mosvet_${mosvetNumber}`;
if (data[mosvetKey] !== undefined) {
potStates[i] = normalizeToggleValue(data[mosvetKey], potStates[i]);
const statusEl = document.getElementById(`status${i}`);
const indicatorEl = document.getElementById(`indicator${i}`);
renderToggleUI(statusEl, indicatorEl, potStates[i]);
}
}
// Update water state (mosvet_1)
if (data[ACTUATOR_KEYS.water] !== undefined) {
actuatorStates.water = normalizeToggleValue(data[ACTUATOR_KEYS.water], actuatorStates.water);
const statusEl = document.getElementById('waterStatus');
const indicatorEl = document.getElementById('waterIndicator');
renderToggleUI(statusEl, indicatorEl, actuatorStates.water);
}
// Update fertilizer state (mosvet_2)
if (data[ACTUATOR_KEYS.fertilizer] !== undefined) {
actuatorStates.fertilizer = normalizeToggleValue(data[ACTUATOR_KEYS.fertilizer], actuatorStates.fertilizer);
const statusEl = document.getElementById('fertilizerStatus');
const indicatorEl = document.getElementById('fertilizerIndicator');
renderToggleUI(statusEl, indicatorEl, actuatorStates.fertilizer);
}
// Update mixer state (mosvet_8)
if (data[ACTUATOR_KEYS.mixer] !== undefined) {
actuatorStates.mixer = normalizeToggleValue(data[ACTUATOR_KEYS.mixer], actuatorStates.mixer);
const statusEl = document.getElementById('mixerStatus');
const indicatorEl = document.getElementById('mixerIndicator');
renderToggleUI(statusEl, indicatorEl, actuatorStates.mixer);
}
} else {
// Initialize default values
initializeDefaultActuatorStates();
}
});
}
// Initialize default actuator states
function initializeDefaultActuatorStates() {
const defaultData = {
mosvet_1: true, // Pompa air
mosvet_2: true, // Pupuk
mosvet_3: true, // Soil 1
mosvet_4: true, // Soil 2
mosvet_5: true, // Soil 3
mosvet_6: true, // Soil 4
mosvet_7: true, // Soil 5
mosvet_8: true // Pompa pengaduk
};
set(aktuatorRef, defaultData)
.then(() => {
console.log('Default actuator states initialized');
})
.catch((error) => {
console.error('Error initializing actuator states:', error);
});
}
function pickNumber(...candidates) {
for (const value of candidates) {
if (value === null || value === undefined) continue;
const n = typeof value === 'number' ? value : Number(value);
if (!Number.isNaN(n)) return n;
}
return null;
}
function parseWaterflowAvailability(value) {
if (value === null || value === undefined) return null;
if (typeof value === 'boolean') return value;
if (typeof value === 'number') return value > 0;
if (typeof value === 'string') {
const normalized = value.trim().toLowerCase();
if (['1', 'true', 'on', 'ada', 'available', 'air', 'yes', 'y'].includes(normalized)) return true;
if (['0', 'false', 'off', 'habis', 'empty', 'no', 'n', ''].includes(normalized)) return false;
const numeric = Number(normalized);
if (!Number.isNaN(numeric)) return numeric > 0;
}
return Boolean(value);
}
function updateWaterflowBadge(isAvailable) {
const badge = document.getElementById('waterflowBadge');
const dot = document.getElementById('waterflowDot');
const text = document.getElementById('waterflowText');
if (badge) {
badge.classList.toggle('has-water', isAvailable);
badge.classList.toggle('no-water', !isAvailable);
}
if (dot) {
dot.classList.toggle('active', isAvailable);
dot.classList.toggle('inactive', !isAvailable);
}
if (text) {
text.textContent = isAvailable ? 'AIR TERSEDIA' : 'AIR HABIS';
}
}
function updateSensorUI(data) {
if (!data || typeof data !== 'object') return;
// Temperature
const temperature = pickNumber(
data.dhtTemp,
data.temperature,
data.temp,
data.suhu,
data.suhu_dht,
data.dht
);
if (temperature !== null) {
const el = document.getElementById('dhtTemp');
if (el) el.textContent = Math.round(temperature * 10) / 10;
}
// Air humidity
const humidity = pickNumber(
data.dhtHumidity,
data.humidity,
data.kelembapan,
data.kelembapan_udara,
data.airHumidity,
data.air_humidity,
data.humid
);
if (humidity !== null) {
const el = document.getElementById('dhtHumidity');
if (el) el.textContent = Math.round(humidity * 10) / 10;
}
const waterflowRaw = data.water_flow ?? data.waterflow ?? data.waterFlow;
const waterflowAvailable = parseWaterflowAvailability(waterflowRaw);
if (waterflowAvailable !== null) {
updateWaterflowBadge(waterflowAvailable);
}
// Light
const light = pickNumber(
data.ldr,
data.sunLight,
data.light,
data.cahaya,
data.ldr_percent,
data.lux
);
if (light !== null) {
const el = document.getElementById('sunLight');
if (el) el.textContent = Math.round(light * 100) / 100;
const statusEl = document.querySelector('.sensor-light .status');
if (statusEl) {
const sunStatus = light < 250 ? 'Gelap' : light < 1000 ? 'Sedang' : light < 2000 ? 'Terang' : 'Sangat Terang';
statusEl.textContent = sunStatus;
}
}
// Moisture per pot (supports several common shapes)
const moistureRoot =
data.moisture ||
data.kelembaban ||
data.soilMoisture ||
data.soil_moisture ||
data.kelembaban_tanah ||
null;
const moistureArray =
(Array.isArray(data.moisture) && data.moisture) ||
(Array.isArray(data.kelembaban) && data.kelembaban) ||
(Array.isArray(data.soilMoisture) && data.soilMoisture) ||
(Array.isArray(data.soil_moisture) && data.soil_moisture) ||
null;
let resolvedSoilCount = 0;
for (let i = 1; i <= 5; i++) {
const direct = pickNumber(
data[`moisture${i}`],
data[`moisture_${i}`],
data[`moisturePot${i}`],
data[`moisture_pot_${i}`],
data[`kelembaban${i}`],
data[`kelembaban_${i}`],
data[`kelembabanTanah${i}`],
data[`kelembaban_tanah_${i}`],
data[`soil${i}`],
data[`soil_${i}`],
data[`soilMoisture${i}`],
data[`soil_moisture_${i}`],
data[`humidity${i}`],
data[`humidity_${i}`],
data[`adc${i}`],
data[`adc_${i}`],
data[`sensor${i}`],
data[`sensor_${i}`],
data[`pot${i}`],
data[`pot_${i}`]
);
const fromArray = moistureArray ? pickNumber(moistureArray[i - 1], moistureArray[i], moistureArray[String(i)]) : null;
const nested = moistureRoot
? pickNumber(
moistureRoot[i],
moistureRoot[String(i)],
moistureRoot[`pot${i}`],
moistureRoot[`pot_${i}`],
moistureRoot[`sensor${i}`],
moistureRoot[`sensor_${i}`],
moistureRoot[`soil${i}`],
moistureRoot[`soil_${i}`],
moistureRoot[`moisture${i}`],
moistureRoot[`moisture_${i}`],
moistureRoot[`kelembaban${i}`],
moistureRoot[`kelembaban_${i}`]
)
: null;
const directPotNode =
data[`pot${i}`] ||
data[`pot_${i}`] ||
data[`sensor${i}`] ||
data[`sensor_${i}`] ||
null;
const potNode = (data.pots && (data.pots[i] || data.pots[String(i)] || data.pots[`pot${i}`]))
|| (data.pot && (data.pot[i] || data.pot[String(i)] || data.pot[`pot${i}`]))
|| directPotNode;
const potMoisture = potNode
? pickNumber(
potNode,
potNode.moisture,
potNode.kelembaban,
potNode.soil,
potNode.soil_moisture,
potNode.soilMoisture,
potNode.kelembaban_tanah,
potNode.humidity,
potNode.adc,
potNode.value,
potNode.nilai
)
: null;
const value = pickNumber(direct, fromArray, nested, potMoisture);
if (value !== null) {
const el = document.getElementById(`moisture${i}`);
if (el) el.textContent = Math.round(value);
resolvedSoilCount++;
}
}
if (resolvedSoilCount === 0 && !didLogSoilDebug) {
didLogSoilDebug = true;
console.warn('[Sensor] Soil values not mapped yet. Available keys:', Object.keys(data || {}));
}
}
async function findFirstExistingPath(paths) {
for (const path of paths) {
try {
const snapshot = await get(ref(database, path));
if (snapshot.exists()) return path;
} catch (error) {
console.warn(`[Sensor] Cannot probe path '${path}':`, error);
}
}
return paths[0];
}
function looksLikeSensorObject(obj) {
if (!obj || typeof obj !== 'object') return false;
const keys = Object.keys(obj);
return keys.some((k) =>
/temp|suhu|dht|cahaya|light|ldr|lux|moisture|kelembaban|soil/i.test(k)
);
}
async function detectSensorPathFromRoot() {
// Best-effort: read root, then pick a child node that contains sensor-ish fields.
try {
const rootRef = ref(database, '/');
const rootSnap = await get(rootRef);
if (!rootSnap.exists()) return null;
const rootVal = rootSnap.val();
if (!rootVal || typeof rootVal !== 'object') return null;
const topKeys = Object.keys(rootVal);
console.log('[Sensor] RTDB root keys:', topKeys);
for (const key of topKeys) {
const child = rootVal[key];
if (looksLikeSensorObject(child)) {
return key;
}
}
return null;
} catch (error) {
console.warn('[Sensor] Root probe failed (rules/network):', error);
return null;
}
}
let stopSensorSubscription = null;
let didLogFirstSensorPayload = false;
let didLogSoilDebug = false;
let sensorCandidateIndex = 0;
async function startSensorSubscription(forcedPath = null, attempt = 0) {
try {
if (typeof stopSensorSubscription === 'function') {
stopSensorSubscription();
stopSensorSubscription = null;
}
const overridePath = typeof SENSOR_PATH_OVERRIDE === 'string' ? SENSOR_PATH_OVERRIDE.trim() : '';
if (!forcedPath && overridePath) {
forcedPath = overridePath;
}
// 1) If forcedPath provided, use it directly
// 2) Try root-based detection (most accurate)
// 3) Otherwise, try candidates (fallback)
const detected = forcedPath ? null : await detectSensorPathFromRoot();
const sensorPath = forcedPath || detected || (await findFirstExistingPath(SENSOR_PATH_CANDIDATES));
console.log(
'[Sensor] Subscribing to:',
sensorPath,
forcedPath ? '(forced)' : (detected ? '(detected)' : '(fallback)')
);
stopSensorSubscription = onValue(
ref(database, sensorPath),
(snapshot) => {
if (!snapshot.exists()) {
console.warn('[Sensor] No data at path:', sensorPath);
// If we got here via fallback and there is no data, try the next candidate.
// This helps when the real sensor node name is different.
if (!forcedPath && !detected && SENSOR_PATH_CANDIDATES.length > 1) {
if (attempt < SENSOR_PATH_CANDIDATES.length - 1) {
const nextIndex = (sensorCandidateIndex + 1) % SENSOR_PATH_CANDIDATES.length;
sensorCandidateIndex = nextIndex;
const nextPath = SENSOR_PATH_CANDIDATES[sensorCandidateIndex];
if (nextPath !== sensorPath) {
console.log('[Sensor] Trying next path:', nextPath);
startSensorSubscription(nextPath, attempt + 1);
}
} else {
console.warn('[Sensor] All sensor path candidates returned empty. Set the correct RTDB path in app.js');
}
}
return;
}
const payload = snapshot.val();
if (!didLogFirstSensorPayload) {
didLogFirstSensorPayload = true;
console.log('[Sensor] First payload:', payload);
}
updateSensorUI(payload);
},
(error) => {
console.error('[Sensor] Subscription error:', error);
}
);
} catch (error) {
console.error('[Sensor] Failed to start subscription:', error);
}
}
// Sign out function
const signOutBtn = document.getElementById('signOutBtn');
if (signOutBtn) {
signOutBtn.addEventListener('click', () => {
signOut(auth).then(() => {
console.log('User signed out');
window.location.href = '../index.html';
}).catch((error) => {
console.error(error);
});
});
}

122
assets/js/controller.js Normal file
View File

@ -0,0 +1,122 @@
import { initializeApp } from "https://www.gstatic.com/firebasejs/12.7.0/firebase-app.js";
import { getAnalytics } from "https://www.gstatic.com/firebasejs/12.7.0/firebase-analytics.js";
import { getAuth, signOut, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/12.7.0/firebase-auth.js";
import { getDatabase, ref, set, get, onValue } from "https://www.gstatic.com/firebasejs/12.7.0/firebase-database.js";
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "AIzaSyCQWvoDxDyVCuLEDiwammjUIVYxVARzJig",
authDomain: "project-ta-951b4.firebaseapp.com",
databaseURL: "https://project-ta-951b4-default-rtdb.firebaseio.com",
projectId: "project-ta-951b4",
storageBucket: "project-ta-951b4.firebasestorage.app",
messagingSenderId: "217854138058",
appId: "1:217854138058:web:50a5bcd5a61ac1820c4633",
measurementId: "G-6ML8QQEGNZ"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const database = getDatabase(app);
const kontrolRef = ref(database, 'kontrol');
// Notification function
function showNotification(message, type = 'success') {
const notification = document.getElementById('notification');
const messageEl = notification.querySelector('.notification-message');
notification.classList.remove('success', 'error', 'warning', 'hidden');
notification.classList.add(type);
messageEl.textContent = message;
setTimeout(() => {
notification.classList.add('hidden');
}, 4000);
}
// Check authentication
onAuthStateChanged(auth, (user) => {
if (user) {
document.getElementById('dashboardPage').hidden = false;
// document.getElementById('loginMessage').hidden = true;
document.getElementById('userEmail').textContent = user.email;
window.scrollTo(0, 0);
initializeController();
} else {
document.getElementById('dashboardPage').hidden = true;
// document.getElementById('loginMessage').hidden = false;
setTimeout(() => {
window.location.href = '../index.html';
}, 2000);
}
});
// Sign out function
document.getElementById('signOutBtn').addEventListener('click', () => {
signOut(auth).then(() => {
console.log('User signed out');
window.location.href = '../index.html';
}).catch((error) => {
console.error(error);
});
});
// Initialize controller
function initializeController() {
// Load data from Firebase
loadControllerData();
// Mode Otomatis - Navigate to ModeOtomatis.html
const modeOtomatisBtn = document.getElementById('modeOtomatis');
if (modeOtomatisBtn) {
modeOtomatisBtn.addEventListener('click', function() {
window.location.href = './ModeOtomatis.html';
});
}
// Mode Waktu - Show notification for now
const modeWaktuBtn = document.getElementById('modeWaktu');
if (modeWaktuBtn) {
modeWaktuBtn.addEventListener('click', function() {
window.location.href = './ModeWaktu.html';
});
}
}
// Load controller data from Firebase
function loadControllerData() {
onValue(kontrolRef, (snapshot) => {
if (snapshot.exists()) {
const data = snapshot.val();
console.log('Controller data loaded:', data);
// Update UI if needed
// You can add code here to display current values
} else {
// Initialize default values if not exists
initializeDefaultControllerData();
}
});
}
// Initialize default controller data in Firebase
function initializeDefaultControllerData() {
const defaultData = {
batas_atas: 80,
batas_bawah: 30,
durasi_1: 5,
durasi_2: 5,
otomatis: false,
waktu: false,
waktu_1: "",
waktu_2: ""
};
set(kontrolRef, defaultData)
.then(() => {
console.log('Default controller data initialized');
})
.catch((error) => {
console.error('Error initializing data:', error);
});
}

793
assets/js/histori.js Normal file
View File

@ -0,0 +1,793 @@
import { initializeApp } from "https://www.gstatic.com/firebasejs/12.7.0/firebase-app.js";
import { getAuth, onAuthStateChanged, signOut } from "https://www.gstatic.com/firebasejs/12.7.0/firebase-auth.js";
import { getDatabase, ref, onValue } from "https://www.gstatic.com/firebasejs/12.7.0/firebase-database.js";
// Firebase configuration
const firebaseConfig = {
apiKey: "AIzaSyCQWvoDxDyVCuLEDiwammjUIVYxVARzJig",
authDomain: "project-ta-951b4.firebaseapp.com",
databaseURL: "https://project-ta-951b4-default-rtdb.firebaseio.com",
projectId: "project-ta-951b4",
storageBucket: "project-ta-951b4.firebasestorage.app",
messagingSenderId: "217854138058",
appId: "1:217854138058:web:50a5bcd5a61ac1820c4633",
measurementId: "G-6ML8QQEGNZ"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const database = getDatabase(app);
const historyRef = ref(database, 'history');
let listenersInitialized = false;
let stopHistorySubscription = null;
// Initialize history state
let historyData = [];
let filteredData = [];
let currentPage = 1;
let itemsPerPage = 20;
// Notification function
function showNotification(message, type = 'success') {
const notification = document.getElementById('notification');
if (!notification) return;
const messageEl = notification.querySelector('.notification-message');
notification.classList.remove('success', 'error', 'warning', 'info', 'hidden');
notification.classList.add(type);
if (messageEl) {
messageEl.textContent = message;
}
setTimeout(() => {
notification.classList.add('hidden');
}, 4000);
}
function setElementDisplay(id, displayValue) {
const element = document.getElementById(id);
if (element) {
element.style.display = displayValue;
}
}
function setElementHidden(id, hiddenValue) {
const element = document.getElementById(id);
if (element) {
element.hidden = hiddenValue;
}
}
// Extract pot numbers from pot_aktif data
function extractPotsFromData(potAktif) {
const pots = [];
if (!potAktif) {
return pots;
}
// Handle array format
if (Array.isArray(potAktif)) {
potAktif.forEach((value) => {
const potNum = Number(value);
if (Number.isFinite(potNum) && potNum >= 1 && potNum <= 5) {
pots.push(potNum);
}
});
return Array.from(new Set(pots));
}
// Handle object format
if (typeof potAktif === 'object') {
// Check for numeric keys (like 0, 1, 2, etc.)
const numericKeys = Object.keys(potAktif).filter((k) => /^\d+$/.test(k));
if (numericKeys.length > 0) {
Object.values(potAktif).forEach((value) => {
const potNum = Number(value);
if (Number.isFinite(potNum) && potNum >= 1 && potNum <= 5) {
pots.push(potNum);
}
});
return Array.from(new Set(pots));
}
// Check for pot_1, pot_2 format or pot1, pot2 format
for (let i = 1; i <= 5; i++) {
const potKey = `pot_${i}`;
const potKeyAlt = `pot${i}`;
if (potAktif[potKey] === true || potAktif[potKeyAlt] === true) {
pots.push(i);
}
}
}
return Array.from(new Set(pots));
}
// Get allowed pots for a given event (Self-Contained & Immutable)
function getAllowedPotsForEvent(entry) {
// 1. PRIMARY: Extract directly from 'pots' array or object in the history entry
if (entry.pots !== undefined && entry.pots !== null) {
const extractedPots = extractPotsFromData(entry.pots);
if (extractedPots.length > 0) {
console.log(`✅ Using self-contained pots data:`, extractedPots);
return extractedPots;
}
}
// 2. FALLBACK FOR VERY OLD HISTORY
// If there is no 'pots' data, fallback to inferring from attached sensor data
const type = String(entry.type ?? '').toLowerCase().trim();
if (type.includes('waktu_jadwal') || type.includes('sensor_threshold')) {
const potsWithData = extractPotsWithSensorData(entry);
if (potsWithData.length > 0) {
console.log(`🔍 Inferred pots dari sensor data (fallback):`, potsWithData);
return potsWithData;
}
}
console.log(`❓ Event tidak bisa di-filter (tidak ada info pot)`);
return null;
}
// Extract pots yang memiliki sensor data di event
function extractPotsWithSensorData(entry) {
const potsWithData = [];
const seenPots = new Set();
Object.entries(entry).forEach(([key, value]) => {
const match = key.match(/^soil[_-]?(\d+)$/i);
if (!match) {
return;
}
const pot = parseInt(match[1], 10);
const sensorValue = normalizeNumber(value);
// Hanya hitung pot yang memiliki sensor data valid
if (Number.isFinite(pot) && sensorValue !== null && !seenPots.has(pot)) {
seenPots.add(pot);
potsWithData.push(pot);
}
});
return potsWithData.sort((a, b) => a - b);
}
// Authentication check
onAuthStateChanged(auth, (user) => {
if (user) {
const userEmailEl = document.getElementById('userEmail');
if (userEmailEl) {
userEmailEl.textContent = user.email;
}
setElementDisplay('dashboardPage', 'block');
setElementHidden('loginMessage', true);
initializeHistory();
} else {
setElementDisplay('dashboardPage', 'none');
setElementHidden('loginMessage', false);
setTimeout(() => {
window.location.href = '../index.html';
}, 2000);
}
});
// Logout handler
const signOutBtn = document.getElementById('signOutBtn');
if (signOutBtn) {
signOutBtn.addEventListener('click', async () => {
try {
await signOut(auth);
window.location.href = '../index.html';
} catch (error) {
showNotification('Error logging out: ' + error.message, 'error');
}
});
}
function initializeHistory() {
const entriesSelect = document.getElementById('entriesPerPage');
if (entriesSelect) {
itemsPerPage = parseInt(entriesSelect.value, 10) || 20;
}
if (!listenersInitialized) {
setupPagination();
setupFilterHandlers();
listenersInitialized = true;
}
loadHistoryFromFirebase();
}
function loadHistoryFromFirebase() {
if (typeof stopHistorySubscription === 'function') {
stopHistorySubscription();
stopHistorySubscription = null;
}
stopHistorySubscription = onValue(historyRef, (snapshot) => {
const rawHistory = snapshot.exists() ? snapshot.val() : {};
historyData = transformHistoryToRows(rawHistory);
applyFiltersAndSearch(false);
if (!snapshot.exists()) {
showNotification('Belum ada data histori di Firebase', 'warning');
}
}, (error) => {
console.error('Gagal membaca data history:', error);
showNotification('Gagal mengambil data histori dari Firebase', 'error');
});
}
function transformHistoryToRows(historyObject) {
const rows = [];
let rowCounter = 0;
function walk(node, context = {}) {
if (!node || typeof node !== 'object') {
return;
}
if (isLogObject(node)) {
const parsedRows = parseLogObject(node, context);
parsedRows.forEach((row) => {
rowCounter += 1;
row.id = `row-${rowCounter}`;
rows.push(row);
});
return;
}
Object.entries(node).forEach(([key, value]) => {
const nextContext = { ...context };
if (!nextContext.date && isDateKey(key)) {
nextContext.date = key;
}
if (!nextContext.time && isTimeKey(key)) {
nextContext.time = normalizeTimeString(key);
}
walk(value, nextContext);
});
}
walk(historyObject, {});
rows.sort((a, b) => b.sortTimestamp - a.sortTimestamp);
return rows;
}
function isLogObject(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return false;
}
const keys = Object.keys(value);
return keys.some((key) => {
const lowerKey = key.toLowerCase();
return lowerKey.startsWith('soil_') ||
lowerKey.startsWith('soil') ||
lowerKey === 'kelembapan' ||
lowerKey === 'moisture' ||
lowerKey === 'temp' ||
lowerKey === 'suhu' ||
lowerKey === 'ldr' ||
lowerKey === 'light' ||
lowerKey === 'timestamp' ||
lowerKey === 'water_flow' ||
lowerKey === 'waterflow' ||
lowerKey === 'type' ||
lowerKey === 'event_type' ||
lowerKey === 'activity_type' ||
lowerKey === 'source' ||
lowerKey === 'mode' ||
lowerKey === 'activity' ||
lowerKey === 'event' ||
lowerKey === 'action' ||
lowerKey === 'schedule_name' ||
lowerKey === 'schedule_id' ||
lowerKey === 'jadwal_name' ||
lowerKey === 'jadwal_id' ||
lowerKey === 'schedulename' ||
lowerKey === 'scheduleid' ||
lowerKey === 'pompa_air' ||
lowerKey === 'pompaair' ||
lowerKey === 'pompa_pupuk' ||
lowerKey === 'pompapupuk' ||
lowerKey === 'pompa_pengaduk' ||
lowerKey === 'pompapengaduk' ||
lowerKey === 'water_pump' ||
lowerKey === 'fertilizer_pump' ||
lowerKey === 'mixer_pump';
});
}
function isDateKey(value) {
return /^\d{4}-\d{2}-\d{2}$/.test(value);
}
function isTimeKey(value) {
return /^\d{2}:\d{2}(:\d{2})?$/.test(value);
}
function parseLogObject(entry, context) {
const resolvedDateTime = resolveDateTime(entry, context);
const temperature = normalizeNumber(entry.suhu ?? entry.temp ?? entry.temperature);
const light = normalizeLightValue(entry.ldr ?? entry.light ?? entry.cahaya);
const waterFlow = normalizeNumber(entry.water_flow ?? entry.waterFlow ?? entry.flow);
const status = resolveStatus(entry, waterFlow);
const source = entry.source ?? entry.mode ?? entry.activity ?? entry.event ?? '-';
const type = entry.type ?? entry.event_type ?? entry.activity_type ?? entry.action ?? '-';
// Get allowed pots for filtering based on schedule/threshold
const allowedPots = getAllowedPotsForEvent(entry);
// DEBUG: Log penyiraman events untuk melihat struktur data
if (type && (String(type).toLowerCase().includes('waktu_jadwal') || String(type).toLowerCase().includes('sensor_threshold'))) {
console.log('🔍 Penyiraman Event:', {
time: resolvedDateTime.time,
type: type,
jadwal_id: entry.jadwal_id,
threshold_id: entry.threshold_id,
allowedPots: allowedPots,
allKeys: Object.keys(entry)
});
}
const potRows = extractPotRows(entry, allowedPots);
if (potRows.length > 0) {
return potRows.map((potItem) => ({
date: resolvedDateTime.date,
time: resolvedDateTime.time,
datetime: resolvedDateTime.datetime,
sortTimestamp: resolvedDateTime.sortTimestamp,
timestamp: resolvedDateTime.timestamp,
pot: potItem.pot,
temp: temperature,
light,
moisture: potItem.moisture,
waterFlow,
status,
source,
type,
raw: entry
}));
}
const fallbackPot = normalizeNumber(entry.pot ?? entry.pot_id ?? entry.potNumber);
const fallbackMoisture = normalizeNumber(entry.kelembapan ?? entry.moisture);
return [{
date: resolvedDateTime.date,
time: resolvedDateTime.time,
datetime: resolvedDateTime.datetime,
sortTimestamp: resolvedDateTime.sortTimestamp,
timestamp: resolvedDateTime.timestamp,
pot: fallbackPot,
temp: temperature,
light,
moisture: fallbackMoisture,
waterFlow,
status,
source,
type,
raw: entry
}];
}
function extractPotRows(entry, allowedPots = null) {
const rows = [];
const seenPots = new Set();
Object.entries(entry).forEach(([key, value]) => {
const match = key.match(/^soil[_-]?(\d+)$/i);
if (!match) {
return;
}
const pot = parseInt(match[1], 10);
const moisture = normalizeNumber(value);
if (!Number.isFinite(pot) || moisture === null || seenPots.has(pot)) {
return;
}
// Filter pot if allowedPots is specified (watering event with schedule/threshold info)
if (allowedPots !== null && !allowedPots.includes(pot)) {
return;
}
seenPots.add(pot);
rows.push({ pot, moisture });
});
rows.sort((a, b) => a.pot - b.pot);
return rows;
}
function resolveDateTime(entry, context) {
const timestampRaw = normalizeNumber(entry.timestamp ?? entry.ts);
const timestamp = normalizeTimestamp(timestampRaw);
const dateFromTimestamp = timestamp ? new Date(timestamp) : null;
const date = context.date || normalizeDateString(entry.date) || (dateFromTimestamp ? formatDate(dateFromTimestamp) : '-');
const time = context.time || normalizeTimeString(entry.time) || (dateFromTimestamp ? formatTime(dateFromTimestamp) : '-');
const datetime = `${date} ${time}`.trim();
const sortTimestamp = timestamp || buildTimestampFromDateTime(date, time);
return {
date,
time,
datetime,
timestamp,
sortTimestamp
};
}
function normalizeTimestamp(value) {
if (value === null || value === undefined || Number.isNaN(value)) {
return null;
}
if (value > 0 && value < 100000000000) {
return Math.round(value * 1000);
}
return Math.round(value);
}
function buildTimestampFromDateTime(date, time) {
if (!isDateKey(date)) {
return 0;
}
const safeTime = isTimeKey(time) ? normalizeTimeString(time) : '00:00';
const parsed = new Date(`${date}T${safeTime}:00`);
return Number.isNaN(parsed.getTime()) ? 0 : parsed.getTime();
}
function normalizeDateString(value) {
if (typeof value !== 'string') {
return null;
}
if (isDateKey(value)) {
return value;
}
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) {
return null;
}
return formatDate(parsed);
}
function normalizeTimeString(value) {
if (typeof value !== 'string') {
return null;
}
const match = value.match(/^(\d{2}):(\d{2})(?::\d{2})?$/);
if (!match) {
return null;
}
return `${match[1]}:${match[2]}`;
}
function formatDate(dateObject) {
const year = dateObject.getFullYear();
const month = String(dateObject.getMonth() + 1).padStart(2, '0');
const day = String(dateObject.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function formatTime(dateObject) {
const hours = String(dateObject.getHours()).padStart(2, '0');
const minutes = String(dateObject.getMinutes()).padStart(2, '0');
return `${hours}:${minutes}`;
}
function normalizeNumber(value) {
if (value === null || value === undefined || value === '') {
return null;
}
const number = Number(value);
return Number.isFinite(number) ? number : null;
}
function normalizeLightValue(value) {
const numericValue = normalizeNumber(value);
if (numericValue === null) {
return null;
}
if (numericValue >= 0 && numericValue <= 1) {
return Math.round(numericValue * 100);
}
if (numericValue > 1 && numericValue <= 100) {
return Math.round(numericValue);
}
if (numericValue > 100 && numericValue <= 1023) {
return Math.round((numericValue / 1023) * 100);
}
return Math.round(numericValue);
}
function resolveStatus(entry, waterFlow) {
const type = String(entry.type ?? '').toLowerCase().trim();
// Deteksi Penyiraman: mode waktu atau sensor threshold
if (type.includes('waktu_jadwal') || type.includes('sensor_threshold')) {
return 'Menyiram';
}
// Deteksi Monitoring: auto_log
if (type === 'auto_log') {
return 'Normal';
}
return 'Normal';
}
function getActivityType(item) {
const status = item.status;
if (status === 'Menyiram') {
return 'Penyiraman';
}
return 'Monitoring';
}
function getActivityBadgeClass(activityType) {
return activityType === 'Penyiraman' ? 'activity-watering' : 'activity-monitoring';
}
function formatDisplayValue(value, suffix = '', decimals = 0) {
if (value === null || value === undefined || Number.isNaN(value)) {
return '-';
}
const formatted = decimals > 0 ? value.toFixed(decimals) : Math.round(value).toString();
return `${formatted}${suffix}`;
}
// Display history data in table with pagination
function displayHistoryData() {
const tbody = document.getElementById('historyTableBody');
if (!tbody) {
return;
}
tbody.innerHTML = '';
if (filteredData.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; padding: 40px;">Tidak ada data</td></tr>';
updatePaginationInfo(0, 0, 0);
updatePaginationButtons();
return;
}
// Calculate pagination
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = Math.min(startIndex + itemsPerPage, filteredData.length);
const pageData = filteredData.slice(startIndex, endIndex);
// Display data
pageData.forEach((item) => {
const potCell = Number.isFinite(item.pot)
? `<span class="pot-badge pot-${item.pot}-badge">Pot ${item.pot}</span>`
: '-';
const activityType = getActivityType(item);
const badgeClass = getActivityBadgeClass(activityType);
const activityBadge = `<span class="activity-badge ${badgeClass}">${activityType}</span>`;
const row = document.createElement('tr');
row.innerHTML = `
<td>${item.datetime}</td>
<td>${potCell}</td>
<td>${formatDisplayValue(item.temp, ' C', 1)}</td>
<td>${formatDisplayValue(item.light, '%')}</td>
<td>${formatDisplayValue(item.moisture, '%')}</td>
<td>${activityBadge}</td>
`;
tbody.appendChild(row);
});
// Update pagination info and controls
updatePaginationInfo(startIndex + 1, endIndex, filteredData.length);
updatePaginationButtons();
}
// Update statistics
function updateStatistics(data) {
// Statistics cards have been removed, this function is kept for compatibility
// but doesn't update any elements anymore
return data;
}
function setupFilterHandlers() {
const applyFilterBtn = document.getElementById('applyFilter');
if (applyFilterBtn) {
applyFilterBtn.addEventListener('click', () => {
applyFiltersAndSearch(true);
showNotification('Filter diterapkan', 'success');
});
}
const searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.addEventListener('input', () => {
applyFiltersAndSearch(true);
});
}
}
function applyFiltersAndSearch(resetPage) {
const potFilter = document.getElementById('potFilter')?.value || 'all';
const searchTerm = (document.getElementById('searchInput')?.value || '').trim().toLowerCase();
filteredData = historyData.filter((item) => {
const passPot = potFilter === 'all' || item.pot === parseInt(potFilter, 10);
if (!passPot) {
return false;
}
if (!searchTerm) {
return true;
}
const searchableText = [
item.datetime,
item.date,
item.time,
item.pot,
item.temp,
item.light,
item.moisture,
item.status,
item.type,
item.source
].join(' ').toLowerCase();
return searchableText.includes(searchTerm);
});
if (resetPage) {
currentPage = 1;
}
displayHistoryData();
updateStatistics(filteredData);
}
// Setup pagination controls
function setupPagination() {
// Entries per page
const entriesSelect = document.getElementById('entriesPerPage');
if (entriesSelect) {
entriesSelect.addEventListener('change', function () {
itemsPerPage = parseInt(this.value, 10);
currentPage = 1;
displayHistoryData();
});
}
// Previous page
const prevBtn = document.getElementById('prevPage');
if (prevBtn) {
prevBtn.addEventListener('click', () => {
if (currentPage > 1) {
currentPage--;
displayHistoryData();
}
});
}
// Next page
const nextBtn = document.getElementById('nextPage');
if (nextBtn) {
nextBtn.addEventListener('click', () => {
const totalPages = Math.ceil(filteredData.length / itemsPerPage);
if (currentPage < totalPages) {
currentPage++;
displayHistoryData();
}
});
}
}
// Update pagination info text
function updatePaginationInfo(start, end, total) {
const info = document.getElementById('paginationInfo');
if (!info) {
return;
}
if (total === 0) {
info.textContent = 'Showing 0 to 0 of 0 entries';
} else {
info.textContent = `Showing ${start} to ${end} of ${total} entries`;
}
}
// Update pagination buttons state
function updatePaginationButtons() {
const totalPages = Math.ceil(filteredData.length / itemsPerPage);
// Disable/enable buttons
const prevBtn = document.getElementById('prevPage');
const nextBtn = document.getElementById('nextPage');
if (prevBtn) {
prevBtn.disabled = currentPage === 1;
}
if (nextBtn) {
nextBtn.disabled = currentPage === totalPages || totalPages === 0;
}
// Update page numbers
updatePageNumbers(totalPages);
}
// Update page number buttons
function updatePageNumbers(totalPages) {
const pageNumbersContainer = document.getElementById('pageNumbers');
if (!pageNumbersContainer) {
return;
}
pageNumbersContainer.innerHTML = '';
if (totalPages === 0) return;
// Show max 3 page numbers
let startPage = Math.max(1, currentPage - 1);
let endPage = Math.min(totalPages, startPage + 2);
// Adjust if we're near the end
if (endPage - startPage < 2) {
startPage = Math.max(1, endPage - 2);
}
for (let i = startPage; i <= endPage; i++) {
const pageBtn = document.createElement('button');
pageBtn.className = 'btn-page-number';
pageBtn.textContent = i;
if (i === currentPage) {
pageBtn.classList.add('active');
}
pageBtn.addEventListener('click', () => {
currentPage = i;
displayHistoryData();
});
pageNumbersContainer.appendChild(pageBtn);
}
// Add next indicator if there are more pages
if (endPage < totalPages) {
const dots = document.createElement('span');
dots.className = 'page-dots';
dots.textContent = '>';
pageNumbersContainer.appendChild(dots);
}
}

198
assets/js/kelola-user.js Normal file
View File

@ -0,0 +1,198 @@
import { initializeApp } from "https://www.gstatic.com/firebasejs/12.7.0/firebase-app.js";
import { getAnalytics } from "https://www.gstatic.com/firebasejs/12.7.0/firebase-analytics.js";
import { getAuth, createUserWithEmailAndPassword, signInWithEmailAndPassword, signOut, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/12.7.0/firebase-auth.js";
import { getFirestore, collection, getDocs, setDoc, doc, deleteDoc } from "https://www.gstatic.com/firebasejs/12.7.0/firebase-firestore.js";
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "AIzaSyCQWvoDxDyVCuLEDiwammjUIVYxVARzJig",
authDomain: "project-ta-951b4.firebaseapp.com",
projectId: "project-ta-951b4",
storageBucket: "project-ta-951b4.firebasestorage.app",
messagingSenderId: "217854138058",
appId: "1:217854138058:web:50a5bcd5a61ac1820c4633",
measurementId: "G-6ML8QQEGNZ"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);
// Notification function
function showNotification(message, type = 'success') {
const notification = document.getElementById('notification');
const messageEl = notification.querySelector('.notification-message');
// Remove previous classes
notification.classList.remove('success', 'error', 'warning', 'hidden');
// Add new class and message
notification.classList.add(type);
messageEl.textContent = message;
// Auto hide after 4 seconds
setTimeout(() => {
notification.classList.add('hidden');
}, 4000);
}
// Check authentication
onAuthStateChanged(auth, (user) => {
if (user) {
document.getElementById('dashboardPage').hidden = false;
document.getElementById('loginMessage').hidden = true;
document.getElementById('userEmail').textContent = user.email;
loadUsers();
} else {
document.getElementById('dashboardPage').hidden = true;
document.getElementById('loginMessage').hidden = false;
setTimeout(() => {
window.location.href = '../index.html';
}, 2000);
}
});
// Sign out function
document.getElementById('signOutBtn').addEventListener('click', () => {
signOut(auth).then(() => {
console.log('User signed out');
window.location.href = '../index.html';
}).catch((error) => {
console.error(error);
});
});
// Add account function
document.getElementById('addAccountBtn').addEventListener('click', async () => {
const email = document.getElementById('newAccountEmail').value;
const password = document.getElementById('newAccountPassword').value;
if (!email || !password) {
showNotification('Email dan password harus diisi!', 'warning');
return;
}
if (password.length < 6) {
showNotification('Password harus minimal 6 karakter!', 'warning');
return;
}
// Disable button to prevent multiple clicks
const addBtn = document.getElementById('addAccountBtn');
addBtn.disabled = true;
addBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Memproses...';
try {
// Simpan kredensial admin saat ini
const currentUser = auth.currentUser;
const adminEmail = currentUser.email;
// Buat instance auth kedua untuk user baru
const secondaryApp = initializeApp(firebaseConfig, 'Secondary');
const secondaryAuth = getAuth(secondaryApp);
// Buat akun baru menggunakan auth instance kedua
const userCredential = await createUserWithEmailAndPassword(secondaryAuth, email, password);
const newUser = userCredential.user;
console.log('Akun baru berhasil dibuat:', newUser);
// Simpan data user ke Firestore
await setDoc(doc(db, 'users', newUser.uid), {
email: email,
uid: newUser.uid,
createdAt: new Date()
});
// Logout dari auth instance kedua
await signOut(secondaryAuth);
// Hapus secondary app
await secondaryApp.delete();
// Clear form
document.getElementById('newAccountEmail').value = '';
document.getElementById('newAccountPassword').value = '';
// Show success notification
showNotification(`Akun ${email} berhasil ditambahkan!`, 'success');
// Reload user list
loadUsers();
} catch (error) {
console.error('Error adding account:', error);
let errorMessage = 'Terjadi kesalahan saat menambah akun';
// Handle specific error codes
if (error.code === 'auth/email-already-in-use') {
errorMessage = 'Email sudah digunakan!';
} else if (error.code === 'auth/invalid-email') {
errorMessage = 'Format email tidak valid!';
} else if (error.code === 'auth/weak-password') {
errorMessage = 'Password terlalu lemah!';
} else {
errorMessage = error.message;
}
showNotification(errorMessage, 'error');
} finally {
// Re-enable button
addBtn.disabled = false;
addBtn.innerHTML = '<i class="fas fa-plus-circle"></i> Tambah User';
}
});
// Load users from Firestore
async function loadUsers() {
const userTableBody = document.getElementById('userTableBody');
userTableBody.innerHTML = '<tr><td colspan="4" class="text-center">Loading...</td></tr>';
try {
const querySnapshot = await getDocs(collection(db, 'users'));
userTableBody.innerHTML = '';
if (querySnapshot.empty) {
userTableBody.innerHTML = '<tr><td colspan="4" class="text-center">Belum ada user</td></tr>';
return;
}
let no = 1;
querySnapshot.forEach((docSnapshot) => {
const user = docSnapshot.data();
const row = document.createElement('tr');
const createdDate = user.createdAt ? new Date(user.createdAt.seconds * 1000).toLocaleDateString('id-ID') : 'N/A';
row.innerHTML = `
<td>${no++}</td>
<td>${user.email}</td>
<td>${createdDate}</td>
<td>
<button class="btn-delete" onclick="deleteUser('${user.uid}', '${user.email}')">
<i class="fas fa-trash"></i> Hapus
</button>
</td>
`;
userTableBody.appendChild(row);
});
} catch (error) {
console.error('Error loading users:', error);
userTableBody.innerHTML = '<tr><td colspan="4" class="text-center">Error loading users</td></tr>';
}
}
// Delete user function
window.deleteUser = async function(userId, email) {
if (confirm(`Apakah Anda yakin ingin menghapus user ${email}?`)) {
try {
await deleteDoc(doc(db, 'users', userId));
alert('User berhasil dihapus dari database!');
loadUsers();
} catch (error) {
console.error('Error deleting user:', error);
alert('Error: ' + error.message);
}
}
}

61
assets/js/login.js Normal file
View File

@ -0,0 +1,61 @@
import { initializeApp } from "https://www.gstatic.com/firebasejs/12.7.0/firebase-app.js";
import { getAnalytics } from "https://www.gstatic.com/firebasejs/12.7.0/firebase-analytics.js";
import { getAuth, signInWithEmailAndPassword, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/12.7.0/firebase-auth.js";
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "AIzaSyCQWvoDxDyVCuLEDiwammjUIVYxVARzJig",
authDomain: "project-ta-951b4.firebaseapp.com",
projectId: "project-ta-951b4",
storageBucket: "project-ta-951b4.firebasestorage.app",
messagingSenderId: "217854138058",
appId: "1:217854138058:web:50a5bcd5a61ac1820c4633",
measurementId: "G-6ML8QQEGNZ"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
// Check if already logged in
onAuthStateChanged(auth, (user) => {
if (user) {
// Redirect to dashboard if already logged in
window.location.href = '../dashboard/';
}
});
// Sign in function
document.getElementById('signInBtn').addEventListener('click', () => {
const email = document.getElementById('emailInput').value;
const password = document.getElementById('passwordInput').value;
if (!email || !password) {
alert('Email dan password harus diisi!');
return;
}
signInWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
console.log('User signed in:', userCredential.user);
// Redirect to dashboard
window.location.href = '../dashboard/';
})
.catch((error) => {
console.error('Sign in error:', error.message);
alert('Login gagal: ' + error.message);
});
});
// Allow Enter key to submit
document.getElementById('passwordInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
document.getElementById('signInBtn').click();
}
});
document.getElementById('emailInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
document.getElementById('signInBtn').click();
}
});

224
assets/js/main.js Normal file
View File

@ -0,0 +1,224 @@
(function() {
"use strict";
/**
* Apply .scrolled class to the body as the page is scrolled down
*/
function toggleScrolled() {
const selectBody = document.querySelector('body');
const selectHeader = document.querySelector('#header');
if (!selectHeader.classList.contains('scroll-up-sticky') && !selectHeader.classList.contains('sticky-top') && !selectHeader.classList.contains('fixed-top')) return;
window.scrollY > 100 ? selectBody.classList.add('scrolled') : selectBody.classList.remove('scrolled');
}
document.addEventListener('scroll', toggleScrolled);
window.addEventListener('load', toggleScrolled);
/**
* Mobile nav toggle
*/
const mobileNavToggleBtn = document.querySelector('.mobile-nav-toggle');
function mobileNavToogle() {
document.querySelector('body').classList.toggle('mobile-nav-active');
mobileNavToggleBtn.classList.toggle('bi-list');
mobileNavToggleBtn.classList.toggle('bi-x');
}
if (mobileNavToggleBtn) {
mobileNavToggleBtn.addEventListener('click', mobileNavToogle);
}
/**
* Hide mobile nav on same-page/hash links
*/
document.querySelectorAll('#navmenu a').forEach(navmenu => {
navmenu.addEventListener('click', () => {
if (document.querySelector('.mobile-nav-active')) {
mobileNavToogle();
}
});
});
/**
* Toggle mobile nav dropdowns
*/
document.querySelectorAll('.navmenu .toggle-dropdown').forEach(navmenu => {
navmenu.addEventListener('click', function(e) {
e.preventDefault();
this.parentNode.classList.toggle('active');
this.parentNode.nextElementSibling.classList.toggle('dropdown-active');
e.stopImmediatePropagation();
});
});
/**
* Preloader
*/
const preloader = document.querySelector('#preloader');
if (preloader) {
window.addEventListener('load', () => {
preloader.remove();
});
}
/**
* Scroll top button
*/
let scrollTop = document.querySelector('.scroll-top');
function toggleScrollTop() {
if (scrollTop) {
window.scrollY > 100 ? scrollTop.classList.add('active') : scrollTop.classList.remove('active');
}
}
scrollTop.addEventListener('click', (e) => {
e.preventDefault();
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
window.addEventListener('load', toggleScrollTop);
document.addEventListener('scroll', toggleScrollTop);
/**
* Animation on scroll function and init
*/
function aosInit() {
AOS.init({
duration: 600,
easing: 'ease-in-out',
once: true,
mirror: false
});
}
window.addEventListener('load', aosInit);
/**
* Initiate glightbox
*/
const glightbox = GLightbox({
selector: '.glightbox'
});
/**
* Init swiper sliders
*/
function initSwiper() {
document.querySelectorAll(".init-swiper").forEach(function(swiperElement) {
let config = JSON.parse(
swiperElement.querySelector(".swiper-config").innerHTML.trim()
);
if (swiperElement.classList.contains("swiper-tab")) {
initSwiperWithCustomPagination(swiperElement, config);
} else {
new Swiper(swiperElement, config);
}
});
}
window.addEventListener("load", initSwiper);
/**
* Frequently Asked Questions Toggle
*/
document.querySelectorAll('.faq-item h3, .faq-item .faq-toggle').forEach((faqItem) => {
faqItem.addEventListener('click', () => {
faqItem.parentNode.classList.toggle('faq-active');
});
});
/**
* Animate the skills items on reveal
*/
let skillsAnimation = document.querySelectorAll('.skills-animation');
skillsAnimation.forEach((item) => {
new Waypoint({
element: item,
offset: '80%',
handler: function(direction) {
let progress = item.querySelectorAll('.progress .progress-bar');
progress.forEach(el => {
el.style.width = el.getAttribute('aria-valuenow') + '%';
});
}
});
});
/**
* Init isotope layout and filters
*/
document.querySelectorAll('.isotope-layout').forEach(function(isotopeItem) {
let layout = isotopeItem.getAttribute('data-layout') ?? 'masonry';
let filter = isotopeItem.getAttribute('data-default-filter') ?? '*';
let sort = isotopeItem.getAttribute('data-sort') ?? 'original-order';
let initIsotope;
imagesLoaded(isotopeItem.querySelector('.isotope-container'), function() {
initIsotope = new Isotope(isotopeItem.querySelector('.isotope-container'), {
itemSelector: '.isotope-item',
layoutMode: layout,
filter: filter,
sortBy: sort
});
});
isotopeItem.querySelectorAll('.isotope-filters li').forEach(function(filters) {
filters.addEventListener('click', function() {
isotopeItem.querySelector('.isotope-filters .filter-active').classList.remove('filter-active');
this.classList.add('filter-active');
initIsotope.arrange({
filter: this.getAttribute('data-filter')
});
if (typeof aosInit === 'function') {
aosInit();
}
}, false);
});
});
/**
* Correct scrolling position upon page load for URLs containing hash links.
*/
window.addEventListener('load', function(e) {
if (window.location.hash) {
if (document.querySelector(window.location.hash)) {
setTimeout(() => {
let section = document.querySelector(window.location.hash);
let scrollMarginTop = getComputedStyle(section).scrollMarginTop;
window.scrollTo({
top: section.offsetTop - parseInt(scrollMarginTop),
behavior: 'smooth'
});
}, 100);
}
}
});
/**
* Navmenu Scrollspy
*/
let navmenulinks = document.querySelectorAll('.navmenu a');
function navmenuScrollspy() {
navmenulinks.forEach(navmenulink => {
if (!navmenulink.hash) return;
let section = document.querySelector(navmenulink.hash);
if (!section) return;
let position = window.scrollY + 200;
if (position >= section.offsetTop && position <= (section.offsetTop + section.offsetHeight)) {
document.querySelectorAll('.navmenu a.active').forEach(link => link.classList.remove('active'));
navmenulink.classList.add('active');
} else {
navmenulink.classList.remove('active');
}
})
}
window.addEventListener('load', navmenuScrollspy);
document.addEventListener('scroll', navmenuScrollspy);
})();

673
assets/js/mode-otomatis.js Normal file
View File

@ -0,0 +1,673 @@
// Import Firebase
import { initializeApp } from 'https://www.gstatic.com/firebasejs/12.7.0/firebase-app.js';
import { getAuth, onAuthStateChanged, signOut } from 'https://www.gstatic.com/firebasejs/12.7.0/firebase-auth.js';
import { getDatabase, ref, onValue, update } from 'https://www.gstatic.com/firebasejs/12.7.0/firebase-database.js';
// Firebase configuration
const firebaseConfig = {
apiKey: 'AIzaSyCQWvoDxDyVCuLEDiwammjUIVYxVARzJig',
authDomain: 'project-ta-951b4.firebaseapp.com',
databaseURL: 'https://project-ta-951b4-default-rtdb.firebaseio.com',
projectId: 'project-ta-951b4',
storageBucket: 'project-ta-951b4.firebasestorage.app',
messagingSenderId: '217854138058',
appId: '1:217854138058:web:50a5bcd5a61ac1820c4633',
measurementId: 'G-6ML8QQEGNZ'
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const database = getDatabase(app);
const KONTROL_BASE_PATH = 'kontrol_1';
const kontrolRef = ref(database, KONTROL_BASE_PATH);
let activeThresholdKey = 'threshold_1';
let thresholdProfiles = ['threshold_1'];
let lastKontrolData = {};
let mainModeActive = false;
let isPageLoaded = false;
function clampNumber(value, min, max) {
const parsed = Number(value);
if (!Number.isFinite(parsed)) {
return min;
}
return Math.min(Math.max(parsed, min), max);
}
function normalizePotValue(value) {
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'number') {
return value !== 0;
}
if (typeof value === 'string') {
const normalized = value.trim().toLowerCase();
if (['1', 'true', 'on', 'aktif', 'yes', 'y'].includes(normalized)) {
return true;
}
if (['0', 'false', 'off', 'nonaktif', 'no', 'n', ''].includes(normalized)) {
return false;
}
const numeric = Number(normalized);
if (!Number.isNaN(numeric)) {
return numeric !== 0;
}
}
return false;
}
function escapeHtml(value) {
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function getThresholdNumber(thresholdKey) {
const match = String(thresholdKey || '').match(/^threshold_(\d+)$/i);
return match ? parseInt(match[1], 10) : 1;
}
function getThresholdLabel(thresholdKey) {
return `Threshold ${getThresholdNumber(thresholdKey)}`;
}
function extractThresholdKeys(data) {
const keys = Object.keys(data || {})
.filter((key) => /^threshold_\d+$/i.test(key))
.sort((a, b) => getThresholdNumber(a) - getThresholdNumber(b));
return keys.length > 0 ? keys : ['threshold_1'];
}
function resolveThresholdKey(value) {
if (!value) {
return thresholdProfiles[0] || 'threshold_1';
}
return thresholdProfiles.includes(value) ? value : (thresholdProfiles[0] || 'threshold_1');
}
function normalizePotAktif(potAktifSource = {}) {
if (Array.isArray(potAktifSource)) {
const normalized = { pot_1: false, pot_2: false, pot_3: false, pot_4: false, pot_5: false };
potAktifSource.forEach((value) => {
const potNumber = Number(value);
if (Number.isFinite(potNumber) && potNumber >= 1 && potNumber <= 5) {
normalized[`pot_${potNumber}`] = true;
}
});
return normalized;
}
if (potAktifSource && typeof potAktifSource === 'object') {
const numericKeys = Object.keys(potAktifSource).filter((key) => /^\d+$/.test(key));
if (numericKeys.length > 0) {
const normalized = { pot_1: false, pot_2: false, pot_3: false, pot_4: false, pot_5: false };
Object.values(potAktifSource).forEach((value) => {
const potNumber = Number(value);
if (Number.isFinite(potNumber) && potNumber >= 1 && potNumber <= 5) {
normalized[`pot_${potNumber}`] = true;
}
});
return normalized;
}
}
return {
pot_1: normalizePotValue(potAktifSource.pot_1 ?? potAktifSource.pot1),
pot_2: normalizePotValue(potAktifSource.pot_2 ?? potAktifSource.pot2),
pot_3: normalizePotValue(potAktifSource.pot_3 ?? potAktifSource.pot3),
pot_4: normalizePotValue(potAktifSource.pot_4 ?? potAktifSource.pot4),
pot_5: normalizePotValue(potAktifSource.pot_5 ?? potAktifSource.pot5)
};
}
function getThresholdDataByKey(data, thresholdKey) {
const source = data?.[thresholdKey] || {};
const batasBawah = Number(source.batas_bawah);
const batasAtas = Number(source.batas_atas);
const namaTanaman = String(source.nama_tanaman || '').trim();
return {
nama_tanaman: namaTanaman || getThresholdLabel(thresholdKey),
batas_bawah: Number.isFinite(batasBawah) ? batasBawah : 30,
batas_atas: Number.isFinite(batasAtas) ? batasAtas : 70,
pot_aktif: normalizePotAktif(source.pot_aktif),
pompa_air: source.pompa_air === true,
pompa_pupuk: source.pompa_pupuk === true,
pompa_pengaduk: source.pompa_pengaduk === true,
aktif: source.aktif === true,
smart_mode: source.smart_mode === true
};
}
function getActiveThresholdData(data) {
return getThresholdDataByKey(data, activeThresholdKey);
}
function getNextThresholdKey() {
const indices = thresholdProfiles
.map(getThresholdNumber)
.filter((value) => Number.isFinite(value));
const nextIndex = indices.length > 0 ? Math.max(...indices) + 1 : 1;
return `threshold_${nextIndex}`;
}
function getPumpType(thresholdData) {
if (thresholdData.pompa_pengaduk) {
return 'pengaduk';
}
if (thresholdData.pompa_pupuk) {
return 'nutrisi';
}
return 'air';
}
function getPumpLabelByType(type) {
switch (type) {
case 'nutrisi':
return 'Pompa Nutrisi';
case 'pengaduk':
return 'Pengaduk';
default:
return 'Pompa Air';
}
}
function getPumpIconByType(type) {
switch (type) {
case 'nutrisi':
return 'fas fa-flask';
case 'pengaduk':
return 'fas fa-blender';
default:
return 'fas fa-water';
}
}
function getPotSummary(thresholdData) {
const selectedPots = [];
for (let i = 1; i <= 5; i++) {
if (thresholdData.pot_aktif[`pot_${i}`] === true) {
selectedPots.push(`Pot ${i}`);
}
}
return selectedPots.length > 0 ? selectedPots.join(', ') : 'Belum ada pot dipilih';
}
function setMainModeVisual(isActive) {
const mainToggle = document.getElementById('mainToggle');
const statusIndicator = document.getElementById('statusIndicator');
const statusText = document.getElementById('statusText');
const addThresholdBtn = document.getElementById('btnAddThreshold');
const saveThresholdBtn = document.getElementById('btnSaveThreshold');
if (mainToggle) {
mainToggle.classList.toggle('active', isActive);
}
if (statusIndicator) {
statusIndicator.classList.toggle('active', isActive);
}
if (statusText) {
statusText.textContent = isActive ? 'Aktif' : 'Tidak Aktif';
}
if (addThresholdBtn) {
addThresholdBtn.disabled = !isActive;
}
if (saveThresholdBtn) {
saveThresholdBtn.disabled = !isActive;
}
}
function updateThresholdProfileUI() {
const activeLabel = document.getElementById('activeThresholdLabel');
const container = document.getElementById('thresholdProfileContainer');
if (activeLabel) {
const activeData = getThresholdDataByKey(lastKontrolData, activeThresholdKey);
activeLabel.textContent = `Profil aktif: ${getThresholdLabel(activeThresholdKey)} - ${activeData.nama_tanaman}`;
}
if (!container) {
return;
}
container.innerHTML = thresholdProfiles.map((thresholdKey) => {
const thresholdData = getThresholdDataByKey(lastKontrolData, thresholdKey);
const isActive = thresholdKey === activeThresholdKey;
const pumpType = getPumpType(thresholdData);
const deleteDisabled = thresholdProfiles.length <= 1 || !mainModeActive;
const safePlantName = escapeHtml(thresholdData.nama_tanaman);
return `
<div class="threshold-profile-item ${isActive ? 'active' : ''}">
<div class="threshold-profile-item-main">
<div class="threshold-profile-item-title-row">
<div class="threshold-profile-item-title">${getThresholdLabel(thresholdKey)}</div>
${isActive ? '<span class="threshold-active-badge">Aktif</span>' : ''}
</div>
<div class="threshold-profile-item-meta"><i class="fas fa-tag"></i> Nama: ${safePlantName}</div>
<div class="threshold-profile-item-meta"><i class="fas fa-sliders-h"></i> Batas: ${thresholdData.batas_bawah}% - ${thresholdData.batas_atas}%</div>
<div class="threshold-profile-item-meta"><i class="fas fa-seedling"></i> Pot: ${getPotSummary(thresholdData)}</div>
<div class="threshold-profile-item-meta"><i class="${getPumpIconByType(pumpType)}"></i> Penyiraman: ${getPumpLabelByType(pumpType)}</div>
</div>
<div class="threshold-profile-item-actions">
<button type="button" class="threshold-action-btn btn-activate-threshold ${isActive ? 'active' : ''}" data-threshold-action="select" data-threshold-key="${thresholdKey}" ${!mainModeActive ? 'disabled' : ''}>${isActive ? 'Aktif' : 'Pilih'}</button>
<button type="button" class="threshold-action-btn btn-edit-threshold" data-threshold-action="edit" data-threshold-key="${thresholdKey}" ${!mainModeActive ? 'disabled' : ''}>Edit</button>
<button type="button" class="threshold-action-btn btn-delete-threshold" data-threshold-action="delete" data-threshold-key="${thresholdKey}" ${deleteDisabled ? 'disabled' : ''}>Hapus</button>
</div>
</div>
`;
}).join('');
}
async function switchThresholdProfile(selectedKey) {
if (!mainModeActive) {
showNotification('Aktifkan Mode Otomatis terlebih dahulu', 'warning');
return;
}
const thresholdKey = resolveThresholdKey(selectedKey);
if (thresholdKey === activeThresholdKey) {
return;
}
const previousKey = activeThresholdKey;
activeThresholdKey = thresholdKey;
updateThresholdProfileUI();
try {
await update(kontrolRef, {
threshold_aktif: thresholdKey
});
showNotification(`Berpindah ke ${getThresholdLabel(thresholdKey)}`, 'info');
} catch (error) {
console.error('Error switching threshold profile:', error);
activeThresholdKey = previousKey;
updateThresholdProfileUI();
showNotification('Gagal mengganti profil threshold', 'error');
}
}
function openThresholdModal(thresholdKey = null) {
if (!mainModeActive) {
showNotification('Aktifkan Mode Otomatis terlebih dahulu', 'warning');
return;
}
const modal = document.getElementById('thresholdModal');
const title = document.getElementById('thresholdModalTitle');
const editInput = document.getElementById('editThresholdKey');
const plantNameInput = document.getElementById('thresholdPlantName');
const minInput = document.getElementById('thresholdMinInput');
const maxInput = document.getElementById('thresholdMaxInput');
if (!modal || !title || !editInput || !plantNameInput || !minInput || !maxInput) {
return;
}
const isEditMode = Boolean(thresholdKey);
const sourceData = isEditMode
? getThresholdDataByKey(lastKontrolData, thresholdKey)
: getActiveThresholdData(lastKontrolData);
title.innerHTML = isEditMode
? '<i class="fas fa-edit"></i> Edit Threshold'
: '<i class="fas fa-layer-group"></i> Tambah Threshold';
editInput.value = isEditMode ? thresholdKey : '';
plantNameInput.value = isEditMode ? (sourceData.nama_tanaman || '') : '';
minInput.value = sourceData.batas_bawah;
maxInput.value = sourceData.batas_atas;
const potCheckboxes = document.querySelectorAll('.threshold-pot-checkbox');
potCheckboxes.forEach((checkbox) => {
const potNumber = checkbox.value.replace('pot', '');
checkbox.checked = sourceData.pot_aktif[`pot_${potNumber}`] === true;
});
const pumpType = getPumpType(sourceData);
const pumpRadio = document.querySelector(`input[name="thresholdPumpType"][value="${pumpType}"]`);
if (pumpRadio) {
pumpRadio.checked = true;
}
modal.style.display = 'flex';
}
window.closeThresholdModal = function() {
const modal = document.getElementById('thresholdModal');
if (modal) {
modal.style.display = 'none';
}
};
window.selectAllThresholdPotsModal = function() {
document.querySelectorAll('.threshold-pot-checkbox').forEach((checkbox) => {
checkbox.checked = true;
});
};
window.deselectAllThresholdPotsModal = function() {
document.querySelectorAll('.threshold-pot-checkbox').forEach((checkbox) => {
checkbox.checked = false;
});
};
function getThresholdPayloadFromForm() {
const editThresholdKey = document.getElementById('editThresholdKey')?.value || '';
const fallbackKey = editThresholdKey || activeThresholdKey;
const plantNameInput = document.getElementById('thresholdPlantName');
const minInput = document.getElementById('thresholdMinInput');
const maxInput = document.getElementById('thresholdMaxInput');
const namaTanaman = String(plantNameInput?.value || '').trim();
if (!namaTanaman) {
showNotification('Nama tanaman untuk threshold wajib diisi', 'warning');
return null;
}
const batasBawah = clampNumber(minInput?.value, 0, 99);
const batasAtas = clampNumber(maxInput?.value, 1, 100);
if (batasBawah >= batasAtas) {
showNotification('Batas bawah harus lebih kecil dari batas atas', 'warning');
return null;
}
const selectedPotValues = Array.from(document.querySelectorAll('.threshold-pot-checkbox:checked'))
.map((checkbox) => checkbox.value);
if (selectedPotValues.length === 0) {
showNotification('Pilih minimal 1 pot untuk threshold', 'warning');
return null;
}
const selectedPump = document.querySelector('input[name="thresholdPumpType"]:checked')?.value || 'air';
const potAktifList = Array.from(new Set(
selectedPotValues
.map((pot) => Number(String(pot).replace('pot', '')))
.filter((potNumber) => Number.isFinite(potNumber) && potNumber >= 1 && potNumber <= 5)
));
return {
nama_tanaman: namaTanaman || getThresholdLabel(fallbackKey),
aktif: mainModeActive,
smart_mode: mainModeActive,
batas_bawah: batasBawah,
batas_atas: batasAtas,
pot_aktif: potAktifList,
pompa_air: selectedPump === 'air',
pompa_pupuk: selectedPump === 'nutrisi',
pompa_pengaduk: selectedPump === 'pengaduk'
};
}
window.saveThresholdProfile = async function() {
if (!mainModeActive) {
showNotification('Aktifkan Mode Otomatis terlebih dahulu', 'warning');
return;
}
const payload = getThresholdPayloadFromForm();
if (!payload) {
return;
}
const editThresholdKey = document.getElementById('editThresholdKey')?.value || '';
const isEditMode = editThresholdKey !== '';
const targetKey = isEditMode ? resolveThresholdKey(editThresholdKey) : getNextThresholdKey();
const updates = {
[targetKey]: payload
};
if (!isEditMode) {
updates.threshold_aktif = targetKey;
}
try {
await update(kontrolRef, updates);
closeThresholdModal();
showNotification(
isEditMode
? `${getThresholdLabel(targetKey)} berhasil diperbarui`
: `${getThresholdLabel(targetKey)} berhasil ditambahkan`,
'success'
);
} catch (error) {
console.error('Error saving threshold profile:', error);
showNotification('Gagal menyimpan threshold', 'error');
}
};
async function deleteThresholdProfile(thresholdKey) {
if (!mainModeActive) {
showNotification('Aktifkan Mode Otomatis terlebih dahulu', 'warning');
return;
}
if (!thresholdProfiles.includes(thresholdKey)) {
return;
}
if (thresholdProfiles.length <= 1) {
showNotification('Minimal harus ada 1 profil threshold', 'warning');
return;
}
if (!confirm(`Hapus ${getThresholdLabel(thresholdKey)}?`)) {
return;
}
const fallbackKey = thresholdProfiles.find((key) => key !== thresholdKey) || 'threshold_1';
const updates = {
[thresholdKey]: null
};
if (activeThresholdKey === thresholdKey) {
updates.threshold_aktif = fallbackKey;
}
try {
await update(kontrolRef, updates);
showNotification(`${getThresholdLabel(thresholdKey)} berhasil dihapus`, 'success');
} catch (error) {
console.error('Error deleting threshold profile:', error);
showNotification('Gagal menghapus profil threshold', 'error');
}
}
function showNotification(message, type = 'success') {
const notification = document.getElementById('notification');
if (!notification) {
return;
}
notification.className = `notification ${type}`;
const icon = notification.querySelector('.notification-icon');
if (icon) {
if (type === 'success') {
icon.className = 'notification-icon fas fa-check-circle';
} else if (type === 'error') {
icon.className = 'notification-icon fas fa-exclamation-circle';
} else if (type === 'info') {
icon.className = 'notification-icon fas fa-info-circle';
} else if (type === 'warning') {
icon.className = 'notification-icon fas fa-exclamation-triangle';
}
}
const messageEl = notification.querySelector('.notification-message');
if (messageEl) {
messageEl.textContent = message;
}
notification.classList.add('show');
notification.classList.remove('hidden');
setTimeout(() => {
notification.classList.remove('show');
notification.classList.add('hidden');
}, 3000);
}
function loadControllerData() {
onValue(kontrolRef, (snapshot) => {
const data = snapshot.exists() ? snapshot.val() : {};
lastKontrolData = data;
thresholdProfiles = extractThresholdKeys(data);
if (data.threshold_aktif) {
activeThresholdKey = resolveThresholdKey(data.threshold_aktif);
} else {
activeThresholdKey = resolveThresholdKey(activeThresholdKey);
}
mainModeActive = data.otomatis === true || data.otomatis === 1;
setMainModeVisual(mainModeActive);
updateThresholdProfileUI();
});
}
window.toggleMainMode = function() {
if (!isPageLoaded) {
return;
}
const nextState = !mainModeActive;
mainModeActive = nextState;
setMainModeVisual(mainModeActive);
updateThresholdProfileUI();
// Ketika Mode Otomatis diaktifkan, matikan Mode Waktu
// Ketika Mode Otomatis dimatikan, Mode Waktu tetap bisa digunakan
update(kontrolRef, {
otomatis: nextState,
waktu: nextState ? false : undefined,
[`${activeThresholdKey}/aktif`]: nextState,
[`${activeThresholdKey}/smart_mode`]: nextState,
threshold_aktif: activeThresholdKey
}).then(() => {
showNotification(nextState ? 'Mode Otomatis diaktifkan' : 'Mode Otomatis dinonaktifkan', nextState ? 'success' : 'info');
}).catch((error) => {
console.error('Error updating mode status:', error);
mainModeActive = !nextState;
setMainModeVisual(mainModeActive);
updateThresholdProfileUI();
showNotification('Gagal menyimpan status mode', 'error');
});
};
onAuthStateChanged(auth, (user) => {
const loginMessage = document.getElementById('loginMessage');
if (user) {
document.getElementById('userEmail').textContent = user.email;
document.getElementById('dashboardPage').style.display = 'block';
if (loginMessage) {
loginMessage.hidden = true;
}
loadControllerData();
isPageLoaded = true;
} else {
document.getElementById('dashboardPage').style.display = 'none';
if (loginMessage) {
loginMessage.hidden = false;
}
setTimeout(() => {
window.location.href = '../index.html';
}, 2000);
}
});
document.getElementById('signOutBtn')?.addEventListener('click', async () => {
try {
await signOut(auth);
window.location.href = '../index.html';
} catch (error) {
showNotification(`Error logging out: ${error.message}`, 'error');
}
});
window.addEventListener('DOMContentLoaded', () => {
setMainModeVisual(false);
updateThresholdProfileUI();
document.getElementById('btnAddThreshold')?.addEventListener('click', () => {
openThresholdModal();
});
document.getElementById('thresholdProfileContainer')?.addEventListener('click', (event) => {
const actionBtn = event.target.closest('[data-threshold-action]');
if (!actionBtn) {
return;
}
const action = actionBtn.dataset.thresholdAction;
const thresholdKey = actionBtn.dataset.thresholdKey;
if (!thresholdKey) {
return;
}
if (action === 'select') {
switchThresholdProfile(thresholdKey);
return;
}
if (action === 'edit') {
openThresholdModal(thresholdKey);
return;
}
if (action === 'delete') {
deleteThresholdProfile(thresholdKey);
}
});
document.getElementById('thresholdModal')?.addEventListener('click', (event) => {
if (event.target.id === 'thresholdModal') {
closeThresholdModal();
}
});
document.getElementById('thresholdMinInput')?.addEventListener('change', (event) => {
const minInput = event.target;
minInput.value = clampNumber(minInput.value, 0, 99);
});
document.getElementById('thresholdMaxInput')?.addEventListener('change', (event) => {
const maxInput = event.target;
maxInput.value = clampNumber(maxInput.value, 1, 100);
});
document.getElementById('thresholdPlantName')?.addEventListener('change', (event) => {
event.target.value = String(event.target.value || '').trim();
});
});

566
assets/js/mode-waktu.js Normal file
View File

@ -0,0 +1,566 @@
// mode-waktu.js - Mode Waktu Controller with CRUD
import { initializeApp } from "https://www.gstatic.com/firebasejs/12.7.0/firebase-app.js";
import { getAuth, onAuthStateChanged, signOut } from "https://www.gstatic.com/firebasejs/12.7.0/firebase-auth.js";
import { getDatabase, ref, set, onValue, update, remove } from "https://www.gstatic.com/firebasejs/12.7.0/firebase-database.js";
// Firebase configuration
const firebaseConfig = {
apiKey: "AIzaSyCQWvoDxDyVCuLEDiwammjUIVYxVARzJig",
authDomain: "project-ta-951b4.firebaseapp.com",
databaseURL: "https://project-ta-951b4-default-rtdb.firebaseio.com",
projectId: "project-ta-951b4",
storageBucket: "project-ta-951b4.firebasestorage.app",
messagingSenderId: "217854138058",
appId: "1:217854138058:web:50a5bcd5a61ac1820c4633",
measurementId: "G-6ML8QQEGNZ"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const database = getDatabase(app);
let schedules = [];
let isPageLoaded = false;
const MODE_WAKTU_BASE_PATH = 'kontrol_1';
function getSchedulePath(scheduleId) {
return `${MODE_WAKTU_BASE_PATH}/${scheduleId}`;
}
function getNextScheduleId() {
const indices = schedules
.map((item) => {
const match = String(item.id || '').match(/^jadwal_(\d+)$/i);
return match ? parseInt(match[1], 10) : 0;
})
.filter((value) => Number.isFinite(value));
const nextIndex = indices.length > 0 ? Math.max(...indices) + 1 : 1;
return `jadwal_${nextIndex}`;
}
function getSelectedPotsFromFirebase(potAktif) {
const selected = [];
if (Array.isArray(potAktif)) {
potAktif.forEach((value) => {
const potNumber = Number(value);
if (Number.isFinite(potNumber) && potNumber >= 1 && potNumber <= 5) {
selected.push(`pot${potNumber}`);
}
});
return Array.from(new Set(selected));
}
if (potAktif && typeof potAktif === 'object') {
const hasNumericKeys = Object.keys(potAktif).some((key) => /^\d+$/.test(key));
if (hasNumericKeys) {
Object.values(potAktif).forEach((value) => {
const potNumber = Number(value);
if (Number.isFinite(potNumber) && potNumber >= 1 && potNumber <= 5) {
selected.push(`pot${potNumber}`);
}
});
return Array.from(new Set(selected));
}
for (let i = 1; i <= 5; i++) {
if (potAktif[`pot_${i}`] === true || potAktif[`pot${i}`] === true) {
selected.push(`pot${i}`);
}
}
}
return selected;
}
function normalizeScheduleFromMobile(id, data) {
const pots = getSelectedPotsFromFirebase(data?.pot_aktif);
let pumpType = 'air';
if (data?.pompa_pengaduk) {
pumpType = 'pengaduk';
} else if (data?.pompa_pupuk) {
pumpType = 'nutrisi';
} else if (data?.pompa_air) {
pumpType = 'air';
}
return {
id,
name: data?.nama || `Jadwal ${String(id).replace('jadwal_', '')}`,
time: data?.waktu || '',
duration: Number(data?.durasi || 30),
pots,
pumpType,
aktif: data?.aktif === true
};
}
function toMobileScheduleData(scheduleData) {
const selectedPots = Array.isArray(scheduleData.pots) ? scheduleData.pots : [];
const potAktifList = Array.from(new Set(
selectedPots
.map((pot) => Number(String(pot).replace('pot', '')))
.filter((potNumber) => Number.isFinite(potNumber) && potNumber >= 1 && potNumber <= 5)
));
return {
aktif: true,
durasi: Number(scheduleData.duration || 30),
waktu: scheduleData.time || '',
pompa_air: scheduleData.pumpType === 'air',
pompa_pupuk: scheduleData.pumpType === 'nutrisi',
pompa_pengaduk: scheduleData.pumpType === 'pengaduk',
pot_aktif: potAktifList
};
}
// Notification function
function showNotification(message, type = 'success') {
const notification = document.getElementById('notification');
if (!notification) {
const notif = document.createElement('div');
notif.id = 'notification';
notif.className = `notification ${type}`;
notif.textContent = message;
document.body.appendChild(notif);
setTimeout(() => {
notif.classList.add('show');
}, 10);
setTimeout(() => {
notif.classList.remove('show');
setTimeout(() => notif.remove(), 300);
}, 3000);
} else {
notification.className = `notification ${type}`;
notification.textContent = message;
notification.classList.add('show');
setTimeout(() => {
notification.classList.remove('show');
}, 3000);
}
}
// Authentication check
onAuthStateChanged(auth, (user) => {
if (user) {
document.getElementById('userEmail').textContent = user.email;
document.getElementById('dashboardPage').style.display = 'block';
// document.getElementById('loginMessage').hidden = true;
initializeMode();
} else {
document.getElementById('dashboardPage').style.display = 'none';
// document.getElementById('loginMessage').hidden = false;
setTimeout(() => {
window.location.href = '../index.html';
}, 2000);
}
});
// Logout handler
document.getElementById('signOutBtn')?.addEventListener('click', async () => {
try {
await signOut(auth);
window.location.href = '../index.html';
} catch (error) {
showNotification('Error logging out: ' + error.message, 'error');
}
});
// Normalize boolean value from Firebase (handle string, number, boolean)
function normalizeBooleanValue(value) {
if (typeof value === 'boolean') return value;
if (typeof value === 'number') return value !== 0;
if (typeof value === 'string') {
const lower = value.toLowerCase().trim();
return lower === 'true' || lower === '1' || lower === 'yes';
}
return false;
}
// Update toggle UI based on active state
function updateToggleUI(isActive) {
const mainToggle = document.getElementById('mainToggle');
const statusIndicator = document.getElementById('statusIndicator');
const statusText = document.getElementById('statusText');
const btnAddSchedule = document.getElementById('btnAddSchedule');
if (mainToggle) {
if (isActive) {
mainToggle.classList.add('active');
} else {
mainToggle.classList.remove('active');
}
}
if (statusIndicator) {
if (isActive) {
statusIndicator.classList.add('active');
statusText.textContent = 'Aktif';
if (btnAddSchedule) btnAddSchedule.disabled = false;
} else {
statusIndicator.classList.remove('active');
statusText.textContent = 'Tidak Aktif';
if (btnAddSchedule) btnAddSchedule.disabled = true;
}
}
}
// Initialize mode
function initializeMode() {
// Listen to Mode Waktu status (field: waktu)
const statusRef = ref(database, `${MODE_WAKTU_BASE_PATH}/waktu`);
onValue(statusRef, (snapshot) => {
const firebaseValue = snapshot.val();
const isActive = normalizeBooleanValue(firebaseValue);
updateToggleUI(isActive);
});
// Load schedules from Firebase
loadSchedulesFromFirebase();
// Setup add schedule button
setupAddScheduleButton();
isPageLoaded = true;
}
// Load schedules from Firebase
function loadSchedulesFromFirebase() {
const schedulesRef = ref(database, MODE_WAKTU_BASE_PATH);
onValue(schedulesRef, (snapshot) => {
schedules = [];
const data = snapshot.val();
if (data) {
Object.keys(data).forEach((key) => {
if (/^jadwal_\d+$/i.test(key) && data[key] && typeof data[key] === 'object') {
schedules.push(normalizeScheduleFromMobile(key, data[key]));
}
});
schedules.sort((a, b) => {
const aNum = parseInt(String(a.id).replace('jadwal_', ''), 10) || 0;
const bNum = parseInt(String(b.id).replace('jadwal_', ''), 10) || 0;
return aNum - bNum;
});
}
renderSchedules();
});
}
// Render schedules to the UI
function renderSchedules() {
const container = document.getElementById('scheduleContainer');
if (!container) return;
if (schedules.length === 0) {
container.innerHTML = `
<div class="empty-schedule">
<i class="fas fa-calendar-times"></i>
<p>Belum ada jadwal penyiraman</p>
<p class="empty-subtitle">Klik tombol "Tambah Jadwal" untuk membuat jadwal baru</p>
</div>
`;
return;
}
container.innerHTML = schedules.map(schedule => {
const pots = schedule.pots || [];
const potList = pots.map(p => p.replace('pot', 'Pot ')).join(', ');
const pumpIcon = getPumpIcon(schedule.pumpType);
const pumpLabel = getPumpLabel(schedule.pumpType);
return `
<div class="schedule-item">
<div class="schedule-item-header">
<div class="schedule-name">
<i class="fas fa-calendar-check"></i>
<span>${schedule.name || 'Jadwal Penyiraman'}</span>
</div>
<div class="schedule-actions">
<button class="btn-action btn-edit" onclick="editSchedule('${schedule.id}')" title="Edit">
<i class="fas fa-edit"></i>
</button>
<button class="btn-action btn-duplicate" onclick="duplicateSchedule('${schedule.id}')" title="Duplikat">
<i class="fas fa-copy"></i>
</button>
<button class="btn-action btn-delete" onclick="deleteSchedule('${schedule.id}')" title="Hapus">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div class="schedule-item-body">
<div class="schedule-info">
<div class="info-item">
<i class="fas fa-clock"></i>
<span>Waktu: ${schedule.time || '-'}</span>
</div>
<div class="info-item">
<i class="fas fa-hourglass-half"></i>
<span>Durasi: ${schedule.duration || 0} detik</span>
</div>
</div>
<div class="schedule-info">
<div class="info-item">
<i class="fas fa-seedling"></i>
<span>Pot: ${potList || 'Tidak ada'}</span>
</div>
<div class="info-item">
<i class="${pumpIcon}"></i>
<span>${pumpLabel}</span>
</div>
</div>
</div>
</div>
`;
}).join('');
}
// Get pump icon based on type
function getPumpIcon(type) {
switch(type) {
case 'air': return 'fas fa-water';
case 'nutrisi': return 'fas fa-flask';
case 'pengaduk': return 'fas fa-blender';
default: return 'fas fa-question';
}
}
// Get pump label based on type
function getPumpLabel(type) {
switch(type) {
case 'air': return 'Pompa Air';
case 'nutrisi': return 'Pompa Nutrisi';
case 'pengaduk': return 'Pengaduk';
default: return 'Tidak diketahui';
}
}
// Setup add schedule button
function setupAddScheduleButton() {
document.getElementById('btnAddSchedule')?.addEventListener('click', () => {
openScheduleModal();
});
}
// Toggle main mode
window.toggleMainMode = function() {
if (!isPageLoaded) return;
const mainToggle = document.getElementById('mainToggle');
const currentState = mainToggle.classList.contains('active');
const newState = !currentState;
// Optimistic UI update for better UX
updateToggleUI(newState);
// Ketika Mode Waktu diaktifkan, matikan Mode Otomatis
// Ketika Mode Waktu dimatikan, Mode Otomatis tetap bisa digunakan
update(ref(database, MODE_WAKTU_BASE_PATH), {
waktu: newState,
otomatis: newState ? false : undefined
}).then(() => {
showNotification(
newState ? 'Mode Waktu diaktifkan' : 'Mode Waktu dinonaktifkan',
newState ? 'success' : 'warning'
);
}).catch((error) => {
console.error('Error updating mode:', error);
// Revert UI on error
updateToggleUI(currentState);
showNotification('Gagal mengubah status mode', 'error');
});
};
// Open schedule modal for add/edit
window.openScheduleModal = function(scheduleId = null) {
const modal = document.getElementById('scheduleModal');
const modalTitle = document.getElementById('modalTitle');
if (scheduleId) {
// Edit mode
const schedule = schedules.find(s => s.id === scheduleId);
if (schedule) {
modalTitle.innerHTML = '<i class="fas fa-edit"></i> Edit Jadwal';
document.getElementById('editScheduleId').value = scheduleId;
document.getElementById('scheduleName').value = schedule.name || '';
document.getElementById('scheduleTime').value = schedule.time || '';
document.getElementById('scheduleDuration').value = schedule.duration || 30;
// Set pot checkboxes
const checkboxes = document.querySelectorAll('.schedule-pot-checkbox');
checkboxes.forEach(cb => {
cb.checked = schedule.pots && schedule.pots.includes(cb.value);
});
// Set pump type
const pumpRadio = document.querySelector(`input[name="pumpType"][value="${schedule.pumpType}"]`);
if (pumpRadio) pumpRadio.checked = true;
}
} else {
// Add mode
modalTitle.innerHTML = '<i class="fas fa-calendar-plus"></i> Tambah Jadwal';
document.getElementById('editScheduleId').value = '';
document.getElementById('scheduleName').value = '';
document.getElementById('scheduleTime').value = '';
document.getElementById('scheduleDuration').value = 30;
// Uncheck all pots
document.querySelectorAll('.schedule-pot-checkbox').forEach(cb => cb.checked = false);
// Reset pump type default to air when creating new schedule.
const defaultRadio = document.querySelector('input[name="pumpType"][value="air"]');
if (defaultRadio) defaultRadio.checked = true;
}
modal.style.display = 'flex';
};
// Close schedule modal
window.closeScheduleModal = function() {
document.getElementById('scheduleModal').style.display = 'none';
};
// Save schedule (add or edit)
window.saveSchedule = function() {
const scheduleId = document.getElementById('editScheduleId').value;
const name = document.getElementById('scheduleName').value.trim();
const time = document.getElementById('scheduleTime').value;
const duration = parseInt(document.getElementById('scheduleDuration').value);
// Get selected pots
const pots = Array.from(document.querySelectorAll('.schedule-pot-checkbox:checked'))
.map(cb => cb.value);
// Get pump type
const pumpType = document.querySelector('input[name="pumpType"]:checked')?.value || 'air';
// Validation
if (!name) {
alert('Nama jadwal harus diisi!');
return;
}
if (!time) {
alert('Waktu mulai harus diisi!');
return;
}
if (pots.length === 0) {
alert('Pilih minimal 1 pot!');
return;
}
const scheduleData = {
name,
time,
duration,
pots,
pumpType
};
const mobileScheduleData = toMobileScheduleData(scheduleData);
if (scheduleId) {
// Update existing schedule
const scheduleRef = ref(database, getSchedulePath(scheduleId));
update(scheduleRef, mobileScheduleData)
.then(() => {
closeScheduleModal();
showNotification('Jadwal berhasil diupdate!', 'success');
})
.catch(error => {
console.error('Error updating schedule:', error);
alert('Gagal mengupdate jadwal!');
});
} else {
// Add new schedule
const newScheduleId = getNextScheduleId();
const newScheduleRef = ref(database, getSchedulePath(newScheduleId));
set(newScheduleRef, mobileScheduleData)
.then(() => {
closeScheduleModal();
showNotification('Jadwal berhasil ditambahkan!', 'success');
})
.catch(error => {
console.error('Error adding schedule:', error);
alert('Gagal menambahkan jadwal!');
});
}
};
// Edit schedule
window.editSchedule = function(scheduleId) {
openScheduleModal(scheduleId);
};
// Duplicate schedule
window.duplicateSchedule = function(scheduleId) {
const schedule = schedules.find(s => s.id === scheduleId);
if (!schedule) return;
const newSchedule = {
name: `${schedule.name} (Copy)`,
time: schedule.time,
duration: schedule.duration,
pots: schedule.pots,
pumpType: schedule.pumpType
};
const newScheduleId = getNextScheduleId();
const newScheduleRef = ref(database, getSchedulePath(newScheduleId));
set(newScheduleRef, toMobileScheduleData(newSchedule))
.then(() => {
showNotification('Jadwal berhasil diduplikat!', 'success');
})
.catch(error => {
console.error('Error duplicating schedule:', error);
alert('Gagal menduplikat jadwal!');
});
};
// Delete schedule
window.deleteSchedule = function(scheduleId) {
if (!confirm('Apakah Anda yakin ingin menghapus jadwal ini?')) return;
const scheduleRef = ref(database, getSchedulePath(scheduleId));
remove(scheduleRef)
.then(() => {
showNotification('Jadwal berhasil dihapus!', 'success');
})
.catch(error => {
console.error('Error deleting schedule:', error);
alert('Gagal menghapus jadwal!');
});
};
// Select all pots in modal
window.selectAllPotsModal = function() {
document.querySelectorAll('.schedule-pot-checkbox').forEach(cb => cb.checked = true);
};
// Deselect all pots in modal
window.deselectAllPotsModal = function() {
document.querySelectorAll('.schedule-pot-checkbox').forEach(cb => cb.checked = false);
};
// Close modal when clicking outside
window.onclick = function(event) {
const modal = document.getElementById('scheduleModal');
if (event.target === modal) {
closeScheduleModal();
}
};

133
assets/js/sidebar.js Normal file
View File

@ -0,0 +1,133 @@
// Sidebar Toggle Functions
function setHamburgerState(isOpen) {
const mobileBtn = document.querySelector('.mobile-menu-btn');
if (!mobileBtn) return;
mobileBtn.classList.toggle('is-open', isOpen);
mobileBtn.setAttribute('aria-expanded', String(isOpen));
mobileBtn.setAttribute('aria-label', isOpen ? 'Tutup menu navigasi' : 'Buka menu navigasi');
const icon = mobileBtn.querySelector('i');
if (!icon) return;
if (isOpen) {
icon.classList.remove('fa-bars');
icon.classList.add('fa-xmark');
} else {
icon.classList.remove('fa-xmark');
icon.classList.add('fa-bars');
}
}
function closeMobileSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebarOverlay');
if (sidebar) sidebar.classList.remove('mobile-open');
if (overlay) overlay.classList.remove('active');
document.body.classList.remove('sidebar-open');
setHamburgerState(false);
}
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebarOverlay');
if (!sidebar || !overlay) return;
const willOpen = !sidebar.classList.contains('mobile-open');
sidebar.classList.toggle('mobile-open', willOpen);
overlay.classList.toggle('active', willOpen);
document.body.classList.toggle('sidebar-open', willOpen);
setHamburgerState(willOpen);
}
function toggleSidebarCollapse() {
if (window.innerWidth <= 768) return;
const sidebar = document.getElementById('sidebar');
if (!sidebar) return;
sidebar.classList.toggle('collapsed');
const icon = document.querySelector('.sidebar-toggle i');
if (!icon) return;
if (sidebar.classList.contains('collapsed')) {
icon.classList.remove('fa-chevron-left');
icon.classList.add('fa-chevron-right');
} else {
icon.classList.remove('fa-chevron-right');
icon.classList.add('fa-chevron-left');
}
}
// Update current time
function updateTime() {
const timeElement = document.getElementById('currentTime');
if (!timeElement) return;
const now = new Date();
let options;
if (window.innerWidth <= 768) {
options = {
day: '2-digit',
month: 'short',
hour: '2-digit',
minute: '2-digit',
hour12: false
};
} else {
options = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
};
}
timeElement.textContent = now.toLocaleString('id-ID', options);
}
function handleResize() {
if (window.innerWidth > 768) {
closeMobileSidebar();
}
updateTime();
}
// Initialize sidebar functionality
function initializeSidebar() {
updateTime();
setInterval(updateTime, 60000);
handleResize();
setHamburgerState(false);
document.querySelectorAll('.sidebar .menu-item').forEach((menuItem) => {
menuItem.addEventListener('click', () => {
if (window.innerWidth <= 768) {
closeMobileSidebar();
}
});
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
closeMobileSidebar();
}
});
window.addEventListener('resize', handleResize);
}
// Auto-initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeSidebar);
} else {
initializeSidebar();
}

63
assets/js/theme-toggle.js Normal file
View File

@ -0,0 +1,63 @@
// ==========================================
// THEME TOGGLE FUNCTIONALITY
// Light Mode / Dark Mode with localStorage
// ==========================================
// Get saved theme from localStorage or default to 'light-mode'
function getSavedTheme() {
const savedTheme = localStorage.getItem('smartpot-theme');
return savedTheme || 'light-mode';
}
// Apply theme to body
function applyTheme(theme) {
document.body.className = theme;
// Update icon
const themeIcon = document.getElementById('themeIcon');
if (themeIcon) {
if (theme === 'dark-mode') {
themeIcon.className = 'fas fa-moon';
} else {
themeIcon.className = 'fas fa-sun';
}
}
// Save to localStorage
localStorage.setItem('smartpot-theme', theme);
// Log for debugging
console.log('Theme applied:', theme);
}
// Toggle between light and dark mode
function toggleTheme() {
const currentTheme = document.body.className || 'light-mode';
const newTheme = currentTheme === 'light-mode' ? 'dark-mode' : 'light-mode';
// Add animation class to icon
const themeToggleBtn = document.getElementById('themeToggleBtn');
if (themeToggleBtn) {
themeToggleBtn.classList.add('theme-toggle-animate');
setTimeout(() => {
themeToggleBtn.classList.remove('theme-toggle-animate');
}, 600);
}
// Apply new theme
applyTheme(newTheme);
}
// Initialize theme on page load
function initTheme() {
const savedTheme = getSavedTheme();
applyTheme(savedTheme);
console.log('Theme initialized:', savedTheme);
}
// Run on DOM content loaded
document.addEventListener('DOMContentLoaded', initTheme);
// Export functions for use in HTML
window.toggleTheme = toggleTheme;
window.initTheme = initTheme;

614
assets/vendor/aos/aos.cjs.js vendored Normal file
View File

@ -0,0 +1,614 @@
'use strict';
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var throttle = _interopDefault(require('lodash.throttle'));
var debounce = _interopDefault(require('lodash.debounce'));
var callback = function callback() {};
function containsAOSNode(nodes) {
var i = void 0,
currentNode = void 0,
result = void 0;
for (i = 0; i < nodes.length; i += 1) {
currentNode = nodes[i];
if (currentNode.dataset && currentNode.dataset.aos) {
return true;
}
result = currentNode.children && containsAOSNode(currentNode.children);
if (result) {
return true;
}
}
return false;
}
function check(mutations) {
if (!mutations) return;
mutations.forEach(function (mutation) {
var addedNodes = Array.prototype.slice.call(mutation.addedNodes);
var removedNodes = Array.prototype.slice.call(mutation.removedNodes);
var allNodes = addedNodes.concat(removedNodes);
if (containsAOSNode(allNodes)) {
return callback();
}
});
}
function getMutationObserver() {
return window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
}
function isSupported() {
return !!getMutationObserver();
}
function ready(selector, fn) {
var doc = window.document;
var MutationObserver = getMutationObserver();
var observer = new MutationObserver(check);
callback = fn;
observer.observe(doc.documentElement, {
childList: true,
subtree: true,
removedNodes: true
});
}
var observer = { isSupported: isSupported, ready: ready };
var classCallCheck = function (instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
};
var createClass = function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function (Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
};
}();
var _extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
/**
* Device detector
*/
var fullNameRe = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i;
var prefixRe = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i;
var fullNameMobileRe = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i;
var prefixMobileRe = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i;
function ua() {
return navigator.userAgent || navigator.vendor || window.opera || '';
}
var Detector = function () {
function Detector() {
classCallCheck(this, Detector);
}
createClass(Detector, [{
key: 'phone',
value: function phone() {
var a = ua();
return !!(fullNameRe.test(a) || prefixRe.test(a.substr(0, 4)));
}
}, {
key: 'mobile',
value: function mobile() {
var a = ua();
return !!(fullNameMobileRe.test(a) || prefixMobileRe.test(a.substr(0, 4)));
}
}, {
key: 'tablet',
value: function tablet() {
return this.mobile() && !this.phone();
}
// http://browserhacks.com/#hack-acea075d0ac6954f275a70023906050c
}, {
key: 'ie11',
value: function ie11() {
return '-ms-scroll-limit' in document.documentElement.style && '-ms-ime-align' in document.documentElement.style;
}
}]);
return Detector;
}();
var detect = new Detector();
/**
* Adds multiple classes on node
* @param {DOMNode} node
* @param {array} classes
*/
var addClasses = function addClasses(node, classes) {
return classes && classes.forEach(function (className) {
return node.classList.add(className);
});
};
/**
* Removes multiple classes from node
* @param {DOMNode} node
* @param {array} classes
*/
var removeClasses = function removeClasses(node, classes) {
return classes && classes.forEach(function (className) {
return node.classList.remove(className);
});
};
var fireEvent = function fireEvent(eventName, data) {
var customEvent = void 0;
if (detect.ie11()) {
customEvent = document.createEvent('CustomEvent');
customEvent.initCustomEvent(eventName, true, true, { detail: data });
} else {
customEvent = new CustomEvent(eventName, {
detail: data
});
}
return document.dispatchEvent(customEvent);
};
/**
* Set or remove aos-animate class
* @param {node} el element
* @param {int} top scrolled distance
*/
var applyClasses = function applyClasses(el, top) {
var options = el.options,
position = el.position,
node = el.node,
data = el.data;
var hide = function hide() {
if (!el.animated) return;
removeClasses(node, options.animatedClassNames);
fireEvent('aos:out', node);
if (el.options.id) {
fireEvent('aos:in:' + el.options.id, node);
}
el.animated = false;
};
var show = function show() {
if (el.animated) return;
addClasses(node, options.animatedClassNames);
fireEvent('aos:in', node);
if (el.options.id) {
fireEvent('aos:in:' + el.options.id, node);
}
el.animated = true;
};
if (options.mirror && top >= position.out && !options.once) {
hide();
} else if (top >= position.in) {
show();
} else if (el.animated && !options.once) {
hide();
}
};
/**
* Scroll logic - add or remove 'aos-animate' class on scroll
*
* @param {array} $elements array of elements nodes
* @return {void}
*/
var handleScroll = function handleScroll($elements) {
return $elements.forEach(function (el, i) {
return applyClasses(el, window.pageYOffset);
});
};
/**
* Get offset of DOM element
* like there were no transforms applied on it
*
* @param {Node} el [DOM element]
* @return {Object} [top and left offset]
*/
var offset = function offset(el) {
var _x = 0;
var _y = 0;
while (el && !isNaN(el.offsetLeft) && !isNaN(el.offsetTop)) {
_x += el.offsetLeft - (el.tagName != 'BODY' ? el.scrollLeft : 0);
_y += el.offsetTop - (el.tagName != 'BODY' ? el.scrollTop : 0);
el = el.offsetParent;
}
return {
top: _y,
left: _x
};
};
/**
* Get inline option with a fallback.
*
* @param {Node} el [Dom element]
* @param {String} key [Option key]
* @param {String} fallback [Default (fallback) value]
* @return {Mixed} [Option set with inline attributes or fallback value if not set]
*/
var getInlineOption = (function (el, key, fallback) {
var attr = el.getAttribute('data-aos-' + key);
if (typeof attr !== 'undefined') {
if (attr === 'true') {
return true;
} else if (attr === 'false') {
return false;
}
}
return attr || fallback;
});
/**
* Calculate offset
* basing on element's settings like:
* - anchor
* - offset
*
* @param {Node} el [Dom element]
* @return {Integer} [Final offset that will be used to trigger animation in good position]
*/
var getPositionIn = function getPositionIn(el, defaultOffset, defaultAnchorPlacement) {
var windowHeight = window.innerHeight;
var anchor = getInlineOption(el, 'anchor');
var inlineAnchorPlacement = getInlineOption(el, 'anchor-placement');
var additionalOffset = Number(getInlineOption(el, 'offset', inlineAnchorPlacement ? 0 : defaultOffset));
var anchorPlacement = inlineAnchorPlacement || defaultAnchorPlacement;
var finalEl = el;
if (anchor && document.querySelectorAll(anchor)) {
finalEl = document.querySelectorAll(anchor)[0];
}
var triggerPoint = offset(finalEl).top - windowHeight;
switch (anchorPlacement) {
case 'top-bottom':
// Default offset
break;
case 'center-bottom':
triggerPoint += finalEl.offsetHeight / 2;
break;
case 'bottom-bottom':
triggerPoint += finalEl.offsetHeight;
break;
case 'top-center':
triggerPoint += windowHeight / 2;
break;
case 'center-center':
triggerPoint += windowHeight / 2 + finalEl.offsetHeight / 2;
break;
case 'bottom-center':
triggerPoint += windowHeight / 2 + finalEl.offsetHeight;
break;
case 'top-top':
triggerPoint += windowHeight;
break;
case 'bottom-top':
triggerPoint += windowHeight + finalEl.offsetHeight;
break;
case 'center-top':
triggerPoint += windowHeight + finalEl.offsetHeight / 2;
break;
}
return triggerPoint + additionalOffset;
};
var getPositionOut = function getPositionOut(el, defaultOffset) {
var windowHeight = window.innerHeight;
var anchor = getInlineOption(el, 'anchor');
var additionalOffset = getInlineOption(el, 'offset', defaultOffset);
var finalEl = el;
if (anchor && document.querySelectorAll(anchor)) {
finalEl = document.querySelectorAll(anchor)[0];
}
var elementOffsetTop = offset(finalEl).top;
return elementOffsetTop + finalEl.offsetHeight - additionalOffset;
};
/* Clearing variables */
var prepare = function prepare($elements, options) {
$elements.forEach(function (el, i) {
var mirror = getInlineOption(el.node, 'mirror', options.mirror);
var once = getInlineOption(el.node, 'once', options.once);
var id = getInlineOption(el.node, 'id');
var customClassNames = options.useClassNames && el.node.getAttribute('data-aos');
var animatedClassNames = [options.animatedClassName].concat(customClassNames ? customClassNames.split(' ') : []).filter(function (className) {
return typeof className === 'string';
});
if (options.initClassName) {
el.node.classList.add(options.initClassName);
}
el.position = {
in: getPositionIn(el.node, options.offset, options.anchorPlacement),
out: mirror && getPositionOut(el.node, options.offset)
};
el.options = {
once: once,
mirror: mirror,
animatedClassNames: animatedClassNames,
id: id
};
});
return $elements;
};
/**
* Generate initial array with elements as objects
* This array will be extended later with elements attributes values
* like 'position'
*/
var elements = (function () {
var elements = document.querySelectorAll('[data-aos]');
return Array.prototype.map.call(elements, function (node) {
return { node: node };
});
});
/**
* *******************************************************
* AOS (Animate on scroll) - wowjs alternative
* made to animate elements on scroll in both directions
* *******************************************************
*/
/**
* Private variables
*/
var $aosElements = [];
var initialized = false;
/**
* Default options
*/
var options = {
offset: 120,
delay: 0,
easing: 'ease',
duration: 400,
disable: false,
once: false,
mirror: false,
anchorPlacement: 'top-bottom',
startEvent: 'DOMContentLoaded',
animatedClassName: 'aos-animate',
initClassName: 'aos-init',
useClassNames: false,
disableMutationObserver: false,
throttleDelay: 99,
debounceDelay: 50
};
// Detect not supported browsers (<=IE9)
// http://browserhacks.com/#hack-e71d8692f65334173fee715c222cb805
var isBrowserNotSupported = function isBrowserNotSupported() {
return document.all && !window.atob;
};
var initializeScroll = function initializeScroll() {
// Extend elements objects in $aosElements with their positions
$aosElements = prepare($aosElements, options);
// Perform scroll event, to refresh view and show/hide elements
handleScroll($aosElements);
/**
* Handle scroll event to animate elements on scroll
*/
window.addEventListener('scroll', throttle(function () {
handleScroll($aosElements, options.once);
}, options.throttleDelay));
return $aosElements;
};
/**
* Refresh AOS
*/
var refresh = function refresh() {
var initialize = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
// Allow refresh only when it was first initialized on startEvent
if (initialize) initialized = true;
if (initialized) initializeScroll();
};
/**
* Hard refresh
* create array with new elements and trigger refresh
*/
var refreshHard = function refreshHard() {
$aosElements = elements();
if (isDisabled(options.disable) || isBrowserNotSupported()) {
return disable();
}
refresh();
};
/**
* Disable AOS
* Remove all attributes to reset applied styles
*/
var disable = function disable() {
$aosElements.forEach(function (el, i) {
el.node.removeAttribute('data-aos');
el.node.removeAttribute('data-aos-easing');
el.node.removeAttribute('data-aos-duration');
el.node.removeAttribute('data-aos-delay');
if (options.initClassName) {
el.node.classList.remove(options.initClassName);
}
if (options.animatedClassName) {
el.node.classList.remove(options.animatedClassName);
}
});
};
/**
* Check if AOS should be disabled based on provided setting
*/
var isDisabled = function isDisabled(optionDisable) {
return optionDisable === true || optionDisable === 'mobile' && detect.mobile() || optionDisable === 'phone' && detect.phone() || optionDisable === 'tablet' && detect.tablet() || typeof optionDisable === 'function' && optionDisable() === true;
};
/**
* Initializing AOS
* - Create options merging defaults with user defined options
* - Set attributes on <body> as global setting - css relies on it
* - Attach preparing elements to options.startEvent,
* window resize and orientation change
* - Attach function that handle scroll and everything connected to it
* to window scroll event and fire once document is ready to set initial state
*/
var init = function init(settings) {
options = _extends(options, settings);
// Create initial array with elements -> to be fullfilled later with prepare()
$aosElements = elements();
/**
* Disable mutation observing if not supported
*/
if (!options.disableMutationObserver && !observer.isSupported()) {
console.info('\n aos: MutationObserver is not supported on this browser,\n code mutations observing has been disabled.\n You may have to call "refreshHard()" by yourself.\n ');
options.disableMutationObserver = true;
}
/**
* Observe [aos] elements
* If something is loaded by AJAX
* it'll refresh plugin automatically
*/
if (!options.disableMutationObserver) {
observer.ready('[data-aos]', refreshHard);
}
/**
* Don't init plugin if option `disable` is set
* or when browser is not supported
*/
if (isDisabled(options.disable) || isBrowserNotSupported()) {
return disable();
}
/**
* Set global settings on body, based on options
* so CSS can use it
*/
document.querySelector('body').setAttribute('data-aos-easing', options.easing);
document.querySelector('body').setAttribute('data-aos-duration', options.duration);
document.querySelector('body').setAttribute('data-aos-delay', options.delay);
/**
* Handle initializing
*/
if (['DOMContentLoaded', 'load'].indexOf(options.startEvent) === -1) {
// Listen to options.startEvent and initialize AOS
document.addEventListener(options.startEvent, function () {
refresh(true);
});
} else {
window.addEventListener('load', function () {
refresh(true);
});
}
if (options.startEvent === 'DOMContentLoaded' && ['complete', 'interactive'].indexOf(document.readyState) > -1) {
// Initialize AOS if default startEvent was already fired
refresh(true);
}
/**
* Refresh plugin on window resize or orientation change
*/
window.addEventListener('resize', debounce(refresh, options.debounceDelay, true));
window.addEventListener('orientationchange', debounce(refresh, options.debounceDelay, true));
return $aosElements;
};
/**
* Export Public API
*/
var aos = {
init: init,
refresh: refresh,
refreshHard: refreshHard
};
module.exports = aos;

1
assets/vendor/aos/aos.css vendored Normal file

File diff suppressed because one or more lines are too long

610
assets/vendor/aos/aos.esm.js vendored Normal file
View File

@ -0,0 +1,610 @@
import throttle from 'lodash.throttle';
import debounce from 'lodash.debounce';
var callback = function callback() {};
function containsAOSNode(nodes) {
var i = void 0,
currentNode = void 0,
result = void 0;
for (i = 0; i < nodes.length; i += 1) {
currentNode = nodes[i];
if (currentNode.dataset && currentNode.dataset.aos) {
return true;
}
result = currentNode.children && containsAOSNode(currentNode.children);
if (result) {
return true;
}
}
return false;
}
function check(mutations) {
if (!mutations) return;
mutations.forEach(function (mutation) {
var addedNodes = Array.prototype.slice.call(mutation.addedNodes);
var removedNodes = Array.prototype.slice.call(mutation.removedNodes);
var allNodes = addedNodes.concat(removedNodes);
if (containsAOSNode(allNodes)) {
return callback();
}
});
}
function getMutationObserver() {
return window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
}
function isSupported() {
return !!getMutationObserver();
}
function ready(selector, fn) {
var doc = window.document;
var MutationObserver = getMutationObserver();
var observer = new MutationObserver(check);
callback = fn;
observer.observe(doc.documentElement, {
childList: true,
subtree: true,
removedNodes: true
});
}
var observer = { isSupported: isSupported, ready: ready };
var classCallCheck = function (instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
};
var createClass = function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function (Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
};
}();
var _extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
/**
* Device detector
*/
var fullNameRe = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i;
var prefixRe = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i;
var fullNameMobileRe = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i;
var prefixMobileRe = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i;
function ua() {
return navigator.userAgent || navigator.vendor || window.opera || '';
}
var Detector = function () {
function Detector() {
classCallCheck(this, Detector);
}
createClass(Detector, [{
key: 'phone',
value: function phone() {
var a = ua();
return !!(fullNameRe.test(a) || prefixRe.test(a.substr(0, 4)));
}
}, {
key: 'mobile',
value: function mobile() {
var a = ua();
return !!(fullNameMobileRe.test(a) || prefixMobileRe.test(a.substr(0, 4)));
}
}, {
key: 'tablet',
value: function tablet() {
return this.mobile() && !this.phone();
}
// http://browserhacks.com/#hack-acea075d0ac6954f275a70023906050c
}, {
key: 'ie11',
value: function ie11() {
return '-ms-scroll-limit' in document.documentElement.style && '-ms-ime-align' in document.documentElement.style;
}
}]);
return Detector;
}();
var detect = new Detector();
/**
* Adds multiple classes on node
* @param {DOMNode} node
* @param {array} classes
*/
var addClasses = function addClasses(node, classes) {
return classes && classes.forEach(function (className) {
return node.classList.add(className);
});
};
/**
* Removes multiple classes from node
* @param {DOMNode} node
* @param {array} classes
*/
var removeClasses = function removeClasses(node, classes) {
return classes && classes.forEach(function (className) {
return node.classList.remove(className);
});
};
var fireEvent = function fireEvent(eventName, data) {
var customEvent = void 0;
if (detect.ie11()) {
customEvent = document.createEvent('CustomEvent');
customEvent.initCustomEvent(eventName, true, true, { detail: data });
} else {
customEvent = new CustomEvent(eventName, {
detail: data
});
}
return document.dispatchEvent(customEvent);
};
/**
* Set or remove aos-animate class
* @param {node} el element
* @param {int} top scrolled distance
*/
var applyClasses = function applyClasses(el, top) {
var options = el.options,
position = el.position,
node = el.node,
data = el.data;
var hide = function hide() {
if (!el.animated) return;
removeClasses(node, options.animatedClassNames);
fireEvent('aos:out', node);
if (el.options.id) {
fireEvent('aos:in:' + el.options.id, node);
}
el.animated = false;
};
var show = function show() {
if (el.animated) return;
addClasses(node, options.animatedClassNames);
fireEvent('aos:in', node);
if (el.options.id) {
fireEvent('aos:in:' + el.options.id, node);
}
el.animated = true;
};
if (options.mirror && top >= position.out && !options.once) {
hide();
} else if (top >= position.in) {
show();
} else if (el.animated && !options.once) {
hide();
}
};
/**
* Scroll logic - add or remove 'aos-animate' class on scroll
*
* @param {array} $elements array of elements nodes
* @return {void}
*/
var handleScroll = function handleScroll($elements) {
return $elements.forEach(function (el, i) {
return applyClasses(el, window.pageYOffset);
});
};
/**
* Get offset of DOM element
* like there were no transforms applied on it
*
* @param {Node} el [DOM element]
* @return {Object} [top and left offset]
*/
var offset = function offset(el) {
var _x = 0;
var _y = 0;
while (el && !isNaN(el.offsetLeft) && !isNaN(el.offsetTop)) {
_x += el.offsetLeft - (el.tagName != 'BODY' ? el.scrollLeft : 0);
_y += el.offsetTop - (el.tagName != 'BODY' ? el.scrollTop : 0);
el = el.offsetParent;
}
return {
top: _y,
left: _x
};
};
/**
* Get inline option with a fallback.
*
* @param {Node} el [Dom element]
* @param {String} key [Option key]
* @param {String} fallback [Default (fallback) value]
* @return {Mixed} [Option set with inline attributes or fallback value if not set]
*/
var getInlineOption = (function (el, key, fallback) {
var attr = el.getAttribute('data-aos-' + key);
if (typeof attr !== 'undefined') {
if (attr === 'true') {
return true;
} else if (attr === 'false') {
return false;
}
}
return attr || fallback;
});
/**
* Calculate offset
* basing on element's settings like:
* - anchor
* - offset
*
* @param {Node} el [Dom element]
* @return {Integer} [Final offset that will be used to trigger animation in good position]
*/
var getPositionIn = function getPositionIn(el, defaultOffset, defaultAnchorPlacement) {
var windowHeight = window.innerHeight;
var anchor = getInlineOption(el, 'anchor');
var inlineAnchorPlacement = getInlineOption(el, 'anchor-placement');
var additionalOffset = Number(getInlineOption(el, 'offset', inlineAnchorPlacement ? 0 : defaultOffset));
var anchorPlacement = inlineAnchorPlacement || defaultAnchorPlacement;
var finalEl = el;
if (anchor && document.querySelectorAll(anchor)) {
finalEl = document.querySelectorAll(anchor)[0];
}
var triggerPoint = offset(finalEl).top - windowHeight;
switch (anchorPlacement) {
case 'top-bottom':
// Default offset
break;
case 'center-bottom':
triggerPoint += finalEl.offsetHeight / 2;
break;
case 'bottom-bottom':
triggerPoint += finalEl.offsetHeight;
break;
case 'top-center':
triggerPoint += windowHeight / 2;
break;
case 'center-center':
triggerPoint += windowHeight / 2 + finalEl.offsetHeight / 2;
break;
case 'bottom-center':
triggerPoint += windowHeight / 2 + finalEl.offsetHeight;
break;
case 'top-top':
triggerPoint += windowHeight;
break;
case 'bottom-top':
triggerPoint += windowHeight + finalEl.offsetHeight;
break;
case 'center-top':
triggerPoint += windowHeight + finalEl.offsetHeight / 2;
break;
}
return triggerPoint + additionalOffset;
};
var getPositionOut = function getPositionOut(el, defaultOffset) {
var windowHeight = window.innerHeight;
var anchor = getInlineOption(el, 'anchor');
var additionalOffset = getInlineOption(el, 'offset', defaultOffset);
var finalEl = el;
if (anchor && document.querySelectorAll(anchor)) {
finalEl = document.querySelectorAll(anchor)[0];
}
var elementOffsetTop = offset(finalEl).top;
return elementOffsetTop + finalEl.offsetHeight - additionalOffset;
};
/* Clearing variables */
var prepare = function prepare($elements, options) {
$elements.forEach(function (el, i) {
var mirror = getInlineOption(el.node, 'mirror', options.mirror);
var once = getInlineOption(el.node, 'once', options.once);
var id = getInlineOption(el.node, 'id');
var customClassNames = options.useClassNames && el.node.getAttribute('data-aos');
var animatedClassNames = [options.animatedClassName].concat(customClassNames ? customClassNames.split(' ') : []).filter(function (className) {
return typeof className === 'string';
});
if (options.initClassName) {
el.node.classList.add(options.initClassName);
}
el.position = {
in: getPositionIn(el.node, options.offset, options.anchorPlacement),
out: mirror && getPositionOut(el.node, options.offset)
};
el.options = {
once: once,
mirror: mirror,
animatedClassNames: animatedClassNames,
id: id
};
});
return $elements;
};
/**
* Generate initial array with elements as objects
* This array will be extended later with elements attributes values
* like 'position'
*/
var elements = (function () {
var elements = document.querySelectorAll('[data-aos]');
return Array.prototype.map.call(elements, function (node) {
return { node: node };
});
});
/**
* *******************************************************
* AOS (Animate on scroll) - wowjs alternative
* made to animate elements on scroll in both directions
* *******************************************************
*/
/**
* Private variables
*/
var $aosElements = [];
var initialized = false;
/**
* Default options
*/
var options = {
offset: 120,
delay: 0,
easing: 'ease',
duration: 400,
disable: false,
once: false,
mirror: false,
anchorPlacement: 'top-bottom',
startEvent: 'DOMContentLoaded',
animatedClassName: 'aos-animate',
initClassName: 'aos-init',
useClassNames: false,
disableMutationObserver: false,
throttleDelay: 99,
debounceDelay: 50
};
// Detect not supported browsers (<=IE9)
// http://browserhacks.com/#hack-e71d8692f65334173fee715c222cb805
var isBrowserNotSupported = function isBrowserNotSupported() {
return document.all && !window.atob;
};
var initializeScroll = function initializeScroll() {
// Extend elements objects in $aosElements with their positions
$aosElements = prepare($aosElements, options);
// Perform scroll event, to refresh view and show/hide elements
handleScroll($aosElements);
/**
* Handle scroll event to animate elements on scroll
*/
window.addEventListener('scroll', throttle(function () {
handleScroll($aosElements, options.once);
}, options.throttleDelay));
return $aosElements;
};
/**
* Refresh AOS
*/
var refresh = function refresh() {
var initialize = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
// Allow refresh only when it was first initialized on startEvent
if (initialize) initialized = true;
if (initialized) initializeScroll();
};
/**
* Hard refresh
* create array with new elements and trigger refresh
*/
var refreshHard = function refreshHard() {
$aosElements = elements();
if (isDisabled(options.disable) || isBrowserNotSupported()) {
return disable();
}
refresh();
};
/**
* Disable AOS
* Remove all attributes to reset applied styles
*/
var disable = function disable() {
$aosElements.forEach(function (el, i) {
el.node.removeAttribute('data-aos');
el.node.removeAttribute('data-aos-easing');
el.node.removeAttribute('data-aos-duration');
el.node.removeAttribute('data-aos-delay');
if (options.initClassName) {
el.node.classList.remove(options.initClassName);
}
if (options.animatedClassName) {
el.node.classList.remove(options.animatedClassName);
}
});
};
/**
* Check if AOS should be disabled based on provided setting
*/
var isDisabled = function isDisabled(optionDisable) {
return optionDisable === true || optionDisable === 'mobile' && detect.mobile() || optionDisable === 'phone' && detect.phone() || optionDisable === 'tablet' && detect.tablet() || typeof optionDisable === 'function' && optionDisable() === true;
};
/**
* Initializing AOS
* - Create options merging defaults with user defined options
* - Set attributes on <body> as global setting - css relies on it
* - Attach preparing elements to options.startEvent,
* window resize and orientation change
* - Attach function that handle scroll and everything connected to it
* to window scroll event and fire once document is ready to set initial state
*/
var init = function init(settings) {
options = _extends(options, settings);
// Create initial array with elements -> to be fullfilled later with prepare()
$aosElements = elements();
/**
* Disable mutation observing if not supported
*/
if (!options.disableMutationObserver && !observer.isSupported()) {
console.info('\n aos: MutationObserver is not supported on this browser,\n code mutations observing has been disabled.\n You may have to call "refreshHard()" by yourself.\n ');
options.disableMutationObserver = true;
}
/**
* Observe [aos] elements
* If something is loaded by AJAX
* it'll refresh plugin automatically
*/
if (!options.disableMutationObserver) {
observer.ready('[data-aos]', refreshHard);
}
/**
* Don't init plugin if option `disable` is set
* or when browser is not supported
*/
if (isDisabled(options.disable) || isBrowserNotSupported()) {
return disable();
}
/**
* Set global settings on body, based on options
* so CSS can use it
*/
document.querySelector('body').setAttribute('data-aos-easing', options.easing);
document.querySelector('body').setAttribute('data-aos-duration', options.duration);
document.querySelector('body').setAttribute('data-aos-delay', options.delay);
/**
* Handle initializing
*/
if (['DOMContentLoaded', 'load'].indexOf(options.startEvent) === -1) {
// Listen to options.startEvent and initialize AOS
document.addEventListener(options.startEvent, function () {
refresh(true);
});
} else {
window.addEventListener('load', function () {
refresh(true);
});
}
if (options.startEvent === 'DOMContentLoaded' && ['complete', 'interactive'].indexOf(document.readyState) > -1) {
// Initialize AOS if default startEvent was already fired
refresh(true);
}
/**
* Refresh plugin on window resize or orientation change
*/
window.addEventListener('resize', debounce(refresh, options.debounceDelay, true));
window.addEventListener('orientationchange', debounce(refresh, options.debounceDelay, true));
return $aosElements;
};
/**
* Export Public API
*/
var aos = {
init: init,
refresh: refresh,
refreshHard: refreshHard
};
export default aos;

1
assets/vendor/aos/aos.js vendored Normal file

File diff suppressed because one or more lines are too long

1
assets/vendor/aos/aos.js.map vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More