187 lines
4.1 KiB
JavaScript

/**
* 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;