Restructure: move all files from Public HTML/ to root for Hostinger deployment
This commit is contained in:
Executable
+440
@@ -0,0 +1,440 @@
|
||||
/**
|
||||
* MSPE Admin Panel JavaScript
|
||||
*/
|
||||
|
||||
// Lock admin UI until auth verification completes (except login/reset pages)
|
||||
document.documentElement.classList.add('auth-pending');
|
||||
|
||||
const authStyle = document.createElement('style');
|
||||
authStyle.textContent = `
|
||||
html.auth-pending body {
|
||||
visibility: hidden;
|
||||
}
|
||||
html.auth-pending::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 99999;
|
||||
background: #0f172a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
html.auth-pending::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 50%; left: 50%;
|
||||
z-index: 100000;
|
||||
width: 36px; height: 36px;
|
||||
margin: -18px 0 0 -18px;
|
||||
border: 3px solid rgba(59,130,246,0.2);
|
||||
border-top-color: #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: authSpin .7s linear infinite;
|
||||
}
|
||||
@keyframes authSpin { to { transform: rotate(360deg) } }
|
||||
`;
|
||||
document.head.appendChild(authStyle);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (isAuthExemptPage()) {
|
||||
unlockAdminUi();
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize components
|
||||
initSidebar();
|
||||
initDropdowns();
|
||||
initLogout();
|
||||
|
||||
checkAuth();
|
||||
});
|
||||
|
||||
/**
|
||||
* Sidebar Toggle
|
||||
*/
|
||||
function initSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const mobileToggle = document.getElementById('mobile-toggle');
|
||||
const sidebarToggle = document.getElementById('sidebar-toggle');
|
||||
|
||||
if (mobileToggle) {
|
||||
mobileToggle.addEventListener('click', function() {
|
||||
sidebar.classList.toggle('active');
|
||||
});
|
||||
}
|
||||
|
||||
if (sidebarToggle) {
|
||||
sidebarToggle.addEventListener('click', function() {
|
||||
sidebar.classList.toggle('active');
|
||||
});
|
||||
}
|
||||
|
||||
// Close sidebar when clicking outside on mobile
|
||||
document.addEventListener('click', function(e) {
|
||||
if (window.innerWidth <= 1024) {
|
||||
if (!sidebar.contains(e.target) && !mobileToggle?.contains(e.target)) {
|
||||
sidebar.classList.remove('active');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dropdowns
|
||||
*/
|
||||
function initDropdowns() {
|
||||
const dropdowns = document.querySelectorAll('.user-dropdown');
|
||||
|
||||
dropdowns.forEach(dropdown => {
|
||||
const toggle = dropdown.querySelector('.dropdown-toggle');
|
||||
|
||||
if (toggle) {
|
||||
toggle.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
dropdown.classList.toggle('active');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Close dropdowns when clicking outside
|
||||
document.addEventListener('click', function() {
|
||||
dropdowns.forEach(dropdown => {
|
||||
dropdown.classList.remove('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout Handler
|
||||
*/
|
||||
function initLogout() {
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener('click', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
await fetch('/api/auth.php', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ action: 'logout' })
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Logout request failed, proceeding with local logout');
|
||||
}
|
||||
|
||||
// Redirect to login
|
||||
window.location.href = 'index.html';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Authentication
|
||||
*/
|
||||
function checkAuth() {
|
||||
fetch('/api/auth.php', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ action: 'verify' })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
applyUserToHeader(data.user || {});
|
||||
unlockAdminUi();
|
||||
startSessionWatchdog();
|
||||
} else {
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
window.location.href = 'index.html';
|
||||
});
|
||||
}
|
||||
|
||||
function isAuthExemptPage() {
|
||||
return window.location.pathname.includes('index.html') ||
|
||||
window.location.pathname.endsWith('/admin/') ||
|
||||
window.location.pathname.includes('reset-password.html');
|
||||
}
|
||||
|
||||
function unlockAdminUi() {
|
||||
document.documentElement.classList.remove('auth-pending');
|
||||
}
|
||||
|
||||
/**
|
||||
* Periodic Session Re-Verification (every 5 minutes)
|
||||
* Catches expired / revoked sessions proactively.
|
||||
*/
|
||||
let _sessionInterval = null;
|
||||
function startSessionWatchdog() {
|
||||
if (_sessionInterval) return;
|
||||
_sessionInterval = setInterval(() => {
|
||||
fetch('/api/auth.php', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'verify' })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (!data.success) {
|
||||
clearInterval(_sessionInterval);
|
||||
showNotification('Session expired — redirecting to login.', 'warning');
|
||||
setTimeout(() => { window.location.href = 'index.html'; }, 2000);
|
||||
}
|
||||
})
|
||||
.catch(() => { /* network hiccup, retry next cycle */ });
|
||||
}, 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
function applyUserToHeader(user) {
|
||||
const nameSpans = document.querySelectorAll('.user-name');
|
||||
nameSpans.forEach(el => {
|
||||
el.textContent = user.username || 'Admin';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* API Helper Functions
|
||||
*/
|
||||
const API = {
|
||||
baseUrl: '/api',
|
||||
|
||||
async request(endpoint, options = {}) {
|
||||
const defaultOptions = {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/${endpoint}`, {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
headers: {
|
||||
...defaultOptions.headers,
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
window.location.href = 'index.html';
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status === 403) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
|
||||
async get(endpoint) {
|
||||
return this.request(endpoint);
|
||||
},
|
||||
|
||||
async post(endpoint, data) {
|
||||
return this.request(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
},
|
||||
|
||||
async put(endpoint, data) {
|
||||
return this.request(endpoint, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
},
|
||||
|
||||
async delete(endpoint) {
|
||||
return this.request(endpoint, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
},
|
||||
|
||||
async upload(endpoint, formData) {
|
||||
const response = await fetch(`${this.baseUrl}/${endpoint}`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
window.location.href = 'index.html';
|
||||
return;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Notification Helper
|
||||
*/
|
||||
function showNotification(message, type = 'success') {
|
||||
const iconMap = {
|
||||
success: 'check-circle',
|
||||
error: 'exclamation-circle',
|
||||
info: 'info-circle',
|
||||
warning: 'exclamation-triangle'
|
||||
};
|
||||
const icon = iconMap[type] || 'info-circle';
|
||||
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification ${type}`;
|
||||
|
||||
const iconEl = document.createElement('i');
|
||||
iconEl.className = `fas fa-${icon}`;
|
||||
const spanEl = document.createElement('span');
|
||||
spanEl.textContent = message;
|
||||
notification.appendChild(iconEl);
|
||||
notification.appendChild(spanEl);
|
||||
|
||||
// Add styles if not present
|
||||
if (!document.querySelector('.notification-styles')) {
|
||||
const styles = document.createElement('style');
|
||||
styles.className = 'notification-styles';
|
||||
styles.textContent = `
|
||||
.notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-weight: 500;
|
||||
z-index: 9999;
|
||||
animation: slideIn 0.3s ease;
|
||||
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1);
|
||||
}
|
||||
.notification.success {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
}
|
||||
.notification.error {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
.notification.info {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
.notification.warning {
|
||||
background: #f59e0b;
|
||||
color: white;
|
||||
}
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(styles);
|
||||
}
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.animation = 'slideIn 0.3s ease reverse';
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format Date Helper
|
||||
*/
|
||||
function formatDate(dateString) {
|
||||
const options = { year: 'numeric', month: 'short', day: 'numeric' };
|
||||
return new Date(dateString).toLocaleDateString('en-US', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm Dialog Helper
|
||||
*/
|
||||
function confirmDialog(message) {
|
||||
return new Promise((resolve) => {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal active';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-overlay"></div>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Confirm</h3>
|
||||
<button class="modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="confirm-msg"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" id="confirm-cancel">Cancel</button>
|
||||
<button class="btn btn-danger" id="confirm-ok">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
modal.querySelector('.confirm-msg').textContent = message;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
modal.querySelector('#confirm-ok').addEventListener('click', () => {
|
||||
modal.remove();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
modal.querySelector('#confirm-cancel').addEventListener('click', () => {
|
||||
modal.remove();
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
modal.querySelector('.modal-close').addEventListener('click', () => {
|
||||
modal.remove();
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
modal.querySelector('.modal-overlay').addEventListener('click', () => {
|
||||
modal.remove();
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounce Helper
|
||||
*/
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
// Export for use in other scripts
|
||||
window.MSPE = {
|
||||
API,
|
||||
showNotification,
|
||||
formatDate,
|
||||
confirmDialog,
|
||||
debounce
|
||||
};
|
||||
Reference in New Issue
Block a user