/** * Bank Sampah Digital - Main JavaScript File * Optimized dengan modern patterns dan clean code principles */ class BankSampahApp { constructor() { this.config = { animationDuration: 600, toastDuration: 3000, counterSpeed: 20, observerThreshold: 0.1, counterThreshold: 0.5 }; this.observers = new Map(); this.timers = new Set(); } /** * Smooth scroll untuk anchor links */ initSmoothScroll() { const anchors = document.querySelectorAll('a[href^="#"]'); anchors.forEach(anchor => { anchor.addEventListener('click', this.handleAnchorClick.bind(this)); }); } /** * Handle anchor click dengan error handling */ handleAnchorClick(event) { event.preventDefault(); const href = event.currentTarget.getAttribute('href'); const target = document.querySelector(href); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } else { console.warn(`Target element ${href} tidak ditemukan`); } } /** * Intersection Observer untuk scroll animations */ initScrollAnimations() { const options = { threshold: this.config.observerThreshold, rootMargin: '0px 0px -50px 0px' }; const observer = new IntersectionObserver( this.handleIntersection.bind(this), options ); this.observers.set('scrollAnimation', observer); this.observeElements(['.card', '.stat'], observer); } /** * Handle intersection untuk animasi */ handleIntersection(entries) { entries.forEach(entry => { if (entry.isIntersecting) { this.animateElement(entry.target); } }); } /** * Animasi element dengan transition */ animateElement(element) { element.style.opacity = '1'; element.style.transform = 'translateY(0)'; element.classList.add('animate-fade-in'); } /** * Setup initial styles untuk animasi */ setupAnimationStyles(element) { Object.assign(element.style, { opacity: '0', transform: 'translateY(20px)', transition: `opacity ${this.config.animationDuration}ms ease, transform ${this.config.animationDuration}ms ease` }); } /** * Observer multiple elements dengan selector */ observeElements(selectors, observer) { selectors.forEach(selector => { document.querySelectorAll(selector).forEach(element => { this.setupAnimationStyles(element); observer.observe(element); }); }); } /** * Counter animation dengan performance optimization */ initCounterAnimation() { const counters = document.querySelectorAll('.stat-value'); const observer = new IntersectionObserver( this.handleCounterIntersection.bind(this), { threshold: this.config.counterThreshold } ); this.observers.set('counter', observer); counters.forEach(counter => observer.observe(counter)); } /** * Handle counter intersection */ handleCounterIntersection(entries) { entries.forEach(entry => { if (entry.isIntersecting) { this.animateCounter(entry.target); this.observers.get('counter')?.unobserve(entry.target); } }); } /** * Animasi counter dengan format preservation */ animateCounter(counter) { const originalText = counter.textContent; const target = this.extractNumber(originalText); const isDecimal = originalText.includes('.'); const suffix = this.extractSuffix(originalText); const increment = target / (this.config.animationDuration / this.config.counterSpeed); let current = 0; const timer = setInterval(() => { current += increment; if (current >= target) { current = target; clearInterval(timer); this.timers.delete(timer); } counter.textContent = this.formatCounter(current, isDecimal, suffix); }, this.config.counterSpeed); this.timers.add(timer); } /** * Extract number dari text */ extractNumber(text) { const match = text.match(/[\d.,]+/); return match ? parseFloat(match[0].replace(',', '')) : 0; } /** * Extract suffix dari text */ extractSuffix(text) { return text.replace(/[\d.,\s]+/, '').trim(); } /** * Format counter value */ formatCounter(value, isDecimal, suffix) { const formatted = isDecimal ? value.toFixed(1) : Math.floor(value).toLocaleString('id-ID'); return suffix ? `${formatted} ${suffix}` : formatted; } /** * Theme management */ initThemeSwitch() { const toggle = document.querySelector('#theme-toggle'); if (toggle) { toggle.addEventListener('change', this.handleThemeChange.bind(this)); } this.loadSavedTheme(); } /** * Handle theme change */ handleThemeChange(event) { const theme = event.target.checked ? 'dark' : 'light'; this.setTheme(theme); } /** * Set theme dan save ke localStorage */ setTheme(theme) { document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('theme', theme); } /** * Load saved theme */ loadSavedTheme() { const savedTheme = localStorage.getItem('theme') || 'emerald'; this.setTheme(savedTheme); } /** * Loading state management */ toggleLoading(show = false) { const loading = document.querySelector('.loading-overlay'); if (loading) { loading.classList.toggle('hidden', !show); } } /** * Toast notification system dengan auto-remove */ showToast(message, type = 'info') { const toast = this.createToastElement(message, type); document.body.appendChild(toast); const timer = setTimeout(() => { this.removeToast(toast); this.timers.delete(timer); }, this.config.toastDuration); this.timers.add(timer); } /** * Create toast element */ createToastElement(message, type) { const toast = document.createElement('div'); toast.className = 'toast toast-top toast-end'; toast.innerHTML = `
${this.escapeHtml(message)}
`; return toast; } /** * Remove toast dengan animation */ removeToast(toast) { toast.style.opacity = '0'; toast.style.transform = 'translateX(100%)'; setTimeout(() => { toast.remove(); }, 300); } /** * Escape HTML untuk security */ escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Form validation dengan detailed feedback */ validateForm(formId) { const form = document.getElementById(formId); if (!form) { console.warn(`Form dengan ID '${formId}' tidak ditemukan`); return false; } const requiredInputs = form.querySelectorAll('input[required], select[required], textarea[required]'); let isValid = true; requiredInputs.forEach(input => { const isFieldValid = this.validateField(input); if (!isFieldValid) { isValid = false; } }); return isValid; } /** * Validate individual field */ validateField(input) { const isValid = input.value.trim() !== ''; input.classList.toggle('input-error', !isValid); if (!isValid) { this.showFieldError(input); } return isValid; } /** * Show field error */ showFieldError(input) { const fieldName = input.getAttribute('placeholder') || input.getAttribute('name') || 'Field'; this.showToast(`${fieldName} harus diisi`, 'error'); } /** * Cleanup resources */ cleanup() { // Clear all timers this.timers.forEach(timer => clearInterval(timer)); this.timers.clear(); // Disconnect all observers this.observers.forEach(observer => observer.disconnect()); this.observers.clear(); } /** * Initialize aplikasi */ init() { try { this.initSmoothScroll(); this.initScrollAnimations(); this.initCounterAnimation(); this.initThemeSwitch(); // Setup cleanup pada page unload window.addEventListener('beforeunload', () => this.cleanup()); // Hide loading setelah load complete window.addEventListener('load', () => this.toggleLoading(false)); console.log('🌱 Bank Sampah Digital - Aplikasi siap digunakan!'); } catch (error) { console.error('Error initializing BankSampahApp:', error); } } } // Create global instance const BankSampah = new BankSampahApp(); // Initialize saat DOM ready document.addEventListener('DOMContentLoaded', function() { BankSampah.init(); }); // Export untuk penggunaan di file lain if (typeof module !== 'undefined' && module.exports) { module.exports = BankSampah; }