bps-rw/wwwroot/js/site.js

378 lines
9.6 KiB
JavaScript

/**
* Bank Sampah Digital - Main JavaScript File
* Optimized dengan modern patterns dan clean code principles
*/
class BpsRwApp {
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 = `
<div class="alert alert-${type}">
<span>${this.escapeHtml(message)}</span>
</div>
`;
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 BpsRwApp:', error);
}
}
}
// Create global instance
const BpsRw = new BpsRwApp();
// Initialize saat DOM ready
document.addEventListener('DOMContentLoaded', function() {
BpsRw.init();
});
// Export untuk penggunaan di file lain
if (typeof module !== 'undefined' && module.exports) {
module.exports = BpsRw;
}