Initial commit
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 462 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 687 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 984 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 259 KiB |
|
After Width: | Height: | Size: 448 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 316 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 281 KiB |
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 323 KiB |
|
After Width: | Height: | Size: 461 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 8.4 MiB |
|
After Width: | Height: | Size: 8.6 MiB |
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
||||
})();
|
||||
|
|
@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||