Fix icon hover colors, text alignment, and UI improvements

This commit is contained in:
Krikorios
2026-02-25 23:25:59 +02:00
parent 00fda3e045
commit fcc4352afa
46 changed files with 4011 additions and 600 deletions
+14
View File
@@ -0,0 +1,14 @@
# Admin directory — disable directory listing
Options -Indexes
# Block direct access to any PHP files OTHER than index.php (auth is handled by JS + API)
# All legitimate admin PHP is in /api/ — nothing in /admin/ should be a PHP script
<FilesMatch "\.php$">
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order deny,allow
Deny from all
</IfModule>
</FilesMatch>
+38 -13
View File
@@ -451,15 +451,15 @@
<div class="quick-slots">
<label>Quick Add Times</label>
<div class="quick-slots-grid">
<button type="button" class="quick-slot-btn" onclick="addQuickTime('09:00')">9:00 AM</button>
<button type="button" class="quick-slot-btn" onclick="addQuickTime('10:00')">10:00 AM</button>
<button type="button" class="quick-slot-btn" onclick="addQuickTime('11:00')">11:00 AM</button>
<button type="button" class="quick-slot-btn" onclick="addQuickTime('13:00')">1:00 PM</button>
<button type="button" class="quick-slot-btn" onclick="addQuickTime('14:00')">2:00 PM</button>
<button type="button" class="quick-slot-btn" onclick="addQuickTime('15:00')">3:00 PM</button>
<button type="button" class="quick-slot-btn" onclick="addQuickTime('16:00')">4:00 PM</button>
<button type="button" class="quick-slot-btn" onclick="addQuickTime('17:00')">5:00 PM</button>
<button type="button" class="quick-slot-btn" onclick="addQuickTime('18:00')">6:00 PM</button>
<button type="button" class="quick-slot-btn" data-time="09:00">9:00 AM</button>
<button type="button" class="quick-slot-btn" data-time="10:00">10:00 AM</button>
<button type="button" class="quick-slot-btn" data-time="11:00">11:00 AM</button>
<button type="button" class="quick-slot-btn" data-time="13:00">1:00 PM</button>
<button type="button" class="quick-slot-btn" data-time="14:00">2:00 PM</button>
<button type="button" class="quick-slot-btn" data-time="15:00">3:00 PM</button>
<button type="button" class="quick-slot-btn" data-time="16:00">4:00 PM</button>
<button type="button" class="quick-slot-btn" data-time="17:00">5:00 PM</button>
<button type="button" class="quick-slot-btn" data-time="18:00">6:00 PM</button>
</div>
</div>
@@ -467,7 +467,7 @@
<button type="submit" class="btn btn-primary">
<i class="fas fa-plus"></i> Add Slot
</button>
<button type="button" class="btn btn-secondary" onclick="generateWeekSlots()">
<button type="button" class="btn btn-secondary" id="generate-week-btn">
<i class="fas fa-calendar-week"></i> Generate Week
</button>
</div>
@@ -577,6 +577,31 @@
document.getElementById('block-time-form').addEventListener('submit', handleBlockTime);
document.getElementById('filter-date-from').addEventListener('change', filterSlots);
document.getElementById('filter-date-to').addEventListener('change', filterSlots);
document.getElementById('generate-week-btn').addEventListener('click', generateWeekSlots);
document.querySelector('.quick-slots-grid')?.addEventListener('click', function(event) {
const btn = event.target.closest('.quick-slot-btn[data-time]');
if (!btn) return;
addQuickTime(btn.dataset.time);
});
document.getElementById('blocked-times-list').addEventListener('click', function(event) {
const btn = event.target.closest('[data-action="unblock-time"]');
if (!btn) return;
unblockTime(btn.dataset.id);
});
document.getElementById('slots-table-body').addEventListener('click', function(event) {
const btn = event.target.closest('[data-action]');
if (!btn) return;
const { action, id } = btn.dataset;
if (action === 'toggle-slot' && id) {
toggleSlot(id);
} else if (action === 'delete-slot' && id) {
deleteSlot(id);
}
});
}
function setDefaultDate() {
@@ -663,7 +688,7 @@
<span style="font-weight: 600;">${formattedTime}</span>
${blocked.reason ? `<span style="color: #64748b; font-size: 0.875rem; margin-left: 0.5rem;">(${blocked.reason})</span>` : ''}
</div>
<button onclick="unblockTime('${blocked.id}')" style="background: none; border: none; color: #dc2626; cursor: pointer; padding: 0.25rem;">
<button data-action="unblock-time" data-id="${blocked.id}" style="background: none; border: none; color: #dc2626; cursor: pointer; padding: 0.25rem;">
<i class="fas fa-times"></i>
</button>
</div>
@@ -837,10 +862,10 @@
<span style="color: var(--gray-500);">${slot.notes || '-'}</span>
</td>
<td>
<button class="action-btn toggle" onclick="toggleSlot('${slot.id}')">
<button class="action-btn toggle" data-action="toggle-slot" data-id="${slot.id}">
<i class="fas fa-${isAvailable ? 'eye-slash' : 'eye'}"></i>
</button>
<button class="action-btn delete" onclick="deleteSlot('${slot.id}')">
<button class="action-btn delete" data-action="delete-slot" data-id="${slot.id}">
<i class="fas fa-trash"></i>
</button>
</td>
+39 -6
View File
@@ -484,6 +484,39 @@
document.getElementById('filter-date-from').addEventListener('change', loadBookings);
document.getElementById('filter-date-to').addEventListener('change', loadBookings);
document.getElementById('bookings-search').addEventListener('input', filterBookings);
document.getElementById('bookings-table-body').addEventListener('click', function(event) {
const actionBtn = event.target.closest('[data-action]');
if (actionBtn) {
event.stopPropagation();
const { action, id } = actionBtn.dataset;
if (action === 'select-booking' && id) {
selectBooking(id);
} else if (action === 'confirm-booking' && id) {
confirmBooking(id);
} else if (action === 'cancel-booking' && id) {
cancelBooking(id);
}
return;
}
const row = event.target.closest('[data-row-action="select-booking"]');
if (row && row.dataset.id) {
selectBooking(row.dataset.id);
}
});
document.getElementById('booking-details').addEventListener('click', function(event) {
const actionBtn = event.target.closest('[data-action]');
if (!actionBtn) return;
const { action, id } = actionBtn.dataset;
if (action === 'confirm-booking' && id) {
confirmBooking(id);
} else if (action === 'cancel-booking' && id) {
cancelBooking(id);
}
});
}
async function loadBookings() {
@@ -536,7 +569,7 @@
const statusClass = `status-${booking.status}`;
return `
<tr class="booking-row" data-id="${booking.id}" onclick="selectBooking('${booking.id}')">
<tr class="booking-row" data-id="${booking.id}" data-row-action="select-booking">
<td>
<div style="font-weight: 600;">${formattedDate}</div>
<div style="font-size: 0.75rem; color: var(--gray-500);">${date.toLocaleDateString('en-US', { weekday: 'long' })}</div>
@@ -550,14 +583,14 @@
<td>${formatService(booking.service_interest)}</td>
<td><span class="status-badge ${statusClass}">${booking.status}</span></td>
<td>
<button class="action-btn view" onclick="event.stopPropagation(); selectBooking('${booking.id}')">
<button class="action-btn view" data-action="select-booking" data-id="${booking.id}">
<i class="fas fa-eye"></i>
</button>
${booking.status === 'pending' ? `
<button class="action-btn confirm" onclick="event.stopPropagation(); confirmBooking('${booking.id}')">
<button class="action-btn confirm" data-action="confirm-booking" data-id="${booking.id}">
<i class="fas fa-check"></i>
</button>
<button class="action-btn cancel" onclick="event.stopPropagation(); cancelBooking('${booking.id}')">
<button class="action-btn cancel" data-action="cancel-booking" data-id="${booking.id}">
<i class="fas fa-times"></i>
</button>
` : ''}
@@ -644,10 +677,10 @@
${selectedBooking.status === 'pending' ? `
<div class="booking-actions">
<button class="btn btn-success" onclick="confirmBooking('${selectedBooking.id}')">
<button class="btn btn-success" data-action="confirm-booking" data-id="${selectedBooking.id}">
<i class="fas fa-check"></i> Confirm Booking
</button>
<button class="btn btn-danger" onclick="cancelBooking('${selectedBooking.id}')">
<button class="btn btn-danger" data-action="cancel-booking" data-id="${selectedBooking.id}">
<i class="fas fa-times"></i> Cancel Booking
</button>
</div>
+30 -13
View File
@@ -112,6 +112,12 @@
<span>Bookings</span>
</a>
</li>
<li class="nav-item">
<a href="/admin/availability.html" class="nav-link">
<i class="fas fa-clock"></i>
<span>Availability</span>
</a>
</li>
</ul>
</div>
@@ -162,7 +168,7 @@
<div class="header-notifications">
<button class="notification-btn">
<i class="fas fa-bell"></i>
<span class="notification-badge">3</span>
<span class="notification-badge" id="notif-badge">0</span>
</button>
</div>
@@ -325,8 +331,14 @@
const stats = response.data;
// Update Messages Stats
document.getElementById('stat-messages-count').textContent = stats.messages?.unread || 0;
const unreadCount = stats.messages?.unread || 0;
document.getElementById('stat-messages-count').textContent = unreadCount;
document.getElementById('stat-messages-total').textContent = `${stats.messages?.total || 0} total`;
const badge = document.getElementById('notif-badge');
if (badge) {
badge.textContent = unreadCount;
badge.style.display = unreadCount > 0 ? '' : 'none';
}
// Update Bookings Stats
document.getElementById('stat-bookings-count').textContent = stats.bookings?.pending || 0;
@@ -355,14 +367,14 @@
container.innerHTML = response.data.map(item => `
<div class="content-item">
<div class="item-image">
<img src="${item.featured_image || '/images/logo/logo.png'}" alt="${item.title}" style="object-fit: cover;">
<img src="${item.featured_image || '/images/logo/logo.png'}" alt="${MSPE.escapeHtml(item.title)}" style="object-fit: cover;">
</div>
<div class="item-info">
<h4>${item.title}</h4>
<span class="item-meta">${MSPE.formatDate(item.created_at)}${item.category || 'News'}</span>
<h4>${MSPE.escapeHtml(item.title)}</h4>
<span class="item-meta">${MSPE.formatDate(item.created_at)}${MSPE.escapeHtml(item.category || 'News')}</span>
</div>
<div class="item-actions">
<a href="/admin/news.html?edit=${item.id}" class="action-btn edit"><i class="fas fa-edit"></i></a>
<a href="/admin/news.html?edit=${encodeURIComponent(item.id)}" class="action-btn edit"><i class="fas fa-edit"></i></a>
</div>
</div>
`).join('');
@@ -392,19 +404,24 @@
const container = document.getElementById('recent-messages');
if (response && response.success && response.data && response.data.length > 0) {
container.innerHTML = response.data.map(msg => `
<div class="message-item" style="cursor: pointer;" onclick="window.location.href='/admin/messages.html?id=${msg.id}'">
container.innerHTML = response.data.map(msg => {
const safeName = MSPE.escapeHtml((msg.first_name || 'Unknown') + ' ' + (msg.last_name || ''));
const safePreview = MSPE.escapeHtml((msg.message || '').substring(0, 50)) + (msg.message && msg.message.length > 50 ? '...' : '');
const safeStatus = ['unread', 'read', 'replied'].includes(msg.status) ? msg.status : 'unread';
const safeId = encodeURIComponent(msg.id);
return `
<a class="message-item" style="cursor: pointer; text-decoration: none; color: inherit;" href="/admin/messages.html?id=${safeId}">
<div class="message-avatar">
<img src="https://ui-avatars.com/api/?name=${encodeURIComponent((msg.first_name || 'U') + ' ' + (msg.last_name || ''))}&background=random" alt="Avatar">
</div>
<div class="message-info">
<h4>${msg.first_name || 'Unknown'} ${msg.last_name || ''}</h4>
<p>${(msg.message || '').substring(0, 50)}${msg.message && msg.message.length > 50 ? '...' : ''}</p>
<h4>${safeName}</h4>
<p>${safePreview}</p>
<span class="message-time">${MSPE.formatDate(msg.created_at)}</span>
</div>
<span class="message-status ${msg.status || 'unread'}"></span>
</div>
`).join('');
<span class="message-status ${safeStatus}"></span>
</a>`;
}).join('');
} else {
container.innerHTML = `
<div class="empty-state" style="padding: 2rem; text-align: center;">
+23 -1
View File
@@ -39,6 +39,7 @@ document.head.appendChild(authStyle);
document.addEventListener('DOMContentLoaded', function() {
if (isAuthExemptPage()) {
unlockAdminUi();
enforceSafeBlankLinks();
return;
}
@@ -46,10 +47,21 @@ document.addEventListener('DOMContentLoaded', function() {
initSidebar();
initDropdowns();
initLogout();
enforceSafeBlankLinks();
checkAuth();
});
function enforceSafeBlankLinks(scope = document) {
scope.querySelectorAll('a[target="_blank"]').forEach(link => {
const currentRel = (link.getAttribute('rel') || '').toLowerCase();
const relParts = new Set(currentRel.split(/\s+/).filter(Boolean));
relParts.add('noopener');
relParts.add('noreferrer');
link.setAttribute('rel', Array.from(relParts).join(' '));
});
}
/**
* Sidebar Toggle
*/
@@ -430,11 +442,21 @@ function debounce(func, wait) {
};
}
/**
* HTML Escape Helper — prevents XSS when inserting dynamic values into innerHTML
*/
function escapeHtml(str) {
const div = document.createElement('div');
div.appendChild(document.createTextNode(String(str ?? '')));
return div.innerHTML;
}
// Export for use in other scripts
window.MSPE = {
API,
showNotification,
formatDate,
confirmDialog,
debounce
debounce,
escapeHtml
};
+82 -40
View File
@@ -112,6 +112,12 @@
<span>Bookings</span>
</a>
</li>
<li class="nav-item">
<a href="/admin/availability.html" class="nav-link">
<i class="fas fa-clock"></i>
<span>Availability</span>
</a>
</li>
</ul>
</div>
@@ -154,7 +160,7 @@
</div>
<div class="header-right">
<button class="btn btn-primary" onclick="openUploadModal()">
<button class="btn btn-primary" id="open-upload-btn">
<i class="fas fa-upload"></i> Upload Files
</button>
@@ -234,7 +240,7 @@
</select>
</div>
<div class="toolbar-right">
<button class="btn btn-outline btn-sm" onclick="createFolder()">
<button class="btn btn-outline btn-sm" id="create-folder-btn">
<i class="fas fa-folder-plus"></i> New Folder
</button>
<div class="view-toggle">
@@ -279,13 +285,13 @@
<!-- Bulk Actions -->
<div class="bulk-actions" id="bulk-actions" style="display: none;">
<span class="selected-count">0 selected</span>
<button class="btn btn-outline btn-sm" onclick="moveSelected()">
<button class="btn btn-outline btn-sm" id="move-selected-btn">
<i class="fas fa-folder"></i> Move
</button>
<button class="btn btn-outline btn-sm" onclick="downloadSelected()">
<button class="btn btn-outline btn-sm" id="download-selected-btn">
<i class="fas fa-download"></i> Download
</button>
<button class="btn btn-danger btn-sm" onclick="deleteSelected()">
<button class="btn btn-danger btn-sm" id="delete-selected-btn">
<i class="fas fa-trash"></i> Delete
</button>
</div>
@@ -299,7 +305,7 @@
<div class="modal-container">
<div class="modal-header">
<h2>Upload Files</h2>
<button class="modal-close" onclick="closeUploadModal()">&times;</button>
<button class="modal-close" id="upload-modal-close">&times;</button>
</div>
<div class="modal-body">
<div class="upload-dropzone" id="dropzone">
@@ -327,7 +333,7 @@
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="closeUploadModal()">Cancel</button>
<button class="btn btn-outline" id="upload-cancel-btn">Cancel</button>
<button class="btn btn-primary" id="start-upload" disabled>
<i class="fas fa-upload"></i> Start Upload
</button>
@@ -341,7 +347,7 @@
<div class="modal-container modal-lg">
<div class="modal-header">
<h2 id="view-title">File Details</h2>
<button class="modal-close" onclick="closeViewModal()">&times;</button>
<button class="modal-close" id="view-modal-close">&times;</button>
</div>
<div class="modal-body">
<div class="media-view-content">
@@ -373,7 +379,7 @@
<label>URL</label>
<div class="url-copy">
<input type="text" id="detail-url" readonly>
<button class="btn btn-sm btn-outline" onclick="copyDetailUrl()">
<button class="btn btn-sm btn-outline" id="copy-detail-url-btn">
<i class="fas fa-copy"></i>
</button>
</div>
@@ -386,13 +392,13 @@
</div>
</div>
<div class="modal-footer">
<button class="btn btn-danger" onclick="deleteFromView()">
<button class="btn btn-danger" id="delete-from-view-btn">
<i class="fas fa-trash"></i> Delete
</button>
<button class="btn btn-outline" onclick="downloadFile()">
<button class="btn btn-outline" id="download-view-file-btn">
<i class="fas fa-download"></i> Download
</button>
<button class="btn btn-primary" onclick="saveDetails()">
<button class="btn btn-primary" id="save-details-btn">
<i class="fas fa-save"></i> Save
</button>
</div>
@@ -404,17 +410,72 @@
let allMedia = [];
document.addEventListener('DOMContentLoaded', function() {
setupEventListeners();
loadMedia();
// Search & Filter
});
function setupEventListeners() {
document.getElementById('media-search').addEventListener('input', MSPE.debounce(filterMedia, 300));
document.getElementById('type-filter').addEventListener('change', filterMedia);
document.getElementById('folder-filter').addEventListener('change', filterMedia);
// File Input for Upload
document.getElementById('file-input').addEventListener('change', handleFileSelect);
document.getElementById('start-upload').addEventListener('click', uploadFiles);
});
document.getElementById('open-upload-btn')?.addEventListener('click', openUploadModal);
document.getElementById('create-folder-btn')?.addEventListener('click', createFolder);
document.getElementById('move-selected-btn')?.addEventListener('click', moveSelected);
document.getElementById('download-selected-btn')?.addEventListener('click', downloadSelected);
document.getElementById('delete-selected-btn')?.addEventListener('click', deleteSelected);
document.getElementById('upload-modal-close')?.addEventListener('click', closeUploadModal);
document.getElementById('upload-cancel-btn')?.addEventListener('click', closeUploadModal);
document.getElementById('view-modal-close')?.addEventListener('click', closeViewModal);
document.getElementById('copy-detail-url-btn')?.addEventListener('click', copyDetailUrl);
document.getElementById('delete-from-view-btn')?.addEventListener('click', deleteFromView);
document.getElementById('download-view-file-btn')?.addEventListener('click', () => downloadFile());
document.getElementById('save-details-btn')?.addEventListener('click', saveDetails);
document.querySelector('#upload-modal .modal-backdrop')?.addEventListener('click', closeUploadModal);
document.querySelector('#view-modal .modal-backdrop')?.addEventListener('click', closeViewModal);
const dropzone = document.getElementById('dropzone');
const fileInput = document.getElementById('file-input');
dropzone.addEventListener('click', () => fileInput.click());
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
dropzone.classList.add('dragover');
});
dropzone.addEventListener('dragleave', () => {
dropzone.classList.remove('dragover');
});
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
dropzone.classList.remove('dragover');
fileInput.files = e.dataTransfer.files;
handleFileSelect({ target: fileInput });
});
document.getElementById('media-grid').addEventListener('click', function(event) {
const actionBtn = event.target.closest('[data-action]');
if (!actionBtn) return;
const { action, id, path } = actionBtn.dataset;
if (action === 'view' && id) {
viewMedia(id);
} else if (action === 'copy' && path) {
copyUrl(path);
} else if (action === 'delete' && id) {
deleteMedia(id);
}
});
document.getElementById('media-grid').addEventListener('change', function(event) {
if (event.target.matches('.media-checkbox input')) {
updateBulkActions();
}
});
}
async function loadMedia() {
try {
@@ -454,9 +515,9 @@
: `<i class="fas fa-file-${getFileIcon(item.type)}"></i>`
}
<div class="media-overlay">
<button class="btn btn-sm" onclick="viewMedia('${item.id}')"><i class="fas fa-eye"></i></button>
<button class="btn btn-sm" onclick="copyUrl('/${item.path}')"><i class="fas fa-link"></i></button>
<button class="btn btn-sm" onclick="deleteMedia('${item.id}')"><i class="fas fa-trash"></i></button>
<button class="btn btn-sm" data-action="view" data-id="${item.id}"><i class="fas fa-eye"></i></button>
<button class="btn btn-sm" data-action="copy" data-path="/${item.path}"><i class="fas fa-link"></i></button>
<button class="btn btn-sm" data-action="delete" data-id="${item.id}"><i class="fas fa-trash"></i></button>
</div>
</div>
<div class="media-info">
@@ -464,7 +525,7 @@
<span class="media-size">${formatBytes(item.size)}</span>
</div>
<label class="media-checkbox">
<input type="checkbox" name="selected[]" value="${item.id}" onchange="updateBulkActions()">
<input type="checkbox" name="selected[]" value="${item.id}">
<span class="checkmark"></span>
</label>
</div>
@@ -618,25 +679,6 @@
document.getElementById('view-modal').classList.remove('active');
}
// Drag & Drop
const dropzone = document.getElementById('dropzone');
const fileInput = document.getElementById('file-input');
dropzone.addEventListener('click', () => fileInput.click());
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
dropzone.classList.add('dragover');
});
dropzone.addEventListener('dragleave', () => {
dropzone.classList.remove('dragover');
});
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
dropzone.classList.remove('dragover');
fileInput.files = e.dataTransfer.files;
handleFileSelect({ target: fileInput });
});
function updateBulkActions() {
const checked = document.querySelectorAll('.media-checkbox input:checked');
const bulkActions = document.getElementById('bulk-actions');
+63 -20
View File
@@ -357,6 +357,12 @@
<span>Bookings</span>
</a>
</li>
<li class="nav-item">
<a href="/admin/availability.html" class="nav-link">
<i class="fas fa-clock"></i>
<span>Availability</span>
</a>
</li>
</ul>
</div>
@@ -531,27 +537,38 @@
container.innerHTML = messages.map(message => {
const isUnread = (message.status === 'unread');
const isActive = (message.id === selectedMessageId);
const name = `${message.first_name} ${message.last_name}`;
const name = `${message.first_name || ''} ${message.last_name || ''}`.trim();
const timeAgo = getTimeAgo(new Date(message.created_at));
const preview = message.message.length > 60
? message.message.substring(0, 60) + '...'
: message.message;
const rawMessage = String(message.message || '');
const preview = rawMessage.length > 60
? rawMessage.substring(0, 60) + '...'
: rawMessage;
const safeId = MSPE.escapeHtml(message.id || '');
const safeName = MSPE.escapeHtml(name || 'Unknown Sender');
const safeTimeAgo = MSPE.escapeHtml(timeAgo || '');
const safePreview = MSPE.escapeHtml(preview || '');
return `
<div class="message-item ${isUnread ? 'unread' : ''} ${isActive ? 'active' : ''}"
onclick="selectMessage('${message.id}')"
data-id="${message.id}">
data-id="${safeId}">
<div class="message-header">
<span class="message-sender">
${isUnread ? '<span class="unread-dot"></span>' : ''}
${name}
${safeName}
</span>
<span class="message-meta">${timeAgo}</span>
<span class="message-meta">${safeTimeAgo}</span>
</div>
<div class="message-preview">${preview}</div>
<div class="message-preview">${safePreview}</div>
</div>
`;
}).join('');
container.querySelectorAll('.message-item').forEach(item => {
item.addEventListener('click', () => {
const id = item.dataset.id;
if (id) selectMessage(id);
});
});
}
async function selectMessage(id) {
@@ -590,7 +607,7 @@
function renderMessageDetails(message) {
const container = document.getElementById('message-details');
const name = `${message.first_name} ${message.last_name}`;
const name = `${message.first_name || ''} ${message.last_name || ''}`.trim();
const date = new Date(message.created_at);
const formattedDate = date.toLocaleDateString('en-US', {
weekday: 'long',
@@ -608,18 +625,28 @@
'consulting': 'Business Consulting',
'general': 'General Inquiry'
};
const safeName = MSPE.escapeHtml(name || 'Unknown Sender');
const safeDate = MSPE.escapeHtml(formattedDate || '');
const safeEmail = MSPE.escapeHtml(message.email || '');
const safePhone = MSPE.escapeHtml(message.phone || 'Not provided');
const safeCompany = MSPE.escapeHtml(message.company || 'Not provided');
const safeService = MSPE.escapeHtml(serviceLabels[message.service] || message.service || 'Not specified');
const safeBudget = message.budget ? MSPE.escapeHtml(message.budget) : '';
const safeMessageBody = MSPE.escapeHtml(message.message || 'Not provided').replace(/\n/g, '<br>');
const safeId = MSPE.escapeHtml(message.id || '');
container.innerHTML = `
<div class="message-details-header">
<div>
<h3>${name}</h3>
<div class="meta">${formattedDate}</div>
<h3>${safeName}</h3>
<div class="meta">${safeDate}</div>
</div>
<div class="message-actions-header">
<button onclick="replyToMessage('${message.email}')">
<button class="reply-btn" data-email="${safeEmail}">
<i class="fas fa-reply"></i> Reply
</button>
<button class="delete" onclick="deleteMessage('${message.id}')">
<button class="delete delete-btn" data-id="${safeId}">
<i class="fas fa-trash"></i> Delete
</button>
</div>
@@ -629,34 +656,50 @@
<div class="contact-info">
<div class="contact-info-item">
<label>Email</label>
<div class="value"><a href="mailto:${message.email}">${message.email}</a></div>
<div class="value"><a href="mailto:${safeEmail}">${safeEmail}</a></div>
</div>
<div class="contact-info-item">
<label>Phone</label>
<div class="value">${message.phone || 'Not provided'}</div>
<div class="value">${safePhone}</div>
</div>
<div class="contact-info-item">
<label>Company</label>
<div class="value">${message.company || 'Not provided'}</div>
<div class="value">${safeCompany}</div>
</div>
<div class="contact-info-item">
<label>Service Interest</label>
<div class="value">${serviceLabels[message.service] || message.service || 'Not specified'}</div>
<div class="value">${safeService}</div>
</div>
${message.budget ? `
<div class="contact-info-item">
<label>Budget</label>
<div class="value">${message.budget}</div>
<div class="value">${safeBudget}</div>
</div>
` : ''}
</div>
<div class="message-body">
<h4>Message</h4>
<p>${message.message.replace(/\n/g, '<br>')}</p>
<p>${safeMessageBody}</p>
</div>
</div>
`;
const replyBtn = container.querySelector('.reply-btn');
if (replyBtn) {
replyBtn.addEventListener('click', () => {
const email = replyBtn.dataset.email || '';
if (email) replyToMessage(email);
});
}
const deleteBtn = container.querySelector('.delete-btn');
if (deleteBtn) {
deleteBtn.addEventListener('click', () => {
const id = deleteBtn.dataset.id || '';
if (id) deleteMessage(id);
});
}
}
function replyToMessage(email) {
+19 -2
View File
@@ -112,6 +112,12 @@
<span>Bookings</span>
</a>
</li>
<li class="nav-item">
<a href="/admin/availability.html" class="nav-link">
<i class="fas fa-clock"></i>
<span>Availability</span>
</a>
</li>
</ul>
</div>
@@ -419,6 +425,17 @@
document.getElementById('add-new-btn').addEventListener('click', showForm);
document.getElementById('back-to-list').addEventListener('click', showList);
document.getElementById('news-form').addEventListener('submit', saveArticle);
document.getElementById('news-table-body').addEventListener('click', function(event) {
const actionBtn = event.target.closest('[data-action]');
if (!actionBtn) return;
const { action, id } = actionBtn.dataset;
if (action === 'edit' && id) {
editArticle(id);
} else if (action === 'delete' && id) {
deleteArticle(id);
}
});
// Save draft button
document.getElementById('save-draft').addEventListener('click', function() {
@@ -541,8 +558,8 @@
<td>${MSPE.formatDate(article.created_at)}</td>
<td>
<div class="table-actions">
<button class="action-btn edit" onclick="editArticle('${article.id}')" title="Edit"><i class="fas fa-edit"></i></button>
<button class="action-btn delete" onclick="deleteArticle('${article.id}')" title="Delete"><i class="fas fa-trash"></i></button>
<button class="action-btn edit" data-action="edit" data-id="${article.id}" title="Edit"><i class="fas fa-edit"></i></button>
<button class="action-btn delete" data-action="delete" data-id="${article.id}" title="Delete"><i class="fas fa-trash"></i></button>
</div>
</td>
</tr>
+51 -11
View File
@@ -112,6 +112,12 @@
<span>Bookings</span>
</a>
</li>
<li class="nav-item">
<a href="/admin/availability.html" class="nav-link">
<i class="fas fa-clock"></i>
<span>Availability</span>
</a>
</li>
</ul>
</div>
@@ -210,11 +216,11 @@
<!-- Page Editor Modal -->
<div class="modal" id="page-modal">
<div class="modal-overlay" onclick="closePageModal()"></div>
<div class="modal-overlay" id="page-modal-overlay"></div>
<div class="modal-content" style="max-width:700px;">
<div class="modal-header">
<h3 id="page-modal-title">Edit Page</h3>
<button class="modal-close" onclick="closePageModal()"><i class="fas fa-times"></i></button>
<button class="modal-close" id="page-modal-close-btn"><i class="fas fa-times"></i></button>
</div>
<div class="modal-body" style="max-height:70vh;overflow-y:auto;">
<form id="page-form">
@@ -249,19 +255,19 @@
</form>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="closePageModal()">Cancel</button>
<button class="btn btn-primary" onclick="savePage()"><i class="fas fa-save"></i> Save Changes</button>
<button class="btn btn-outline" id="page-modal-cancel-btn">Cancel</button>
<button class="btn btn-primary" id="page-modal-save-btn"><i class="fas fa-save"></i> Save Changes</button>
</div>
</div>
</div>
<!-- Global Section Editor Modal -->
<div class="modal" id="global-modal">
<div class="modal-overlay" onclick="closeGlobalModal()"></div>
<div class="modal-overlay" id="global-modal-overlay"></div>
<div class="modal-content" style="max-width:550px;">
<div class="modal-header">
<h3 id="global-modal-title">Edit Section</h3>
<button class="modal-close" onclick="closeGlobalModal()"><i class="fas fa-times"></i></button>
<button class="modal-close" id="global-modal-close-btn"><i class="fas fa-times"></i></button>
</div>
<div class="modal-body">
<form id="global-form">
@@ -270,8 +276,8 @@
</form>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="closeGlobalModal()">Cancel</button>
<button class="btn btn-primary" onclick="saveGlobalSection()"><i class="fas fa-save"></i> Save</button>
<button class="btn btn-outline" id="global-modal-cancel-btn">Cancel</button>
<button class="btn btn-primary" id="global-modal-save-btn"><i class="fas fa-save"></i> Save</button>
</div>
</div>
</div>
@@ -283,10 +289,44 @@
let currentPageData = null;
document.addEventListener('DOMContentLoaded', function() {
setupPageEventListeners();
loadPages();
loadGlobalSections();
});
function setupPageEventListeners() {
document.getElementById('page-modal-overlay')?.addEventListener('click', closePageModal);
document.getElementById('page-modal-close-btn')?.addEventListener('click', closePageModal);
document.getElementById('page-modal-cancel-btn')?.addEventListener('click', closePageModal);
document.getElementById('page-modal-save-btn')?.addEventListener('click', savePage);
document.getElementById('global-modal-overlay')?.addEventListener('click', closeGlobalModal);
document.getElementById('global-modal-close-btn')?.addEventListener('click', closeGlobalModal);
document.getElementById('global-modal-cancel-btn')?.addEventListener('click', closeGlobalModal);
document.getElementById('global-modal-save-btn')?.addEventListener('click', saveGlobalSection);
document.getElementById('pages-grid')?.addEventListener('click', function(e) {
const editBtn = e.target.closest('[data-action="edit-page"]');
if (!editBtn) return;
const slug = editBtn.dataset.slug;
if (slug) editPage(slug);
});
document.getElementById('global-sections-grid')?.addEventListener('click', function(e) {
const editBtn = e.target.closest('[data-action="edit-global-section"]');
if (!editBtn) return;
const slug = editBtn.dataset.slug;
if (slug) editGlobalSection(slug);
});
document.getElementById('page-sections-editor')?.addEventListener('click', function(e) {
const toggle = e.target.closest('.section-toggle');
if (!toggle) return;
const body = toggle.nextElementSibling;
if (body) body.classList.toggle('collapsed');
});
}
// ── Load & Render ────────────────────────────────────
async function loadPages() {
@@ -327,7 +367,7 @@
<a href="${page.url || '#'}" target="_blank" class="btn btn-sm btn-outline">
<i class="fas fa-eye"></i> View
</a>
<button class="btn btn-sm btn-primary" onclick="editPage('${page.slug}')">
<button class="btn btn-sm btn-primary" data-action="edit-page" data-slug="${MSPE.escapeHtml(page.slug || '')}">
<i class="fas fa-edit"></i> Edit
</button>
</div>
@@ -356,7 +396,7 @@
<h4>${s.label}</h4>
<p>${s.description}</p>
</div>
<button class="btn btn-sm btn-outline" onclick="editGlobalSection('${s.slug}')">Edit</button>
<button class="btn btn-sm btn-outline" data-action="edit-global-section" data-slug="${MSPE.escapeHtml(s.slug || '')}">Edit</button>
</div>
`).join('');
}
@@ -395,7 +435,7 @@
}).join('');
return `<div style="margin-bottom:1.5rem;border:1px solid var(--gray-200);border-radius:var(--radius-sm);overflow:hidden;">
<div style="background:var(--gray-100);padding:.625rem 1rem;font-size:.8rem;font-weight:600;color:var(--dark);display:flex;align-items:center;gap:.5rem;cursor:pointer;" onclick="this.parentElement.querySelector('.sect-body').classList.toggle('collapsed')">
<div class="section-toggle" style="background:var(--gray-100);padding:.625rem 1rem;font-size:.8rem;font-weight:600;color:var(--dark);display:flex;align-items:center;gap:.5rem;cursor:pointer;">
<i class="fas fa-puzzle-piece" style="color:var(--primary);"></i> ${section.label}
<i class="fas fa-chevron-down" style="margin-left:auto;font-size:.65rem;color:var(--gray-400);"></i>
</div>
+28 -4
View File
@@ -112,6 +112,12 @@
<span>Bookings</span>
</a>
</li>
<li class="nav-item">
<a href="/admin/availability.html" class="nav-link">
<i class="fas fa-clock"></i>
<span>Availability</span>
</a>
</li>
</ul>
</div>
@@ -387,6 +393,24 @@
document.getElementById('back-to-list').addEventListener('click', showList);
document.getElementById('publish-btn').addEventListener('click', () => saveProject('published'));
document.getElementById('save-draft-btn').addEventListener('click', () => saveProject('draft'));
document.getElementById('portfolio-grid').addEventListener('click', function(event) {
const actionBtn = event.target.closest('[data-action]');
if (!actionBtn) return;
const { action, id } = actionBtn.dataset;
if (action === 'edit' && id) {
editProject(id);
} else if (action === 'delete' && id) {
deleteProject(id);
}
});
document.getElementById('gallery-preview').addEventListener('click', function(event) {
const removeBtn = event.target.closest('[data-action="remove-gallery-item"]');
if (!removeBtn) return;
removeGalleryItem(removeBtn);
});
// Filters
document.getElementById('category-filter').addEventListener('change', filterProjects);
@@ -450,10 +474,10 @@
<p class="portfolio-category">${project.category}</p>
<p class="portfolio-client">Client: ${project.client || 'N/A'}</p>
<div class="portfolio-admin-actions">
<button class="btn btn-sm btn-outline" onclick="editProject('${project.id}')">
<button class="btn btn-sm btn-outline" data-action="edit" data-id="${project.id}">
<i class="fas fa-edit"></i> Edit
</button>
<button class="btn btn-sm btn-outline btn-danger" onclick="deleteProject('${project.id}')">
<button class="btn btn-sm btn-outline btn-danger" data-action="delete" data-id="${project.id}">
<i class="fas fa-trash"></i>
</button>
</div>
@@ -585,7 +609,7 @@
galleryPreview.innerHTML = existingGallery.map(url =>
`<div class="gallery-preview-item" data-url="${url}">
<img src="/${url}" alt="Gallery image">
<button type="button" class="gallery-remove-btn" onclick="removeGalleryItem(this)"><i class="fas fa-times"></i></button>
<button type="button" class="gallery-remove-btn" data-action="remove-gallery-item"><i class="fas fa-times"></i></button>
</div>`
).join('');
}
@@ -621,7 +645,7 @@
const item = document.createElement('div');
item.className = 'gallery-preview-item';
item.dataset.galleryIdx = idx;
item.innerHTML = `<img src="${ev.target.result}" alt=""><button type="button" class="gallery-remove-btn" onclick="removeGalleryItem(this)"><i class="fas fa-times"></i></button>`;
item.innerHTML = `<img src="${ev.target.result}" alt=""><button type="button" class="gallery-remove-btn" data-action="remove-gallery-item"><i class="fas fa-times"></i></button>`;
preview.appendChild(item);
};
reader.readAsDataURL(file);
+40 -6
View File
@@ -112,6 +112,12 @@
<span>Bookings</span>
</a>
</li>
<li class="nav-item">
<a href="/admin/availability.html" class="nav-link">
<i class="fas fa-clock"></i>
<span>Availability</span>
</a>
</li>
</ul>
</div>
@@ -201,7 +207,7 @@
<div class="modal-content modal-lg">
<div class="modal-header">
<h2>Edit Service</h2>
<button class="modal-close" onclick="closeModal()">
<button class="modal-close" id="service-modal-close-btn">
<i class="fas fa-times"></i>
</button>
</div>
@@ -251,8 +257,8 @@ Network Management"></textarea>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="closeModal()">Cancel</button>
<button class="btn btn-primary" onclick="saveService()">Save Changes</button>
<button class="btn btn-outline" id="service-modal-cancel-btn">Cancel</button>
<button class="btn btn-primary" id="service-modal-save-btn">Save Changes</button>
</div>
</div>
</div>
@@ -264,6 +270,34 @@ Network Management"></textarea>
document.addEventListener('DOMContentLoaded', function() {
loadServices();
document.getElementById('add-service-btn').addEventListener('click', openAddServiceModal);
document.getElementById('service-modal-close-btn')?.addEventListener('click', closeModal);
document.getElementById('service-modal-cancel-btn')?.addEventListener('click', closeModal);
document.getElementById('service-modal-save-btn')?.addEventListener('click', saveService);
document.querySelector('#service-modal .modal-overlay')?.addEventListener('click', closeModal);
const servicesGrid = document.getElementById('services-admin-grid');
servicesGrid?.addEventListener('click', function(e) {
const editBtn = e.target.closest('[data-action="edit-service"]');
if (editBtn) {
const id = editBtn.dataset.id;
if (id) editService(id);
return;
}
const deleteBtn = e.target.closest('[data-action="delete-service"]');
if (deleteBtn) {
const id = deleteBtn.dataset.id;
if (id) deleteService(id);
}
});
servicesGrid?.addEventListener('change', function(e) {
const toggle = e.target.closest('[data-action="toggle-service"]');
if (!toggle) return;
const id = toggle.dataset.id;
if (id) toggleService(id, toggle.checked);
});
});
async function loadServices() {
@@ -297,14 +331,14 @@ Network Management"></textarea>
<i class="fas ${service.icon || 'fa-cogs'}"></i>
</div>
<div class="service-actions">
<button class="btn btn-sm btn-icon" onclick="editService('${service.id}')">
<button class="btn btn-sm btn-icon" data-action="edit-service" data-id="${MSPE.escapeHtml(service.id || '')}">
<i class="fas fa-edit"></i>
</button>
<label class="toggle-switch">
<input type="checkbox" ${service.active ? 'checked' : ''} onchange="toggleService('${service.id}', this.checked)">
<input type="checkbox" ${service.active ? 'checked' : ''} data-action="toggle-service" data-id="${MSPE.escapeHtml(service.id || '')}">
<span class="toggle-slider"></span>
</label>
<button class="btn btn-sm btn-icon text-danger" onclick="deleteService('${service.id}')">
<button class="btn btn-sm btn-icon text-danger" data-action="delete-service" data-id="${MSPE.escapeHtml(service.id || '')}">
<i class="fas fa-trash"></i>
</button>
</div>
+25 -3
View File
@@ -112,6 +112,12 @@
<span>Bookings</span>
</a>
</li>
<li class="nav-item">
<a href="/admin/availability.html" class="nav-link">
<i class="fas fa-clock"></i>
<span>Availability</span>
</a>
</li>
</ul>
</div>
@@ -231,15 +237,15 @@
<div class="logo-upload">
<img src="/images/logo/logo.png" alt="Logo" class="current-logo">
<input type="file" id="logo-input" accept="image/*" style="display:none">
<button class="btn btn-outline btn-sm" onclick="document.getElementById('logo-input').click()">Change Logo</button>
<button class="btn btn-outline btn-sm" id="logo-upload-trigger">Change Logo</button>
</div>
</div>
<div class="form-group">
<label>Favicon</label>
<div class="logo-upload">
<img src="/images/logo/logo.png" alt="Favicon" class="current-favicon" onerror="this.style.display='none'">
<img src="/images/logo/logo.png" alt="Favicon" class="current-favicon" id="current-favicon">
<input type="file" id="favicon-input" accept="image/*,.ico" style="display:none">
<button class="btn btn-outline btn-sm" onclick="document.getElementById('favicon-input').click()">Upload Favicon</button>
<button class="btn btn-outline btn-sm" id="favicon-upload-trigger">Upload Favicon</button>
</div>
</div>
</div>
@@ -639,8 +645,24 @@ Sitemap: https://mspe.pro/sitemap.xml</textarea>
setupColorPickers();
setupSaveButtons();
setupEmailTest();
setupAssetControls();
});
function setupAssetControls() {
document.getElementById('logo-upload-trigger')?.addEventListener('click', function() {
document.getElementById('logo-input')?.click();
});
document.getElementById('favicon-upload-trigger')?.addEventListener('click', function() {
document.getElementById('favicon-input')?.click();
});
const favicon = document.getElementById('current-favicon');
favicon?.addEventListener('error', function() {
this.style.display = 'none';
});
}
function setupTabs() {
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', function() {
+17 -2
View File
@@ -243,6 +243,12 @@
<span>Bookings</span>
</a>
</li>
<li class="nav-item">
<a href="/admin/availability.html" class="nav-link">
<i class="fas fa-clock"></i>
<span>Availability</span>
</a>
</li>
</ul>
</div>
@@ -283,7 +289,7 @@
</div>
<div class="header-right">
<button class="btn btn-primary export-btn" onclick="exportSubscribers()">
<button class="btn btn-primary export-btn" id="export-subscribers-btn">
<i class="fas fa-download"></i> Export CSV
</button>
@@ -371,6 +377,15 @@
function setupEventListeners() {
document.getElementById('search-input').addEventListener('input', MSPE.debounce(filterSubscribers, 300));
document.getElementById('export-subscribers-btn')?.addEventListener('click', exportSubscribers);
const tableBody = document.getElementById('subscribers-table-body');
tableBody?.addEventListener('click', function(e) {
const deleteBtn = e.target.closest('[data-action="delete-subscriber"]');
if (!deleteBtn) return;
const id = deleteBtn.dataset.id;
if (id) deleteSubscriber(id);
});
}
async function loadSubscribers() {
@@ -453,7 +468,7 @@
<span class="status-badge ${statusClass}">${subscriber.status}</span>
</td>
<td>
<button class="action-btn delete" onclick="deleteSubscriber('${subscriber.id}')">
<button class="action-btn delete" data-action="delete-subscriber" data-id="${MSPE.escapeHtml(subscriber.id || '')}">
<i class="fas fa-trash"></i>
</button>
</td>
+39 -7
View File
@@ -112,6 +112,12 @@
<span>Bookings</span>
</a>
</li>
<li class="nav-item">
<a href="/admin/availability.html" class="nav-link">
<i class="fas fa-clock"></i>
<span>Availability</span>
</a>
</li>
</ul>
</div>
@@ -154,7 +160,7 @@
</div>
<div class="header-right">
<button class="btn btn-primary" onclick="openAddMemberModal()">
<button class="btn btn-primary" id="add-member-btn">
<i class="fas fa-plus"></i> Add Team Member
</button>
@@ -216,7 +222,7 @@
<div class="modal-container">
<div class="modal-header">
<h2 id="modal-title">Add Team Member</h2>
<button class="modal-close" onclick="closeModal()">&times;</button>
<button class="modal-close" id="member-modal-close">&times;</button>
</div>
<div class="modal-body">
<form id="member-form">
@@ -298,8 +304,8 @@
</form>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="closeModal()">Cancel</button>
<button class="btn btn-primary" onclick="saveMember()">
<button class="btn btn-outline" id="member-cancel-btn">Cancel</button>
<button class="btn btn-primary" id="member-save-btn">
<i class="fas fa-save"></i> Save Member
</button>
</div>
@@ -311,6 +317,7 @@
let teamMembers = [];
document.addEventListener('DOMContentLoaded', function() {
setupEventListeners();
loadTeam();
document.getElementById('department-filter').addEventListener('change', filterTeam);
@@ -329,6 +336,31 @@
});
});
function setupEventListeners() {
document.getElementById('add-member-btn')?.addEventListener('click', openAddMemberModal);
document.getElementById('member-modal-close')?.addEventListener('click', closeModal);
document.getElementById('member-cancel-btn')?.addEventListener('click', closeModal);
document.getElementById('member-save-btn')?.addEventListener('click', saveMember);
const modalBackdrop = document.querySelector('#member-modal .modal-backdrop');
modalBackdrop?.addEventListener('click', closeModal);
const grid = document.getElementById('team-grid');
grid?.addEventListener('click', function(event) {
const actionBtn = event.target.closest('[data-action]');
if (!actionBtn) return;
const { action, id } = actionBtn.dataset;
if (action === 'edit' && id) {
editMember(id);
} else if (action === 'delete' && id) {
deleteMember(id);
} else if (action === 'change-photo' && id) {
changePhoto(id);
}
});
}
async function loadTeam() {
try {
const response = await MSPE.API.get('team.php');
@@ -364,7 +396,7 @@
<div class="team-member-photo">
<img src="${member.photo || 'https://ui-avatars.com/api/?name=' + encodeURIComponent(member.name) + '&background=random'}" alt="${member.name}">
<div class="photo-overlay">
<button class="btn btn-sm btn-outline" onclick="editMember('${member.id}')">
<button class="btn btn-sm btn-outline" data-action="change-photo" data-id="${member.id}">
<i class="fas fa-camera"></i> Change
</button>
</div>
@@ -380,10 +412,10 @@
${member.email ? `<a href="mailto:${member.email}" class="social-icon email"><i class="fas fa-envelope"></i></a>` : ''}
</div>
<div class="team-admin-actions">
<button class="btn btn-sm btn-outline" onclick="editMember('${member.id}')">
<button class="btn btn-sm btn-outline" data-action="edit" data-id="${member.id}">
<i class="fas fa-edit"></i> Edit
</button>
<button class="btn btn-sm btn-danger" onclick="deleteMember('${member.id}')">
<button class="btn btn-sm btn-danger" data-action="delete" data-id="${member.id}">
<i class="fas fa-trash"></i>
</button>
</div>
+40 -8
View File
@@ -112,6 +112,12 @@
<span>Bookings</span>
</a>
</li>
<li class="nav-item">
<a href="/admin/availability.html" class="nav-link">
<i class="fas fa-clock"></i>
<span>Availability</span>
</a>
</li>
</ul>
</div>
@@ -154,7 +160,7 @@
</div>
<div class="header-right">
<button class="btn btn-primary" onclick="openAddTestimonialModal()">
<button class="btn btn-primary" id="add-testimonial-btn">
<i class="fas fa-plus"></i> Add Testimonial
</button>
@@ -243,7 +249,7 @@
<div class="modal-container">
<div class="modal-header">
<h2 id="modal-title">Add Testimonial</h2>
<button class="modal-close" onclick="closeModal()">&times;</button>
<button class="modal-close" id="testimonial-modal-close">&times;</button>
</div>
<div class="modal-body">
<form id="testimonial-form">
@@ -317,8 +323,8 @@
</form>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="closeModal()">Cancel</button>
<button class="btn btn-primary" onclick="saveTestimonial()">
<button class="btn btn-outline" id="testimonial-cancel-btn">Cancel</button>
<button class="btn btn-primary" id="testimonial-save-btn">
<i class="fas fa-save"></i> Save Testimonial
</button>
</div>
@@ -330,9 +336,35 @@
let allTestimonials = [];
document.addEventListener('DOMContentLoaded', function() {
setupEventListeners();
loadTestimonials();
});
function setupEventListeners() {
document.getElementById('add-testimonial-btn')?.addEventListener('click', openAddTestimonialModal);
document.getElementById('testimonial-modal-close')?.addEventListener('click', closeModal);
document.getElementById('testimonial-cancel-btn')?.addEventListener('click', closeModal);
document.getElementById('testimonial-save-btn')?.addEventListener('click', saveTestimonial);
const modalBackdrop = document.querySelector('#testimonial-modal .modal-backdrop');
modalBackdrop?.addEventListener('click', closeModal);
const grid = document.getElementById('testimonials-grid');
grid?.addEventListener('click', function(event) {
const actionBtn = event.target.closest('[data-action]');
if (!actionBtn) return;
const { action, id, status } = actionBtn.dataset;
if (action === 'update-status' && id && status) {
updateStatus(id, status);
} else if (action === 'edit' && id) {
editTestimonial(id);
} else if (action === 'delete' && id) {
deleteTestimonial(id);
}
});
}
async function loadTestimonials() {
try {
const response = await MSPE.API.get('testimonials.php');
@@ -387,18 +419,18 @@
</div>
<div class="testimonial-admin-actions">
${item.status === 'pending' ? `
<button class="btn btn-sm btn-primary" onclick="updateStatus('${item.id}', 'published')">
<button class="btn btn-sm btn-primary" data-action="update-status" data-id="${item.id}" data-status="published">
<i class="fas fa-check"></i> Approve
</button>
` : `
<button class="btn btn-sm btn-outline" onclick="updateStatus('${item.id}', 'pending')">
<button class="btn btn-sm btn-outline" data-action="update-status" data-id="${item.id}" data-status="pending">
<i class="fas fa-eye-slash"></i> Unpublish
</button>
`}
<button class="btn btn-sm btn-outline" onclick="editTestimonial('${item.id}')">
<button class="btn btn-sm btn-outline" data-action="edit" data-id="${item.id}">
<i class="fas fa-edit"></i> Edit
</button>
<button class="btn btn-sm btn-danger" onclick="deleteTestimonial('${item.id}')">
<button class="btn btn-sm btn-danger" data-action="delete" data-id="${item.id}">
<i class="fas fa-trash"></i>
</button>
</div>
+97 -79
View File
@@ -112,6 +112,12 @@
<span>Bookings</span>
</a>
</li>
<li class="nav-item">
<a href="/admin/availability.html" class="nav-link">
<i class="fas fa-clock"></i>
<span>Availability</span>
</a>
</li>
</ul>
</div>
@@ -154,7 +160,7 @@
</div>
<div class="header-right">
<button class="btn btn-primary" onclick="openAddUserModal()">
<button class="btn btn-primary" id="add-user-btn">
<i class="fas fa-user-plus"></i> Add User
</button>
@@ -223,8 +229,8 @@
<div class="roles-grid">
<div class="role-card">
<div class="role-header">
<span class="role-badge super-admin">Super Admin</span>
<span class="role-count">1 user</span>
<span class="role-badge admin">Administrator</span>
<span class="role-count" id="count-admin"> users</span>
</div>
<ul class="permissions-list">
<li><i class="fas fa-check"></i> Full system access</li>
@@ -238,7 +244,7 @@
<div class="role-card">
<div class="role-header">
<span class="role-badge editor">Editor</span>
<span class="role-count">1 user</span>
<span class="role-count" id="count-editor"> users</span>
</div>
<ul class="permissions-list">
<li><i class="fas fa-check"></i> Create/edit content</li>
@@ -248,20 +254,6 @@
<li><i class="fas fa-times"></i> Site settings</li>
</ul>
</div>
<div class="role-card">
<div class="role-header">
<span class="role-badge contributor">Contributor</span>
<span class="role-count">1 user</span>
</div>
<ul class="permissions-list">
<li><i class="fas fa-check"></i> Create content</li>
<li><i class="fas fa-times"></i> Publish content</li>
<li><i class="fas fa-check"></i> Upload media</li>
<li><i class="fas fa-times"></i> Delete content</li>
<li><i class="fas fa-times"></i> Site settings</li>
</ul>
</div>
</div>
</div>
</div>
@@ -270,48 +262,12 @@
<div class="card" style="margin-top: 2rem;">
<div class="card-header">
<h3><i class="fas fa-history"></i> Recent Activity</h3>
<a href="#" class="card-action">View All</a>
</div>
<div class="card-body">
<div class="activity-timeline">
<div class="activity-item">
<div class="activity-icon login">
<i class="fas fa-sign-in-alt"></i>
</div>
<div class="activity-content">
<p><strong>Super Admin</strong> logged in</p>
<span class="activity-time">10 minutes ago</span>
</div>
</div>
<div class="activity-item">
<div class="activity-icon edit">
<i class="fas fa-edit"></i>
</div>
<div class="activity-content">
<p><strong>Sarah Johnson</strong> edited <em>"New IT Security Features"</em></p>
<span class="activity-time">2 hours ago</span>
</div>
</div>
<div class="activity-item">
<div class="activity-icon create">
<i class="fas fa-plus"></i>
</div>
<div class="activity-content">
<p><strong>Sarah Johnson</strong> created new news article</p>
<span class="activity-time">Yesterday</span>
</div>
</div>
<div class="activity-item">
<div class="activity-icon settings">
<i class="fas fa-cog"></i>
</div>
<div class="activity-content">
<p><strong>Super Admin</strong> updated site settings</p>
<span class="activity-time">2 days ago</span>
</div>
<div class="activity-timeline" id="activity-timeline">
<div class="empty-state" style="padding: 2rem; text-align: center;">
<i class="fas fa-history" style="font-size: 2rem; opacity: 0.3; margin-bottom: 0.5rem; display:block;"></i>
<p>Activity is recorded in the server error log under <code>[MSPE_AUDIT]</code> entries.</p>
</div>
</div>
</div>
@@ -326,7 +282,7 @@
<div class="modal-container">
<div class="modal-header">
<h2 id="modal-title">Add Admin User</h2>
<button class="modal-close" onclick="closeModal()">&times;</button>
<button class="modal-close" id="user-modal-close">&times;</button>
</div>
<div class="modal-body">
<form id="user-form">
@@ -361,9 +317,8 @@
<div class="form-group">
<label>Role *</label>
<select class="form-select" name="role" required>
<option value="contributor">Contributor</option>
<option value="editor">Editor</option>
<option value="super-admin">Super Admin</option>
<option value="admin">Administrator</option>
</select>
</div>
<div class="form-group">
@@ -384,8 +339,8 @@
</form>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="closeModal()">Cancel</button>
<button class="btn btn-primary" onclick="saveUser()">
<button class="btn btn-outline" id="user-cancel-btn">Cancel</button>
<button class="btn btn-primary" id="user-save-btn">
<i class="fas fa-save"></i> Save User
</button>
</div>
@@ -398,7 +353,7 @@
<div class="modal-container modal-sm">
<div class="modal-header">
<h2>Change Password</h2>
<button class="modal-close" onclick="closePasswordModal()">&times;</button>
<button class="modal-close" id="password-modal-close">&times;</button>
</div>
<div class="modal-body">
<form id="password-form">
@@ -419,8 +374,8 @@
</form>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="closePasswordModal()">Cancel</button>
<button class="btn btn-primary" onclick="updatePassword()">
<button class="btn btn-outline" id="password-cancel-btn">Cancel</button>
<button class="btn btn-primary" id="password-save-btn">
<i class="fas fa-key"></i> Update Password
</button>
</div>
@@ -432,15 +387,49 @@
let allUsers = [];
document.addEventListener('DOMContentLoaded', function() {
setupEventListeners();
loadUsers();
});
function setupEventListeners() {
document.getElementById('add-user-btn')?.addEventListener('click', openAddUserModal);
document.getElementById('user-modal-close')?.addEventListener('click', closeModal);
document.getElementById('user-cancel-btn')?.addEventListener('click', closeModal);
document.getElementById('user-save-btn')?.addEventListener('click', saveUser);
document.getElementById('password-modal-close')?.addEventListener('click', closePasswordModal);
document.getElementById('password-cancel-btn')?.addEventListener('click', closePasswordModal);
document.getElementById('password-save-btn')?.addEventListener('click', updatePassword);
const userBackdrop = document.querySelector('#user-modal .modal-backdrop');
userBackdrop?.addEventListener('click', closeModal);
const passwordBackdrop = document.querySelector('#password-modal .modal-backdrop');
passwordBackdrop?.addEventListener('click', closePasswordModal);
const tbody = document.querySelector('tbody');
tbody?.addEventListener('click', function(event) {
const actionBtn = event.target.closest('[data-action]');
if (!actionBtn) return;
const { action, id } = actionBtn.dataset;
if (action === 'edit' && id) {
editUser(id);
} else if (action === 'password' && id) {
changePassword(id);
} else if (action === 'delete' && id) {
deleteUser(id);
}
});
}
async function loadUsers() {
try {
const response = await MSPE.API.get('users.php');
if (response && response.success) {
allUsers = response.data;
renderUsers(allUsers);
updateRoleCounts(allUsers);
} else {
document.querySelector('tbody').innerHTML = '<tr><td colspan="6" class="text-center">Failed to load users</td></tr>';
}
@@ -450,6 +439,14 @@
}
}
function updateRoleCounts(users) {
const counts = users.reduce((acc, u) => { acc[u.role] = (acc[u.role] || 0) + 1; return acc; }, {});
const adminCount = document.getElementById('count-admin');
const editorCount = document.getElementById('count-editor');
if (adminCount) adminCount.textContent = `${counts['admin'] || 0} user(s)`;
if (editorCount) editorCount.textContent = `${counts['editor'] || 0} user(s)`;
}
function renderUsers(users) {
const tbody = document.querySelector('tbody');
if (users.length === 0) {
@@ -457,36 +454,43 @@
return;
}
tbody.innerHTML = users.map(user => `
tbody.innerHTML = users.map(user => {
const safeName = MSPE.escapeHtml(user.name);
const safeUsername = MSPE.escapeHtml(user.username);
const safeEmail = MSPE.escapeHtml(user.email);
const safeRole = MSPE.escapeHtml(user.role);
const safeStatus = MSPE.escapeHtml(user.status);
const safeId = MSPE.escapeHtml(user.id);
return `
<tr>
<td>
<div class="user-info">
<img src="https://ui-avatars.com/api/?name=${encodeURIComponent(user.name)}&background=random" alt="${user.name}">
<img src="https://ui-avatars.com/api/?name=${encodeURIComponent(user.name)}&background=random" alt="${safeName}">
<div>
<span class="name">${user.name}</span>
<span class="username">${user.username}</span>
<span class="name">${safeName}</span>
<span class="username">${safeUsername}</span>
</div>
</div>
</td>
<td>${user.email}</td>
<td><span class="role-badge ${user.role}">${user.role}</span></td>
<td>${safeEmail}</td>
<td><span class="role-badge ${safeRole}">${safeRole}</span></td>
<td>${user.last_login ? MSPE.formatDate(user.last_login) : 'Never'}</td>
<td><span class="status-badge ${user.status}">${user.status}</span></td>
<td><span class="status-badge ${safeStatus}">${safeStatus}</span></td>
<td>
<div class="action-buttons">
<button class="btn btn-sm btn-outline" onclick="editUser('${user.id}')">
<button class="btn btn-sm btn-outline" data-action="edit" data-id="${safeId}">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-outline" onclick="changePassword('${user.id}')">
<button class="btn btn-sm btn-outline" data-action="password" data-id="${safeId}">
<i class="fas fa-key"></i>
</button>
<button class="btn btn-sm btn-danger" onclick="deleteUser('${user.id}')">
<button class="btn btn-sm btn-danger" data-action="delete" data-id="${safeId}">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
`).join('');
</tr>`;
}).join('');
}
function openAddUserModal() {
@@ -552,7 +556,21 @@
const form = document.getElementById('user-form');
const formData = new FormData(form);
const id = form.dataset.id;
// Client-side password match check for new users
if (!id) {
const pw = form.querySelector('[name="password"]').value;
const cpw = form.querySelector('[name="password_confirm"]').value;
if (pw !== cpw) {
MSPE.showNotification('Passwords do not match', 'error');
return;
}
if (pw.length < 12) {
MSPE.showNotification('Password must be at least 12 characters', 'error');
return;
}
}
if (id) formData.append('id', id);
try {