/** * CLQMS Frontend - Global Alpine.js Components & Utilities */ // Wait for Alpine to be ready document.addEventListener('alpine:init', () => { /** * Global Auth Store * Manages authentication state across the app */ Alpine.store('auth', { user: null, isAuthenticated: false, setUser(userData) { this.user = userData; this.isAuthenticated = !!userData; }, clearUser() { this.user = null; this.isAuthenticated = false; } }); /** * Toast Notification Store */ Alpine.store('toast', { messages: [], show(message, type = 'info', duration = 4000) { const id = Date.now(); this.messages.push({ id, message, type }); setTimeout(() => { this.dismiss(id); }, duration); }, dismiss(id) { this.messages = this.messages.filter(m => m.id !== id); }, success(message) { this.show(message, 'success'); }, error(message) { this.show(message, 'error', 6000); }, info(message) { this.show(message, 'info'); } }); /** * Login Component */ Alpine.data('loginForm', () => ({ username: '', password: '', rememberMe: false, showPassword: false, isLoading: false, error: null, async submitLogin() { // Reset error this.error = null; // Validation if (!this.username.trim()) { this.error = 'Please enter your username'; this.shakeForm(); return; } if (!this.password) { this.error = 'Please enter your password'; this.shakeForm(); return; } // Start loading this.isLoading = true; try { const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, credentials: 'include', // Important for cookies body: JSON.stringify({ username: this.username.trim(), password: this.password }) }); const data = await response.json(); if (response.ok && data.status === 'success') { // Store user data Alpine.store('auth').setUser(data.data); // Show success feedback Alpine.store('toast').success('Login successful! Redirecting...'); // Redirect to dashboard setTimeout(() => { window.location.href = '/dashboard'; }, 500); } else { // Handle error this.error = data.message || 'Invalid username or password'; this.shakeForm(); } } catch (err) { console.error('Login error:', err); this.error = 'Connection error. Please try again.'; this.shakeForm(); } finally { this.isLoading = false; } }, shakeForm() { const form = this.$refs.loginCard; if (form) { form.classList.add('shake'); setTimeout(() => form.classList.remove('shake'), 500); } }, togglePassword() { this.showPassword = !this.showPassword; } })); }); /** * Utility Functions */ const Utils = { // Format date to locale string formatDate(dateString) { return new Date(dateString).toLocaleDateString('id-ID', { year: 'numeric', month: 'short', day: 'numeric' }); }, // Debounce function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }, // API helper with credentials async api(endpoint, options = {}) { const defaultOptions = { headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, credentials: 'include' }; const response = await fetch(endpoint, { ...defaultOptions, ...options }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || 'API request failed'); } return data; } }; // Expose Utils globally window.Utils = Utils;