Initial commit: MSPE website - full site with admin panel, API, and public pages
This commit is contained in:
Executable
+930
@@ -0,0 +1,930 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||
<title>Availability - MSPE Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha384-t1nt8BQoYMLFN5p42tRAtuAAFQaCQODekUVeKKZrEnEyp4H2R0RHFz0KWpmj7i8g" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/admin/css/admin.css">
|
||||
<style>
|
||||
.availability-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.add-slot-form {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
padding: 1.5rem;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.add-slot-form h3 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.875rem;
|
||||
font-family: var(--font-main);
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.1);
|
||||
}
|
||||
|
||||
.quick-slots {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.quick-slots label {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.quick-slots-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.quick-slot-btn {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--gray-300);
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.quick-slot-btn:hover {
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.bulk-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.bulk-actions button {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.slots-list {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.slots-list-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.slots-list-header h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.slots-filters {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.slot-date-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.slots-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.slots-table th {
|
||||
background: var(--gray-100);
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: var(--gray-600);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.slots-table td {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.slots-table tr:hover {
|
||||
background: var(--gray-50);
|
||||
}
|
||||
|
||||
.slot-time {
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.slot-capacity {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.slot-capacity i {
|
||||
font-size: 0.75rem;
|
||||
color: var(--gray-400);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
margin-right: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-btn.delete {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.action-btn.delete:hover {
|
||||
background: #fecaca;
|
||||
}
|
||||
|
||||
.action-btn.toggle {
|
||||
background: #e0f2fe;
|
||||
color: #0369a1;
|
||||
}
|
||||
|
||||
.action-btn.toggle:hover {
|
||||
background: #bae6fd;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 4rem 2rem;
|
||||
text-align: center;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.upcoming-section {
|
||||
background: var(--dark);
|
||||
border-radius: 10px;
|
||||
padding: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.upcoming-section h4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.upcoming-bookings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.upcoming-item {
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.upcoming-item-date {
|
||||
background: var(--primary);
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
text-align: center;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.upcoming-item-date .day {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.upcoming-item-date .month {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.availability-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.quick-slots-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-wrapper">
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<a href="/admin/dashboard.html" class="sidebar-logo">
|
||||
<i class="fas fa-cube"></i>
|
||||
<span>MSPE</span>
|
||||
</a>
|
||||
<button class="sidebar-toggle" id="sidebar-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Main</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/dashboard.html" class="nav-link">
|
||||
<i class="fas fa-th-large"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Content</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/pages.html" class="nav-link">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
<span>Pages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/services.html" class="nav-link">
|
||||
<i class="fas fa-concierge-bell"></i>
|
||||
<span>Services</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/portfolio.html" class="nav-link">
|
||||
<i class="fas fa-briefcase"></i>
|
||||
<span>Portfolio</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/news.html" class="nav-link">
|
||||
<i class="fas fa-newspaper"></i>
|
||||
<span>News & Events</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/testimonials.html" class="nav-link">
|
||||
<i class="fas fa-quote-right"></i>
|
||||
<span>Testimonials</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/team.html" class="nav-link">
|
||||
<i class="fas fa-users"></i>
|
||||
<span>Team</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Media</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/media.html" class="nav-link">
|
||||
<i class="fas fa-images"></i>
|
||||
<span>Media Library</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Inquiries</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/messages.html" class="nav-link">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<span>Messages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/subscribers.html" class="nav-link">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span>Subscribers</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/bookings.html" class="nav-link">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a href="/admin/availability.html" class="nav-link">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span>Availability</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Settings</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings.html" class="nav-link">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span>Site Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/users.html" class="nav-link">
|
||||
<i class="fas fa-user-shield"></i>
|
||||
<span>Admin Users</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<a href="/index.html" class="view-site-btn" target="_blank">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
<span>View Website</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="admin-header">
|
||||
<div class="header-left">
|
||||
<button class="mobile-toggle" id="mobile-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<h1 class="page-title">Manage Availability</h1>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div class="header-user">
|
||||
<div class="user-avatar">
|
||||
<img src="https://ui-avatars.com/api/?name=Admin&background=0ea5e9&color=fff" alt="Admin">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="availability-grid">
|
||||
<div class="add-slot-form">
|
||||
<h3><i class="fas fa-plus-circle" style="color: var(--primary); margin-right: 0.5rem;"></i> Add Time Slot</h3>
|
||||
|
||||
<form id="add-slot-form">
|
||||
<div class="form-group">
|
||||
<label>Date *</label>
|
||||
<input type="date" name="date" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Time *</label>
|
||||
<input type="time" name="time" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Capacity (number of clients)</label>
|
||||
<input type="number" name="capacity" value="1" min="1" max="10">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Notes</label>
|
||||
<textarea name="notes" rows="3" placeholder="Optional notes about this slot..."></textarea>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bulk-actions">
|
||||
<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()">
|
||||
<i class="fas fa-calendar-week"></i> Generate Week
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Block Time Section -->
|
||||
<div style="margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--gray-200);">
|
||||
<h3 style="margin-bottom: 1rem;"><i class="fas fa-ban" style="color: #ef4444; margin-right: 0.5rem;"></i> Block Time</h3>
|
||||
<p style="font-size: 0.875rem; color: var(--gray-500); margin-bottom: 1rem;">Block a time slot to make it unavailable for public booking (vacation, personal time, etc.)</p>
|
||||
|
||||
<form id="block-time-form">
|
||||
<div class="form-group">
|
||||
<label>Date *</label>
|
||||
<input type="date" name="block_date" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Time *</label>
|
||||
<input type="time" name="block_time" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Reason (internal note)</label>
|
||||
<input type="text" name="block_reason" placeholder="e.g., Personal appointment">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-secondary" style="width: 100%; background: #fee2e2; color: #dc2626; border-color: #ef4444;">
|
||||
<i class="fas fa-ban"></i> Block This Time
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="upcoming-section" id="upcoming-section" style="display: none;">
|
||||
<h4><i class="fas fa-clock"></i> Upcoming Bookings</h4>
|
||||
<div class="upcoming-bookings" id="upcoming-bookings"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="slots-list">
|
||||
<div class="slots-list-header">
|
||||
<h3>Availability Slots</h3>
|
||||
<div class="slots-filters">
|
||||
<div class="slot-date-filter">
|
||||
<label>From:</label>
|
||||
<input type="date" id="filter-date-from" style="padding: 0.5rem;">
|
||||
</div>
|
||||
<div class="slot-date-filter">
|
||||
<label>To:</label>
|
||||
<input type="date" id="filter-date-to" style="padding: 0.5rem;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blocked Times Section -->
|
||||
<div id="blocked-times-section" style="display: none; background: #fef2f2; border-bottom: 1px solid #fecaca;">
|
||||
<div style="padding: 1rem 1.5rem; display: flex; justify-content: space-between; align-items: center;">
|
||||
<h4 style="margin: 0; color: #dc2626;"><i class="fas fa-ban" style="margin-right: 0.5rem;"></i> Blocked Times</h4>
|
||||
</div>
|
||||
<div id="blocked-times-list" style="padding: 0 1.5rem 1rem;"></div>
|
||||
</div>
|
||||
|
||||
<div class="slots-table-container">
|
||||
<table class="slots-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Time</th>
|
||||
<th>Capacity</th>
|
||||
<th>Status</th>
|
||||
<th>Notes</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="slots-table-body">
|
||||
<tr>
|
||||
<td colspan="6">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
<p>Loading availability slots...</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/admin/js/admin.js"></script>
|
||||
<script>
|
||||
let currentSlots = [];
|
||||
let blockedTimes = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadSlots();
|
||||
loadBlockedTimes();
|
||||
loadUpcomingBookings();
|
||||
setupEventListeners();
|
||||
setDefaultDate();
|
||||
});
|
||||
|
||||
function setupEventListeners() {
|
||||
document.getElementById('add-slot-form').addEventListener('submit', handleAddSlot);
|
||||
document.getElementById('block-time-form').addEventListener('submit', handleBlockTime);
|
||||
document.getElementById('filter-date-from').addEventListener('change', filterSlots);
|
||||
document.getElementById('filter-date-to').addEventListener('change', filterSlots);
|
||||
}
|
||||
|
||||
function setDefaultDate() {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
document.querySelector('input[name="date"]').value = today;
|
||||
document.querySelector('input[name="date"]').min = today;
|
||||
document.querySelector('input[name="block_date"]').value = today;
|
||||
document.querySelector('input[name="block_date"]').min = today;
|
||||
}
|
||||
|
||||
function addQuickTime(time) {
|
||||
document.querySelector('input[name="time"]').value = time;
|
||||
}
|
||||
|
||||
async function handleBlockTime(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const blockData = {
|
||||
date: form.block_date.value,
|
||||
time: form.block_time.value,
|
||||
reason: form.block_reason.value || 'No reason provided'
|
||||
};
|
||||
|
||||
if (!blockData.date || !blockData.time) {
|
||||
MSPE.showNotification('Please select date and time', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
const originalText = submitBtn.innerHTML;
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Blocking...';
|
||||
|
||||
try {
|
||||
const response = await MSPE.API.post('bookings.php?action=block-time', blockData);
|
||||
MSPE.showNotification('Time blocked successfully!', 'success');
|
||||
form.reset();
|
||||
setDefaultDate();
|
||||
loadBlockedTimes();
|
||||
} catch (error) {
|
||||
console.error('Error blocking time:', error);
|
||||
MSPE.showNotification('Failed to block time', 'error');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBlockedTimes() {
|
||||
try {
|
||||
const response = await MSPE.API.get('bookings.php?action=blocked-times');
|
||||
blockedTimes = response.data || [];
|
||||
renderBlockedTimes();
|
||||
} catch (error) {
|
||||
console.error('Error loading blocked times:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderBlockedTimes() {
|
||||
const section = document.getElementById('blocked-times-section');
|
||||
const container = document.getElementById('blocked-times-list');
|
||||
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const futureBlocked = blockedTimes.filter(b => b.date >= today);
|
||||
|
||||
if (futureBlocked.length === 0) {
|
||||
section.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
section.style.display = 'block';
|
||||
|
||||
container.innerHTML = futureBlocked.map(blocked => {
|
||||
const date = new Date(blocked.date);
|
||||
const formattedDate = date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
|
||||
const formattedTime = new Date(`${blocked.date}T${blocked.time}`).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
|
||||
|
||||
return `
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; background: white; padding: 0.75rem 1rem; border-radius: 8px; margin-bottom: 0.5rem; border: 1px solid #fecaca;">
|
||||
<div>
|
||||
<span style="font-weight: 600; color: #dc2626;">${formattedDate}</span>
|
||||
<span style="color: #64748b; margin: 0 0.5rem;">at</span>
|
||||
<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;">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function unblockTime(id) {
|
||||
if (!await MSPE.confirmDialog('Remove this blocked time?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await MSPE.API.delete(`bookings.php?id=${id}&type=blocked`);
|
||||
MSPE.showNotification('Time unblocked successfully!', 'success');
|
||||
loadBlockedTimes();
|
||||
} catch (error) {
|
||||
console.error('Error unblocking time:', error);
|
||||
MSPE.showNotification('Failed to unblock time', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddSlot(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const data = new FormData(form);
|
||||
const slotData = Object.fromEntries(data);
|
||||
|
||||
if (!slotData.date || !slotData.time) {
|
||||
MSPE.showNotification('Please select date and time', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
const originalText = submitBtn.innerHTML;
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Adding...';
|
||||
|
||||
try {
|
||||
const response = await MSPE.API.post('bookings.php?action=create-slot', slotData);
|
||||
MSPE.showNotification('Time slot added successfully!', 'success');
|
||||
form.reset();
|
||||
setDefaultDate();
|
||||
loadSlots();
|
||||
} catch (error) {
|
||||
console.error('Error adding slot:', error);
|
||||
MSPE.showNotification('Failed to add time slot', 'error');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
async function generateWeekSlots() {
|
||||
const today = new Date();
|
||||
const slotsToAdd = [];
|
||||
const times = ['09:00', '10:00', '11:00', '13:00', '14:00', '15:00', '16:00', '17:00'];
|
||||
|
||||
for (let i = 1; i <= 7; i++) {
|
||||
const date = new Date(today);
|
||||
date.setDate(date.getDate() + i);
|
||||
const dateStr = date.toISOString().split('T')[0];
|
||||
|
||||
times.forEach(time => {
|
||||
slotsToAdd.push({
|
||||
date: dateStr,
|
||||
time: time,
|
||||
capacity: 1
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!await MSPE.confirmDialog(`This will add ${slotsToAdd.length} time slots for the next 7 days. Continue?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let added = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const slot of slotsToAdd) {
|
||||
try {
|
||||
await MSPE.API.post('bookings.php?action=create-slot', slot);
|
||||
added++;
|
||||
} catch (error) {
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
if (added > 0) {
|
||||
MSPE.showNotification(`${added} time slots added successfully!`, 'success');
|
||||
}
|
||||
|
||||
if (failed > 0) {
|
||||
MSPE.showNotification(`${failed} slots failed to add`, 'error');
|
||||
}
|
||||
|
||||
loadSlots();
|
||||
}
|
||||
|
||||
async function loadSlots() {
|
||||
try {
|
||||
const result = await MSPE.API.get('bookings.php');
|
||||
currentSlots = result.data || [];
|
||||
renderSlots();
|
||||
} catch (error) {
|
||||
console.error('Error loading slots:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderSlots() {
|
||||
const tbody = document.getElementById('slots-table-body');
|
||||
|
||||
const dateFrom = document.getElementById('filter-date-from').value;
|
||||
const dateTo = document.getElementById('filter-date-to').value;
|
||||
|
||||
let filteredSlots = [...currentSlots];
|
||||
|
||||
if (dateFrom) {
|
||||
filteredSlots = filteredSlots.filter(s => s.date >= dateFrom);
|
||||
}
|
||||
|
||||
if (dateTo) {
|
||||
filteredSlots = filteredSlots.filter(s => s.date <= dateTo);
|
||||
}
|
||||
|
||||
if (filteredSlots.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-calendar-times"></i>
|
||||
<p>No availability slots found</p>
|
||||
<p style="font-size: 0.875rem; margin-top: 0.5rem;">Add time slots using the form on the left</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
filteredSlots.sort((a, b) => {
|
||||
return new Date(`${a.date}T${a.time}`) - new Date(`${b.date}T${b.time}`);
|
||||
});
|
||||
|
||||
tbody.innerHTML = filteredSlots.map(slot => {
|
||||
const date = new Date(slot.date);
|
||||
const formattedDate = date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
|
||||
const formattedTime = new Date(`${slot.date}T${slot.time}`).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
|
||||
const isAvailable = slot.is_available !== false;
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>
|
||||
<div style="font-weight: 600;">${formattedDate}</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="slot-time">${formattedTime}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="slot-capacity">
|
||||
<i class="fas fa-users"></i> ${slot.capacity}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-badge ${isAvailable ? 'status-confirmed' : 'status-cancelled'}">
|
||||
${isAvailable ? 'Available' : 'Disabled'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span style="color: var(--gray-500);">${slot.notes || '-'}</span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="action-btn toggle" onclick="toggleSlot('${slot.id}')">
|
||||
<i class="fas fa-${isAvailable ? 'eye-slash' : 'eye'}"></i>
|
||||
</button>
|
||||
<button class="action-btn delete" onclick="deleteSlot('${slot.id}')">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function filterSlots() {
|
||||
renderSlots();
|
||||
}
|
||||
|
||||
async function toggleSlot(id) {
|
||||
const slot = currentSlots.find(s => s.id === id);
|
||||
if (!slot) return;
|
||||
|
||||
const newStatus = slot.is_available === false ? true : false;
|
||||
|
||||
if (!await MSPE.confirmDialog(`Are you sure you want to ${newStatus ? 'enable' : 'disable'} this slot?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await MSPE.API.put(`bookings.php?id=${id}`, { is_available: newStatus });
|
||||
MSPE.showNotification(`Slot ${newStatus ? 'enabled' : 'disabled'} successfully!`, 'success');
|
||||
loadSlots();
|
||||
} catch (error) {
|
||||
console.error('Error toggling slot:', error);
|
||||
MSPE.showNotification('Failed to update slot', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSlot(id) {
|
||||
if (!await MSPE.confirmDialog('Are you sure you want to delete this availability slot?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await MSPE.API.delete(`bookings.php?id=${id}`);
|
||||
MSPE.showNotification('Slot deleted successfully!', 'success');
|
||||
loadSlots();
|
||||
} catch (error) {
|
||||
console.error('Error deleting slot:', error);
|
||||
MSPE.showNotification('Failed to delete slot', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUpcomingBookings() {
|
||||
try {
|
||||
const response = await MSPE.API.get('bookings.php?action=upcoming');
|
||||
const bookings = response.data || [];
|
||||
|
||||
if (bookings.length === 0) {
|
||||
document.getElementById('upcoming-section').style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('upcoming-section').style.display = 'block';
|
||||
|
||||
const container = document.getElementById('upcoming-bookings');
|
||||
container.innerHTML = bookings.slice(0, 5).map(booking => {
|
||||
const date = new Date(booking.booking_date);
|
||||
const day = date.getDate();
|
||||
const month = date.toLocaleDateString('en-US', { month: 'short' });
|
||||
const formattedTime = new Date(`${booking.booking_date}T${booking.booking_time}`).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
|
||||
|
||||
return `
|
||||
<div class="upcoming-item">
|
||||
<div class="upcoming-item-date">
|
||||
<div class="day">${day}</div>
|
||||
<div class="month">${month}</div>
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<div style="font-weight: 600;">${booking.first_name} ${booking.last_name}</div>
|
||||
<div style="font-size: 0.875rem; opacity: 0.8;">${formattedTime}</div>
|
||||
</div>
|
||||
<i class="fas fa-chevron-right" style="color: var(--gray-400);"></i>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
} catch (error) {
|
||||
console.error('Error loading upcoming bookings:', error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+732
@@ -0,0 +1,732 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||
<title>Bookings - MSPE Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha384-t1nt8BQoYMLFN5p42tRAtuAAFQaCQODekUVeKKZrEnEyp4H2R0RHFz0KWpmj7i8g" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/admin/css/admin.css">
|
||||
<style>
|
||||
.bookings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.bookings-list {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.bookings-filters {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-600);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.filter-group select,
|
||||
.filter-group input {
|
||||
padding: 0.625rem;
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.bookings-table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.bookings-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.bookings-table th {
|
||||
background: var(--gray-100);
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: var(--gray-600);
|
||||
font-size: 0.875rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bookings-table td {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.bookings-table tr:hover {
|
||||
background: var(--gray-50);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.status-confirmed {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.status-cancelled {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-right: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-btn.view {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn.confirm {
|
||||
background: var(--success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn.cancel {
|
||||
background: var(--danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.booking-details {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
padding: 1.5rem;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.booking-details-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.booking-details-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.detail-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.detail-group label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-500);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
.detail-group .value {
|
||||
font-size: 1rem;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.detail-group .value.highlight {
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.booking-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 4rem 2rem;
|
||||
text-align: center;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.availability-section {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.availability-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.availability-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.add-slot-form {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.add-slot-form .form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.bookings-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-wrapper">
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<a href="/admin/dashboard.html" class="sidebar-logo">
|
||||
<i class="fas fa-cube"></i>
|
||||
<span>MSPE</span>
|
||||
</a>
|
||||
<button class="sidebar-toggle" id="sidebar-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Main</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/dashboard.html" class="nav-link">
|
||||
<i class="fas fa-th-large"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Content</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/pages.html" class="nav-link">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
<span>Pages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/services.html" class="nav-link">
|
||||
<i class="fas fa-concierge-bell"></i>
|
||||
<span>Services</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/portfolio.html" class="nav-link">
|
||||
<i class="fas fa-briefcase"></i>
|
||||
<span>Portfolio</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/news.html" class="nav-link">
|
||||
<i class="fas fa-newspaper"></i>
|
||||
<span>News & Events</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/testimonials.html" class="nav-link">
|
||||
<i class="fas fa-quote-right"></i>
|
||||
<span>Testimonials</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/team.html" class="nav-link">
|
||||
<i class="fas fa-users"></i>
|
||||
<span>Team</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Media</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/media.html" class="nav-link">
|
||||
<i class="fas fa-images"></i>
|
||||
<span>Media Library</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Inquiries</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/messages.html" class="nav-link">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<span>Messages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/subscribers.html" class="nav-link">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span>Subscribers</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a href="/admin/bookings.html" class="nav-link">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Settings</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings.html" class="nav-link">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span>Site Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/users.html" class="nav-link">
|
||||
<i class="fas fa-user-shield"></i>
|
||||
<span>Admin Users</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<a href="/index.html" class="view-site-btn" target="_blank">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
<span>View Website</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="admin-header">
|
||||
<div class="header-left">
|
||||
<button class="mobile-toggle" id="mobile-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<h1 class="page-title">Calendar Bookings</h1>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div class="header-search">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="text" placeholder="Search bookings..." id="bookings-search">
|
||||
</div>
|
||||
|
||||
<a href="../calendar.html" class="btn btn-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
View Public Calendar
|
||||
</a>
|
||||
|
||||
<div class="header-user">
|
||||
<div class="user-avatar">
|
||||
<img src="https://ui-avatars.com/api/?name=Admin&background=0ea5e9&color=fff" alt="Admin">
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<span class="user-name">Admin</span>
|
||||
<span class="user-role">Administrator</span>
|
||||
</div>
|
||||
<div class="user-dropdown">
|
||||
<button class="dropdown-toggle">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="/admin/settings.html"><i class="fas fa-cog"></i> Settings</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" id="logout-btn"><i class="fas fa-sign-out-alt"></i> Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="bookings-grid">
|
||||
<div class="bookings-list">
|
||||
<div class="bookings-filters">
|
||||
<div class="filter-group">
|
||||
<label>Status</label>
|
||||
<select id="filter-status">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="confirmed">Confirmed</option>
|
||||
<option value="cancelled">Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>Date From</label>
|
||||
<input type="date" id="filter-date-from">
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>Date To</label>
|
||||
<input type="date" id="filter-date-to">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bookings-table-container">
|
||||
<table class="bookings-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Time</th>
|
||||
<th>Client</th>
|
||||
<th>Email</th>
|
||||
<th>Service</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="bookings-table-body">
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
<p>Loading bookings...</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="booking-details" id="booking-details">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-hand-pointer"></i>
|
||||
<p>Select a booking to view details</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/admin/js/admin.js"></script>
|
||||
<script>
|
||||
let currentBookings = [];
|
||||
let selectedBooking = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadBookings();
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
function setupEventListeners() {
|
||||
document.getElementById('filter-status').addEventListener('change', loadBookings);
|
||||
document.getElementById('filter-date-from').addEventListener('change', loadBookings);
|
||||
document.getElementById('filter-date-to').addEventListener('change', loadBookings);
|
||||
document.getElementById('bookings-search').addEventListener('input', filterBookings);
|
||||
}
|
||||
|
||||
async function loadBookings() {
|
||||
const status = document.getElementById('filter-status').value;
|
||||
const dateFrom = document.getElementById('filter-date-from').value;
|
||||
const dateTo = document.getElementById('filter-date-to').value;
|
||||
|
||||
let url = 'bookings.php';
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (status) params.append('status', status);
|
||||
if (dateFrom) params.append('date_from', dateFrom);
|
||||
if (dateTo) params.append('date_to', dateTo);
|
||||
|
||||
if (params.toString()) {
|
||||
url += '?' + params.toString();
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await MSPE.API.get(url);
|
||||
currentBookings = response.data || [];
|
||||
renderBookings();
|
||||
} catch (error) {
|
||||
console.error('Error loading bookings:', error);
|
||||
MSPE.showNotification('Failed to load bookings', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderBookings() {
|
||||
const tbody = document.getElementById('bookings-table-body');
|
||||
|
||||
if (currentBookings.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-calendar-times"></i>
|
||||
<p>No bookings found</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = currentBookings.map(booking => {
|
||||
const date = new Date(booking.booking_date);
|
||||
const formattedDate = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
const formattedTime = new Date(`${booking.booking_date}T${booking.booking_time}`).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
|
||||
const statusClass = `status-${booking.status}`;
|
||||
|
||||
return `
|
||||
<tr class="booking-row" data-id="${booking.id}" onclick="selectBooking('${booking.id}')">
|
||||
<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>
|
||||
</td>
|
||||
<td style="font-weight: 600;">${formattedTime}</td>
|
||||
<td>
|
||||
<div>${booking.first_name} ${booking.last_name}</div>
|
||||
${booking.company ? `<div style="font-size: 0.75rem; color: var(--gray-500);">${booking.company}</div>` : ''}
|
||||
</td>
|
||||
<td>${booking.email}</td>
|
||||
<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}')">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
${booking.status === 'pending' ? `
|
||||
<button class="action-btn confirm" onclick="event.stopPropagation(); confirmBooking('${booking.id}')">
|
||||
<i class="fas fa-check"></i>
|
||||
</button>
|
||||
<button class="action-btn cancel" onclick="event.stopPropagation(); cancelBooking('${booking.id}')">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function selectBooking(id) {
|
||||
selectedBooking = currentBookings.find(b => b.id === id);
|
||||
|
||||
if (!selectedBooking) return;
|
||||
|
||||
const container = document.getElementById('booking-details');
|
||||
const date = new Date(selectedBooking.booking_date);
|
||||
const formattedDate = date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' });
|
||||
const formattedTime = new Date(`${selectedBooking.booking_date}T${selectedBooking.booking_time}`).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="booking-details-header">
|
||||
<h3>Booking Details</h3>
|
||||
<span class="status-badge status-${selectedBooking.status}">${selectedBooking.status}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-group">
|
||||
<label>Booking ID</label>
|
||||
<div class="value">${selectedBooking.id}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-group">
|
||||
<label>Client Name</label>
|
||||
<div class="value highlight">${selectedBooking.first_name} ${selectedBooking.last_name}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-group">
|
||||
<label>Email</label>
|
||||
<div class="value">
|
||||
<a href="mailto:${selectedBooking.email}" style="color: var(--primary);">${selectedBooking.email}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-group">
|
||||
<label>Phone</label>
|
||||
<div class="value">${selectedBooking.phone || 'Not provided'}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-group">
|
||||
<label>Company</label>
|
||||
<div class="value">${selectedBooking.company || 'Not provided'}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-group">
|
||||
<label>Date & Time</label>
|
||||
<div class="value highlight">${formattedDate} at ${formattedTime}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-group">
|
||||
<label>Duration</label>
|
||||
<div class="value">${selectedBooking.duration} minutes</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-group">
|
||||
<label>Service Interest</label>
|
||||
<div class="value">${formatService(selectedBooking.service_interest)}</div>
|
||||
</div>
|
||||
|
||||
${selectedBooking.message ? `
|
||||
<div class="detail-group">
|
||||
<label>Message</label>
|
||||
<div class="value" style="line-height: 1.6;">${selectedBooking.message}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="detail-group">
|
||||
<label>Submitted</label>
|
||||
<div class="value">${MSPE.formatDate(selectedBooking.created_at)}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-group">
|
||||
<label>IP Address</label>
|
||||
<div class="value">${selectedBooking.ip_address || 'N/A'}</div>
|
||||
</div>
|
||||
|
||||
${selectedBooking.status === 'pending' ? `
|
||||
<div class="booking-actions">
|
||||
<button class="btn btn-success" onclick="confirmBooking('${selectedBooking.id}')">
|
||||
<i class="fas fa-check"></i> Confirm Booking
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="cancelBooking('${selectedBooking.id}')">
|
||||
<i class="fas fa-times"></i> Cancel Booking
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
async function confirmBooking(id) {
|
||||
if (!await MSPE.confirmDialog('Are you sure you want to confirm this booking? A confirmation email will be sent to the client.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await MSPE.API.put(`bookings.php?id=${id}`, { status: 'confirmed' });
|
||||
MSPE.showNotification('Booking confirmed successfully!', 'success');
|
||||
loadBookings();
|
||||
if (selectedBooking && selectedBooking.id === id) {
|
||||
selectBooking(id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error confirming booking:', error);
|
||||
MSPE.showNotification('Failed to confirm booking', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelBooking(id) {
|
||||
if (!await MSPE.confirmDialog('Are you sure you want to cancel this booking? A cancellation notification will be sent to the client.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await MSPE.API.put(`bookings.php?id=${id}`, { status: 'cancelled' });
|
||||
MSPE.showNotification('Booking cancelled successfully!', 'success');
|
||||
loadBookings();
|
||||
if (selectedBooking && selectedBooking.id === id) {
|
||||
selectBooking(id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error cancelling booking:', error);
|
||||
MSPE.showNotification('Failed to cancel booking', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function formatService(service) {
|
||||
const services = {
|
||||
'it-support': 'IT Support',
|
||||
'cybersecurity': 'Cybersecurity',
|
||||
'cloud': 'Cloud Services',
|
||||
'consulting': 'Business Consulting',
|
||||
'general': 'General Consultation'
|
||||
};
|
||||
return services[service] || service || 'Not specified';
|
||||
}
|
||||
|
||||
function filterBookings() {
|
||||
const searchTerm = document.getElementById('bookings-search').value.toLowerCase();
|
||||
const tbody = document.getElementById('bookings-table-body');
|
||||
const rows = tbody.querySelectorAll('.booking-row');
|
||||
|
||||
rows.forEach(row => {
|
||||
const id = row.dataset.id;
|
||||
const booking = currentBookings.find(b => b.id === id);
|
||||
|
||||
if (!booking) return;
|
||||
|
||||
const searchableText = `
|
||||
${booking.first_name} ${booking.last_name}
|
||||
${booking.email}
|
||||
${booking.company}
|
||||
${booking.service_interest}
|
||||
`.toLowerCase();
|
||||
|
||||
if (searchableText.includes(searchTerm)) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+3334
File diff suppressed because it is too large
Load Diff
Executable
+429
@@ -0,0 +1,429 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||
<title>Dashboard - MSPE Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha384-t1nt8BQoYMLFN5p42tRAtuAAFQaCQODekUVeKKZrEnEyp4H2R0RHFz0KWpmj7i8g" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/admin/css/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-wrapper">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<a href="/admin/dashboard.html" class="sidebar-logo">
|
||||
<i class="fas fa-cube"></i>
|
||||
<span>MSPE</span>
|
||||
</a>
|
||||
<button class="sidebar-toggle" id="sidebar-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Main</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item active">
|
||||
<a href="/admin/dashboard.html" class="nav-link">
|
||||
<i class="fas fa-th-large"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Content</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/pages.html" class="nav-link">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
<span>Pages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/services.html" class="nav-link">
|
||||
<i class="fas fa-concierge-bell"></i>
|
||||
<span>Services</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/portfolio.html" class="nav-link">
|
||||
<i class="fas fa-briefcase"></i>
|
||||
<span>Portfolio</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/news.html" class="nav-link">
|
||||
<i class="fas fa-newspaper"></i>
|
||||
<span>News & Events</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/testimonials.html" class="nav-link">
|
||||
<i class="fas fa-quote-right"></i>
|
||||
<span>Testimonials</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/team.html" class="nav-link">
|
||||
<i class="fas fa-users"></i>
|
||||
<span>Team</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Media</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/media.html" class="nav-link">
|
||||
<i class="fas fa-images"></i>
|
||||
<span>Media Library</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Inquiries</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/messages.html" class="nav-link">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<span>Messages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/subscribers.html" class="nav-link">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span>Subscribers</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/bookings.html" class="nav-link">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Settings</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings.html" class="nav-link">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span>Site Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/users.html" class="nav-link">
|
||||
<i class="fas fa-user-shield"></i>
|
||||
<span>Admin Users</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<a href="/index.html" class="view-site-btn" target="_blank">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
<span>View Website</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<!-- Header -->
|
||||
<header class="admin-header">
|
||||
<div class="header-left">
|
||||
<button class="mobile-toggle" id="mobile-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<h1 class="page-title">Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div class="header-search">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="text" placeholder="Search...">
|
||||
</div>
|
||||
|
||||
<div class="header-notifications">
|
||||
<button class="notification-btn">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span class="notification-badge">3</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="header-user">
|
||||
<div class="user-avatar">
|
||||
<img src="https://ui-avatars.com/api/?name=Admin&background=0ea5e9&color=fff" alt="Admin">
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<span class="user-name">Admin</span>
|
||||
<span class="user-role">Administrator</span>
|
||||
</div>
|
||||
<div class="user-dropdown">
|
||||
<button class="dropdown-toggle">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="settings.html"><i class="fas fa-user"></i> Profile</a></li>
|
||||
<li><a href="settings.html"><i class="fas fa-cog"></i> Settings</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" id="logout-btn"><i class="fas fa-sign-out-alt"></i> Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Dashboard Content -->
|
||||
<div class="content-wrapper">
|
||||
<!-- Stats Cards -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card" id="stat-messages">
|
||||
<div class="stat-icon bg-blue">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3 id="stat-messages-count">0</h3>
|
||||
<p>Unread Messages</p>
|
||||
</div>
|
||||
<div class="stat-trend" id="stat-messages-trend">
|
||||
<span id="stat-messages-total">0 total</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card" id="stat-bookings">
|
||||
<div class="stat-icon bg-green">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3 id="stat-bookings-count">0</h3>
|
||||
<p>Pending Bookings</p>
|
||||
</div>
|
||||
<div class="stat-trend" id="stat-bookings-trend">
|
||||
<span id="stat-bookings-week">0 this week</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card" id="stat-news">
|
||||
<div class="stat-icon bg-purple">
|
||||
<i class="fas fa-newspaper"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3 id="stat-news-count">0</h3>
|
||||
<p>Published Articles</p>
|
||||
</div>
|
||||
<div class="stat-trend" id="stat-news-trend">
|
||||
<span id="stat-news-draft">0 drafts</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card" id="stat-subscribers">
|
||||
<div class="stat-icon bg-orange">
|
||||
<i class="fas fa-users"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3 id="stat-subscribers-count">0</h3>
|
||||
<p>Subscribers</p>
|
||||
</div>
|
||||
<div class="stat-trend" id="stat-subscribers-trend">
|
||||
<span id="stat-subscribers-active">0 active</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="dashboard-row">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Quick Actions</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="quick-actions">
|
||||
<a href="/admin/news.html?action=add" class="quick-action-btn">
|
||||
<i class="fas fa-newspaper"></i>
|
||||
<span>Add Article</span>
|
||||
</a>
|
||||
<a href="/admin/portfolio.html?action=add" class="quick-action-btn">
|
||||
<i class="fas fa-briefcase"></i>
|
||||
<span>Add Project</span>
|
||||
</a>
|
||||
<a href="/admin/messages.html" class="quick-action-btn">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<span>Messages</span>
|
||||
</a>
|
||||
<a href="/admin/bookings.html" class="quick-action-btn">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Content -->
|
||||
<div class="dashboard-row two-col">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Recent News</h2>
|
||||
<a href="/admin/news.html" class="view-all">View All</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="content-list" id="recent-news">
|
||||
<!-- Will be populated by JavaScript -->
|
||||
<div class="loading-state">
|
||||
<i class="fas fa-spinner fa-spin"></i> Loading...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Recent Messages</h2>
|
||||
<a href="/admin/messages.html" class="view-all">View All</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="content-list" id="recent-messages">
|
||||
<div class="loading-state">
|
||||
<i class="fas fa-spinner fa-spin"></i> Loading...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/admin/js/admin.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadDashboardStats();
|
||||
loadRecentNews();
|
||||
loadRecentMessages();
|
||||
});
|
||||
|
||||
async function loadDashboardStats() {
|
||||
try {
|
||||
const response = await MSPE.API.get('stats.php');
|
||||
if (response && response.success) {
|
||||
const stats = response.data;
|
||||
|
||||
// Update Messages Stats
|
||||
document.getElementById('stat-messages-count').textContent = stats.messages?.unread || 0;
|
||||
document.getElementById('stat-messages-total').textContent = `${stats.messages?.total || 0} total`;
|
||||
|
||||
// Update Bookings Stats
|
||||
document.getElementById('stat-bookings-count').textContent = stats.bookings?.pending || 0;
|
||||
document.getElementById('stat-bookings-week').textContent = `${stats.bookings?.this_week || 0} this week`;
|
||||
|
||||
// Update News Stats
|
||||
document.getElementById('stat-news-count').textContent = stats.news?.published || 0;
|
||||
document.getElementById('stat-news-draft').textContent = `${stats.news?.draft || 0} drafts`;
|
||||
|
||||
// Update Subscribers Stats
|
||||
document.getElementById('stat-subscribers-count').textContent = stats.subscribers?.total || 0;
|
||||
document.getElementById('stat-subscribers-active').textContent = `${stats.subscribers?.active || 0} active`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load stats:', error);
|
||||
// Show zeros on error
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRecentNews() {
|
||||
try {
|
||||
const response = await MSPE.API.get('news.php?limit=3');
|
||||
const container = document.getElementById('recent-news');
|
||||
|
||||
if (response && response.success && response.data && response.data.length > 0) {
|
||||
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;">
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<h4>${item.title}</h4>
|
||||
<span class="item-meta">${MSPE.formatDate(item.created_at)} • ${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>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state" style="padding: 2rem; text-align: center;">
|
||||
<i class="fas fa-newspaper" style="font-size: 2rem; opacity: 0.3; margin-bottom: 0.5rem;"></i>
|
||||
<p>No articles yet</p>
|
||||
<a href="/admin/news.html?action=add" class="btn btn-primary btn-sm" style="margin-top: 1rem;">Create First Article</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load recent news:', error);
|
||||
document.getElementById('recent-news').innerHTML = `
|
||||
<div class="empty-state" style="padding: 2rem; text-align: center;">
|
||||
<i class="fas fa-exclamation-circle" style="font-size: 2rem; opacity: 0.3;"></i>
|
||||
<p>Could not load articles</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRecentMessages() {
|
||||
try {
|
||||
const response = await MSPE.API.get('contact.php?limit=3');
|
||||
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}'">
|
||||
<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>
|
||||
<span class="message-time">${MSPE.formatDate(msg.created_at)}</span>
|
||||
</div>
|
||||
<span class="message-status ${msg.status || 'unread'}"></span>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state" style="padding: 2rem; text-align: center;">
|
||||
<i class="fas fa-inbox" style="font-size: 2rem; opacity: 0.3; margin-bottom: 0.5rem;"></i>
|
||||
<p>No messages yet</p>
|
||||
<span style="font-size: 0.85rem; opacity: 0.7;">Messages from your contact form will appear here</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load recent messages:', error);
|
||||
document.getElementById('recent-messages').innerHTML = `
|
||||
<div class="empty-state" style="padding: 2rem; text-align: center;">
|
||||
<i class="fas fa-exclamation-circle" style="font-size: 2rem; opacity: 0.3;"></i>
|
||||
<p>Could not load messages</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+141
@@ -0,0 +1,141 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||
<title>Admin Login - MSPE</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha384-t1nt8BQoYMLFN5p42tRAtuAAFQaCQODekUVeKKZrEnEyp4H2R0RHFz0KWpmj7i8g" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/admin/css/admin.css">
|
||||
</head>
|
||||
<body class="login-page">
|
||||
<div class="login-container">
|
||||
<div class="login-card">
|
||||
<div class="login-header">
|
||||
<div class="login-logo">
|
||||
<i class="fas fa-cube"></i>
|
||||
<span>MSPE</span>
|
||||
</div>
|
||||
<h1>Admin Panel</h1>
|
||||
<p>Sign in to manage your website</p>
|
||||
</div>
|
||||
|
||||
<form id="login-form" class="login-form">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<div class="input-icon">
|
||||
<i class="fas fa-user"></i>
|
||||
<input type="text" id="username" name="username" required placeholder="Enter username">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<div class="input-icon">
|
||||
<i class="fas fa-lock"></i>
|
||||
<input type="password" id="password" name="password" required placeholder="Enter password">
|
||||
<button type="button" class="toggle-password" tabindex="-1">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-options">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="remember">
|
||||
<span>Remember me</span>
|
||||
</label>
|
||||
<a href="/admin/reset-password.html" class="forgot-link">Forgot password?</a>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
<span>Sign In</span>
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</button>
|
||||
|
||||
<div class="login-message" id="login-message"></div>
|
||||
</form>
|
||||
|
||||
<div class="login-footer">
|
||||
<p>MSPE Admin Panel</p>
|
||||
<p>Secure access for authorized personnel only</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="login-bg">
|
||||
<div class="bg-shape bg-shape-1"></div>
|
||||
<div class="bg-shape bg-shape-2"></div>
|
||||
<div class="bg-shape bg-shape-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Toggle password visibility
|
||||
document.querySelector('.toggle-password').addEventListener('click', function() {
|
||||
const input = document.getElementById('password');
|
||||
const icon = this.querySelector('i');
|
||||
|
||||
if (input.type === 'password') {
|
||||
input.type = 'text';
|
||||
icon.className = 'fas fa-eye-slash';
|
||||
} else {
|
||||
input.type = 'password';
|
||||
icon.className = 'fas fa-eye';
|
||||
}
|
||||
});
|
||||
|
||||
// Login form handler
|
||||
document.getElementById('login-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const remember = this.querySelector('input[name="remember"]').checked;
|
||||
const message = document.getElementById('login-message');
|
||||
const btn = this.querySelector('button[type="submit"]');
|
||||
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Signing in...';
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth.php', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'login',
|
||||
username,
|
||||
password,
|
||||
remember
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
message.className = 'login-message success';
|
||||
message.innerHTML = '<i class="fas fa-check-circle"></i> Login successful! Redirecting...';
|
||||
message.style.display = 'block';
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = '/admin/dashboard.html';
|
||||
}, 1000);
|
||||
} else {
|
||||
throw new Error(data.message || 'Invalid credentials');
|
||||
}
|
||||
} catch (error) {
|
||||
message.className = 'login-message error';
|
||||
message.innerHTML = '<i class="fas fa-exclamation-circle"></i> ' + error.message;
|
||||
message.style.display = 'block';
|
||||
|
||||
btn.innerHTML = '<span>Sign In</span><i class="fas fa-arrow-right"></i>';
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
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
|
||||
};
|
||||
Executable
+731
@@ -0,0 +1,731 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||
<title>Media Library - MSPE Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha384-t1nt8BQoYMLFN5p42tRAtuAAFQaCQODekUVeKKZrEnEyp4H2R0RHFz0KWpmj7i8g" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/admin/css/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-wrapper">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<a href="/admin/dashboard.html" class="sidebar-logo">
|
||||
<i class="fas fa-cube"></i>
|
||||
<span>MSPE</span>
|
||||
</a>
|
||||
<button class="sidebar-toggle" id="sidebar-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Main</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/dashboard.html" class="nav-link">
|
||||
<i class="fas fa-th-large"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Content</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/pages.html" class="nav-link">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
<span>Pages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/services.html" class="nav-link">
|
||||
<i class="fas fa-concierge-bell"></i>
|
||||
<span>Services</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/portfolio.html" class="nav-link">
|
||||
<i class="fas fa-briefcase"></i>
|
||||
<span>Portfolio</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/news.html" class="nav-link">
|
||||
<i class="fas fa-newspaper"></i>
|
||||
<span>News & Events</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/testimonials.html" class="nav-link">
|
||||
<i class="fas fa-quote-right"></i>
|
||||
<span>Testimonials</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/team.html" class="nav-link">
|
||||
<i class="fas fa-users"></i>
|
||||
<span>Team</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Media</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item active">
|
||||
<a href="/admin/media.html" class="nav-link">
|
||||
<i class="fas fa-images"></i>
|
||||
<span>Media Library</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Inquiries</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/messages.html" class="nav-link">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<span>Messages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/subscribers.html" class="nav-link">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span>Subscribers</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/bookings.html" class="nav-link">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Settings</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings.html" class="nav-link">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span>Site Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/users.html" class="nav-link">
|
||||
<i class="fas fa-user-shield"></i>
|
||||
<span>Admin Users</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<a href="/index.html" class="view-site-btn" target="_blank">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
<span>View Website</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<!-- Header -->
|
||||
<header class="admin-header">
|
||||
<div class="header-left">
|
||||
<button class="mobile-toggle" id="mobile-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<h1 class="page-title">Media Library</h1>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<button class="btn btn-primary" onclick="openUploadModal()">
|
||||
<i class="fas fa-upload"></i> Upload Files
|
||||
</button>
|
||||
|
||||
<div class="header-user">
|
||||
<div class="user-avatar">
|
||||
<img src="https://ui-avatars.com/api/?name=Admin&background=0ea5e9&color=fff" alt="Admin">
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<span class="user-name">Admin</span>
|
||||
<span class="user-role">Administrator</span>
|
||||
</div>
|
||||
<div class="user-dropdown">
|
||||
<button class="dropdown-toggle">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="/admin/settings.html"><i class="fas fa-cog"></i> Settings</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" id="logout-btn"><i class="fas fa-sign-out-alt"></i> Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="content-wrapper">
|
||||
<div class="media-stats">
|
||||
<div class="storage-overview">
|
||||
<div class="storage-used">
|
||||
<i class="fas fa-database"></i>
|
||||
<div>
|
||||
<span class="storage-value">0 Bytes</span>
|
||||
<span class="storage-label">Used of 5 GB</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="storage-bar">
|
||||
<div class="storage-progress" style="width: 0%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-counts">
|
||||
<div class="media-count-item">
|
||||
<i class="fas fa-image"></i>
|
||||
<span>0 Images</span>
|
||||
</div>
|
||||
<div class="media-count-item">
|
||||
<i class="fas fa-file-pdf"></i>
|
||||
<span>0 Documents</span>
|
||||
</div>
|
||||
<div class="media-count-item">
|
||||
<i class="fas fa-video"></i>
|
||||
<span>0 Videos</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters & Search -->
|
||||
<div class="media-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<div class="search-box">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="text" placeholder="Search files..." id="media-search">
|
||||
</div>
|
||||
<select class="form-select" id="type-filter">
|
||||
<option value="">All Types</option>
|
||||
<option value="image">Images</option>
|
||||
<option value="document">Documents</option>
|
||||
<option value="video">Videos</option>
|
||||
</select>
|
||||
<select class="form-select" id="folder-filter">
|
||||
<option value="">All Folders</option>
|
||||
<option value="news">News</option>
|
||||
<option value="portfolio">Portfolio</option>
|
||||
<option value="team">Team</option>
|
||||
<option value="clients">Clients</option>
|
||||
<option value="general">General</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<button class="btn btn-outline btn-sm" onclick="createFolder()">
|
||||
<i class="fas fa-folder-plus"></i> New Folder
|
||||
</button>
|
||||
<div class="view-toggle">
|
||||
<button class="view-btn active" data-view="grid"><i class="fas fa-th"></i></button>
|
||||
<button class="view-btn" data-view="list"><i class="fas fa-list"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Folders -->
|
||||
<div class="media-folders" id="media-folders">
|
||||
<div class="folder-item" data-folder="news">
|
||||
<i class="fas fa-folder"></i>
|
||||
<span>news</span>
|
||||
<small>0 files</small>
|
||||
</div>
|
||||
<div class="folder-item" data-folder="portfolio">
|
||||
<i class="fas fa-folder"></i>
|
||||
<span>portfolio</span>
|
||||
<small>0 files</small>
|
||||
</div>
|
||||
<div class="folder-item" data-folder="team">
|
||||
<i class="fas fa-folder"></i>
|
||||
<span>team</span>
|
||||
<small>0 files</small>
|
||||
</div>
|
||||
<div class="folder-item" data-folder="clients">
|
||||
<i class="fas fa-folder"></i>
|
||||
<span>clients</span>
|
||||
<small>0 files</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Media Grid -->
|
||||
<div class="media-grid" id="media-grid">
|
||||
<div style="grid-column:1/-1;text-align:center;padding:3rem;color:var(--gray-400);">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||
<p style="margin-top:1rem;">Loading media...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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()">
|
||||
<i class="fas fa-folder"></i> Move
|
||||
</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="downloadSelected()">
|
||||
<i class="fas fa-download"></i> Download
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteSelected()">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Upload Modal -->
|
||||
<div class="modal" id="upload-modal">
|
||||
<div class="modal-backdrop"></div>
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h2>Upload Files</h2>
|
||||
<button class="modal-close" onclick="closeUploadModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="upload-dropzone" id="dropzone">
|
||||
<i class="fas fa-cloud-upload-alt"></i>
|
||||
<h3>Drag & Drop Files Here</h3>
|
||||
<p>or click to browse</p>
|
||||
<span class="upload-hint">Supports: JPG, PNG, GIF, PDF, DOC, MP4 (Max 50MB each)</span>
|
||||
<input type="file" id="file-input" multiple accept="image/*,.pdf,.doc,.docx,.mp4" style="display: none;">
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top: 1rem;">
|
||||
<label>Upload to Folder</label>
|
||||
<select class="form-select" id="upload-folder">
|
||||
<option value="">Root (uploads/)</option>
|
||||
<option value="news">news/</option>
|
||||
<option value="portfolio">portfolio/</option>
|
||||
<option value="team">team/</option>
|
||||
<option value="clients">clients/</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="upload-queue" id="upload-queue" style="display: none;">
|
||||
<h4>Upload Queue</h4>
|
||||
<div class="queue-list" id="queue-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" onclick="closeUploadModal()">Cancel</button>
|
||||
<button class="btn btn-primary" id="start-upload" disabled>
|
||||
<i class="fas fa-upload"></i> Start Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Media View Modal -->
|
||||
<div class="modal" id="view-modal">
|
||||
<div class="modal-backdrop"></div>
|
||||
<div class="modal-container modal-lg">
|
||||
<div class="modal-header">
|
||||
<h2 id="view-title">File Details</h2>
|
||||
<button class="modal-close" onclick="closeViewModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="media-view-content">
|
||||
<div class="media-view-preview">
|
||||
<img src="" id="view-image" alt="">
|
||||
</div>
|
||||
<div class="media-view-details">
|
||||
<div class="detail-row">
|
||||
<label>File Name</label>
|
||||
<span id="detail-name">-</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<label>Type</label>
|
||||
<span id="detail-type">-</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<label>Size</label>
|
||||
<span id="detail-size">-</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<label>Dimensions</label>
|
||||
<span id="detail-dimensions">-</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<label>Uploaded</label>
|
||||
<span id="detail-date">-</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<label>URL</label>
|
||||
<div class="url-copy">
|
||||
<input type="text" id="detail-url" readonly>
|
||||
<button class="btn btn-sm btn-outline" onclick="copyDetailUrl()">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<label>Alt Text</label>
|
||||
<input type="text" class="form-control" id="detail-alt" placeholder="Enter alt text...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-danger" onclick="deleteFromView()">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
<button class="btn btn-outline" onclick="downloadFile()">
|
||||
<i class="fas fa-download"></i> Download
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="saveDetails()">
|
||||
<i class="fas fa-save"></i> Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/admin/js/admin.js"></script>
|
||||
<script>
|
||||
let allMedia = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadMedia();
|
||||
|
||||
// Search & Filter
|
||||
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);
|
||||
});
|
||||
|
||||
async function loadMedia() {
|
||||
try {
|
||||
const response = await MSPE.API.get('media.php');
|
||||
if (response && response.success) {
|
||||
allMedia = response.data;
|
||||
renderMedia(allMedia);
|
||||
updateStats(allMedia);
|
||||
updateFolderCounts(allMedia);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading media:', error);
|
||||
document.getElementById('media-grid').innerHTML = '<div class="empty-state">Failed to load media</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function updateFolderCounts(media) {
|
||||
document.querySelectorAll('#media-folders .folder-item').forEach(el => {
|
||||
const folder = el.dataset.folder;
|
||||
const count = media.filter(m => m.folder === folder).length;
|
||||
el.querySelector('small').textContent = count + ' files';
|
||||
});
|
||||
}
|
||||
|
||||
function renderMedia(media) {
|
||||
const container = document.getElementById('media-grid');
|
||||
if (media.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state"><i class="fas fa-images"></i><p>No media files found</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = media.map(item => `
|
||||
<div class="media-item" data-id="${item.id}" data-type="${item.type}">
|
||||
<div class="media-preview ${item.type.includes('image') ? '' : 'document'}">
|
||||
${item.type.includes('image')
|
||||
? `<img src="/${item.path}" alt="${item.name}">`
|
||||
: `<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-info">
|
||||
<span class="media-name">${item.name}</span>
|
||||
<span class="media-size">${formatBytes(item.size)}</span>
|
||||
</div>
|
||||
<label class="media-checkbox">
|
||||
<input type="checkbox" name="selected[]" value="${item.id}" onchange="updateBulkActions()">
|
||||
<span class="checkmark"></span>
|
||||
</label>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function getFileIcon(type) {
|
||||
if (type.includes('pdf')) return 'pdf';
|
||||
if (type.includes('video')) return 'video';
|
||||
if (type.includes('word') || type.includes('document')) return 'word';
|
||||
return 'alt';
|
||||
}
|
||||
|
||||
function filterMedia() {
|
||||
const search = document.getElementById('media-search').value.toLowerCase();
|
||||
const type = document.getElementById('type-filter').value;
|
||||
const folder = document.getElementById('folder-filter').value;
|
||||
|
||||
let filtered = allMedia;
|
||||
|
||||
if (search) filtered = filtered.filter(m => m.name.toLowerCase().includes(search));
|
||||
if (type) filtered = filtered.filter(m => m.type.includes(type));
|
||||
if (folder) filtered = filtered.filter(m => m.folder === folder);
|
||||
|
||||
renderMedia(filtered);
|
||||
}
|
||||
|
||||
function updateStats(media) {
|
||||
const totalSize = media.reduce((acc, item) => acc + parseInt(item.size), 0);
|
||||
const images = media.filter(m => m.type.includes('image')).length;
|
||||
const videos = media.filter(m => m.type.includes('video')).length;
|
||||
const docs = media.length - images - videos;
|
||||
|
||||
document.querySelector('.storage-value').textContent = formatBytes(totalSize);
|
||||
document.querySelectorAll('.media-count-item span')[0].textContent = images + ' Images';
|
||||
document.querySelectorAll('.media-count-item span')[1].textContent = docs + ' Documents';
|
||||
document.querySelectorAll('.media-count-item span')[2].textContent = videos + ' Videos';
|
||||
|
||||
// Simple progress bar logic (assuming 500MB limit for demo)
|
||||
const pct = Math.min((totalSize / (500 * 1024 * 1024)) * 100, 100);
|
||||
document.querySelector('.storage-progress').style.width = pct + '%';
|
||||
}
|
||||
|
||||
function formatBytes(bytes, decimals = 2) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// Upload Logic
|
||||
let uploadFilesList = [];
|
||||
|
||||
function handleFileSelect(e) {
|
||||
const files = Array.from(e.target.files);
|
||||
uploadFilesList = files;
|
||||
|
||||
const queueList = document.getElementById('queue-list');
|
||||
document.getElementById('upload-queue').style.display = 'block';
|
||||
queueList.innerHTML = files.map(f => `
|
||||
<div class="queue-item">
|
||||
<span>${f.name}</span>
|
||||
<small>${formatBytes(f.size)}</small>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
document.getElementById('start-upload').disabled = false;
|
||||
}
|
||||
|
||||
async function uploadFiles() {
|
||||
const folder = document.getElementById('upload-folder').value;
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('folder', folder);
|
||||
uploadFilesList.forEach(file => {
|
||||
formData.append('files[]', file); // Use array name for multiple
|
||||
});
|
||||
|
||||
const btn = document.getElementById('start-upload');
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Uploading...';
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const result = await MSPE.API.upload('media.php', formData);
|
||||
|
||||
if (result && result.success) {
|
||||
MSPE.showNotification('Upload successful', 'success');
|
||||
closeUploadModal();
|
||||
loadMedia();
|
||||
} else {
|
||||
MSPE.showNotification(result?.message || 'Upload failed', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
MSPE.showNotification('Error uploading files', 'error');
|
||||
} finally {
|
||||
btn.innerHTML = '<i class="fas fa-upload"></i> Start Upload';
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteMedia(id) {
|
||||
if (!await MSPE.confirmDialog('Delete this file?')) return;
|
||||
|
||||
try {
|
||||
const response = await MSPE.API.delete(`media.php?id=${id}`);
|
||||
if (response.success) {
|
||||
MSPE.showNotification('File deleted', 'success');
|
||||
loadMedia();
|
||||
}
|
||||
} catch (error) {
|
||||
MSPE.showNotification('Error deleting file', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function copyUrl(path) {
|
||||
const url = window.location.origin + path;
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
MSPE.showNotification('URL copied to clipboard', 'success');
|
||||
});
|
||||
}
|
||||
|
||||
// --- Modals ---
|
||||
function openUploadModal() {
|
||||
document.getElementById('upload-modal').classList.add('active');
|
||||
uploadFilesList = [];
|
||||
document.getElementById('queue-list').innerHTML = '';
|
||||
document.getElementById('upload-queue').style.display = 'none';
|
||||
document.getElementById('start-upload').disabled = true;
|
||||
}
|
||||
|
||||
function closeUploadModal() {
|
||||
document.getElementById('upload-modal').classList.remove('active');
|
||||
}
|
||||
|
||||
function viewMedia(id) {
|
||||
const item = allMedia.find(m => m.id === id);
|
||||
if (!item) return;
|
||||
|
||||
document.getElementById('view-modal').classList.add('active');
|
||||
document.getElementById('view-image').src = '/' + item.path;
|
||||
document.getElementById('detail-name').textContent = item.name;
|
||||
document.getElementById('detail-type').textContent = item.type;
|
||||
document.getElementById('detail-size').textContent = formatBytes(item.size);
|
||||
document.getElementById('detail-date').textContent = MSPE.formatDate(item.created_at);
|
||||
document.getElementById('detail-url').value = window.location.origin + '/' + item.path;
|
||||
}
|
||||
|
||||
function closeViewModal() {
|
||||
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');
|
||||
if (checked.length > 0) {
|
||||
bulkActions.style.display = 'flex';
|
||||
bulkActions.querySelector('.selected-count').textContent = checked.length + ' selected';
|
||||
} else {
|
||||
bulkActions.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function createFolder() {
|
||||
const name = prompt('Enter folder name:');
|
||||
if (name) {
|
||||
MSPE.showNotification('Folder management is handled on the server file system', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
function moveSelected() {
|
||||
const checked = document.querySelectorAll('.media-checkbox input:checked');
|
||||
if (checked.length === 0) return;
|
||||
MSPE.showNotification('Move functionality coming soon', 'info');
|
||||
}
|
||||
|
||||
async function downloadSelected() {
|
||||
const checked = document.querySelectorAll('.media-checkbox input:checked');
|
||||
if (checked.length === 0) return;
|
||||
checked.forEach(cb => {
|
||||
const item = allMedia.find(m => m.id === cb.value);
|
||||
if (item) downloadFile('/' + item.path);
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteSelected() {
|
||||
const checked = document.querySelectorAll('.media-checkbox input:checked');
|
||||
if (checked.length === 0) return;
|
||||
if (!await MSPE.confirmDialog(`Delete ${checked.length} selected file(s)?`)) return;
|
||||
|
||||
let success = 0;
|
||||
for (const cb of checked) {
|
||||
try {
|
||||
const response = await MSPE.API.delete(`media.php?id=${cb.value}`);
|
||||
if (response && response.success) success++;
|
||||
} catch (e) { /* continue */ }
|
||||
}
|
||||
MSPE.showNotification(`${success} file(s) deleted`, 'success');
|
||||
loadMedia();
|
||||
}
|
||||
|
||||
function downloadFile(path) {
|
||||
const link = document.createElement('a');
|
||||
link.href = path || '';
|
||||
link.download = '';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
function copyDetailUrl() {
|
||||
const url = document.getElementById('detail-url').value;
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
MSPE.showNotification('URL copied to clipboard', 'success');
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteFromView() {
|
||||
const img = document.getElementById('view-image');
|
||||
const item = allMedia.find(m => img.src.includes(m.path));
|
||||
if (!item) return;
|
||||
|
||||
if (!await MSPE.confirmDialog('Delete this file?')) return;
|
||||
|
||||
try {
|
||||
const response = await MSPE.API.delete(`media.php?id=${item.id}`);
|
||||
if (response && response.success) {
|
||||
MSPE.showNotification('File deleted', 'success');
|
||||
closeViewModal();
|
||||
loadMedia();
|
||||
}
|
||||
} catch (error) {
|
||||
MSPE.showNotification('Error deleting file', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function saveDetails() {
|
||||
const alt = document.getElementById('detail-alt').value;
|
||||
MSPE.showNotification('Details saved', 'success');
|
||||
closeViewModal();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+741
@@ -0,0 +1,741 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||
<title>Messages - MSPE Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha384-t1nt8BQoYMLFN5p42tRAtuAAFQaCQODekUVeKKZrEnEyp4H2R0RHFz0KWpmj7i8g" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/admin/css/admin.css">
|
||||
<style>
|
||||
.messages-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
gap: 2rem;
|
||||
height: calc(100vh - 200px);
|
||||
}
|
||||
|
||||
.messages-list {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.messages-list-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.messages-list-header h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.unread-count {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.messages-filters {
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.messages-filters select {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.messages-items {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.message-item:hover {
|
||||
background: var(--gray-50);
|
||||
}
|
||||
|
||||
.message-item.active {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.message-item.active .message-meta,
|
||||
.message-item.active .message-preview {
|
||||
color: rgba(255,255,255,0.8);
|
||||
}
|
||||
|
||||
.message-item.unread {
|
||||
background: rgba(14, 165, 233, 0.05);
|
||||
border-left: 3px solid var(--primary);
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.message-sender {
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.message-sender .unread-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--primary);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.message-meta {
|
||||
font-size: 0.75rem;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.message-preview {
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-600);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.message-details {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.message-details-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.message-details-header h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.message-details-header .meta {
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.message-actions-header {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.message-actions-header button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid var(--gray-300);
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.message-actions-header button:hover {
|
||||
background: var(--gray-100);
|
||||
}
|
||||
|
||||
.message-actions-header button.delete:hover {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
border-color: #dc2626;
|
||||
}
|
||||
|
||||
.message-details-content {
|
||||
flex: 1;
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.contact-info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.contact-info-item label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-500);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
.contact-info-item .value {
|
||||
font-size: 0.9375rem;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.contact-info-item .value a {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.message-body {
|
||||
line-height: 1.7;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.message-body h4 {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 4rem 2rem;
|
||||
text-align: center;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.messages-grid {
|
||||
grid-template-columns: 1fr;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.messages-list {
|
||||
max-height: 400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-wrapper">
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<a href="/admin/dashboard.html" class="sidebar-logo">
|
||||
<i class="fas fa-cube"></i>
|
||||
<span>MSPE</span>
|
||||
</a>
|
||||
<button class="sidebar-toggle" id="sidebar-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Main</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/dashboard.html" class="nav-link">
|
||||
<i class="fas fa-th-large"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Content</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/pages.html" class="nav-link">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
<span>Pages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/services.html" class="nav-link">
|
||||
<i class="fas fa-concierge-bell"></i>
|
||||
<span>Services</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/portfolio.html" class="nav-link">
|
||||
<i class="fas fa-briefcase"></i>
|
||||
<span>Portfolio</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/news.html" class="nav-link">
|
||||
<i class="fas fa-newspaper"></i>
|
||||
<span>News & Events</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/testimonials.html" class="nav-link">
|
||||
<i class="fas fa-quote-right"></i>
|
||||
<span>Testimonials</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/team.html" class="nav-link">
|
||||
<i class="fas fa-users"></i>
|
||||
<span>Team</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Media</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/media.html" class="nav-link">
|
||||
<i class="fas fa-images"></i>
|
||||
<span>Media Library</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Inquiries</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item active">
|
||||
<a href="/admin/messages.html" class="nav-link">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<span>Messages</span>
|
||||
<span class="badge" id="sidebar-unread-count">0</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/subscribers.html" class="nav-link">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span>Subscribers</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/bookings.html" class="nav-link">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Settings</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings.html" class="nav-link">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span>Site Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/users.html" class="nav-link">
|
||||
<i class="fas fa-user-shield"></i>
|
||||
<span>Admin Users</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<a href="/index.html" class="view-site-btn" target="_blank">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
<span>View Website</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="admin-header">
|
||||
<div class="header-left">
|
||||
<button class="mobile-toggle" id="mobile-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<h1 class="page-title">Messages</h1>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div class="header-search">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="text" placeholder="Search messages..." id="search-input">
|
||||
</div>
|
||||
|
||||
<div class="header-user">
|
||||
<div class="user-avatar">
|
||||
<img src="https://ui-avatars.com/api/?name=Admin&background=0ea5e9&color=fff" alt="Admin">
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<span class="user-name">Admin</span>
|
||||
<span class="user-role">Administrator</span>
|
||||
</div>
|
||||
<div class="user-dropdown">
|
||||
<button class="dropdown-toggle">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="/admin/settings.html"><i class="fas fa-cog"></i> Settings</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" id="logout-btn"><i class="fas fa-sign-out-alt"></i> Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="messages-grid">
|
||||
<div class="messages-list">
|
||||
<div class="messages-list-header">
|
||||
<h3>Inbox</h3>
|
||||
<span class="unread-count" id="unread-count">0 unread</span>
|
||||
</div>
|
||||
|
||||
<div class="messages-filters">
|
||||
<select id="status-filter">
|
||||
<option value="">All Messages</option>
|
||||
<option value="unread">Unread</option>
|
||||
<option value="read">Read</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="messages-items" id="messages-list">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
<p>Loading messages...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-details" id="message-details">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-envelope-open-text"></i>
|
||||
<p>Select a message to view details</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/admin/js/admin.js"></script>
|
||||
<script>
|
||||
let allMessages = [];
|
||||
let selectedMessageId = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadMessages();
|
||||
setupEventListeners();
|
||||
checkUrlParams();
|
||||
});
|
||||
|
||||
function setupEventListeners() {
|
||||
document.getElementById('status-filter').addEventListener('change', filterMessages);
|
||||
document.getElementById('search-input').addEventListener('input', MSPE.debounce(filterMessages, 300));
|
||||
}
|
||||
|
||||
function checkUrlParams() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const messageId = params.get('id');
|
||||
if (messageId) {
|
||||
setTimeout(() => selectMessage(messageId), 500);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMessages() {
|
||||
try {
|
||||
const response = await MSPE.API.get('contact.php');
|
||||
|
||||
if (response && response.success) {
|
||||
allMessages = response.data || [];
|
||||
updateUnreadCount(response.unread || 0);
|
||||
renderMessagesList(allMessages);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load messages:', error);
|
||||
document.getElementById('messages-list').innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<p>Failed to load messages</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function updateUnreadCount(count) {
|
||||
document.getElementById('unread-count').textContent = `${count} unread`;
|
||||
document.getElementById('sidebar-unread-count').textContent = count;
|
||||
|
||||
if (count === 0) {
|
||||
document.getElementById('sidebar-unread-count').style.display = 'none';
|
||||
} else {
|
||||
document.getElementById('sidebar-unread-count').style.display = 'inline';
|
||||
}
|
||||
}
|
||||
|
||||
function renderMessagesList(messages) {
|
||||
const container = document.getElementById('messages-list');
|
||||
|
||||
if (!messages || messages.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<p>No messages yet</p>
|
||||
<span style="font-size: 0.875rem;">Messages from your contact form will appear here</span>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = messages.map(message => {
|
||||
const isUnread = (message.status === 'unread');
|
||||
const isActive = (message.id === selectedMessageId);
|
||||
const name = `${message.first_name} ${message.last_name}`;
|
||||
const timeAgo = getTimeAgo(new Date(message.created_at));
|
||||
const preview = message.message.length > 60
|
||||
? message.message.substring(0, 60) + '...'
|
||||
: message.message;
|
||||
|
||||
return `
|
||||
<div class="message-item ${isUnread ? 'unread' : ''} ${isActive ? 'active' : ''}"
|
||||
onclick="selectMessage('${message.id}')"
|
||||
data-id="${message.id}">
|
||||
<div class="message-header">
|
||||
<span class="message-sender">
|
||||
${isUnread ? '<span class="unread-dot"></span>' : ''}
|
||||
${name}
|
||||
</span>
|
||||
<span class="message-meta">${timeAgo}</span>
|
||||
</div>
|
||||
<div class="message-preview">${preview}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function selectMessage(id) {
|
||||
selectedMessageId = id;
|
||||
|
||||
// Update list selection
|
||||
document.querySelectorAll('.message-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
if (item.dataset.id === id) {
|
||||
item.classList.add('active');
|
||||
item.classList.remove('unread');
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await MSPE.API.get(`contact.php?id=${id}`);
|
||||
|
||||
if (response && response.success && response.data) {
|
||||
const message = response.data;
|
||||
renderMessageDetails(message);
|
||||
|
||||
// Update message in local array
|
||||
const index = allMessages.findIndex(m => m.id === id);
|
||||
if (index !== -1) {
|
||||
allMessages[index].status = 'read';
|
||||
}
|
||||
|
||||
// Update unread count
|
||||
const unreadCount = allMessages.filter(m => m.status === 'unread').length;
|
||||
updateUnreadCount(unreadCount);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load message:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderMessageDetails(message) {
|
||||
const container = document.getElementById('message-details');
|
||||
const name = `${message.first_name} ${message.last_name}`;
|
||||
const date = new Date(message.created_at);
|
||||
const formattedDate = date.toLocaleDateString('en-US', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
});
|
||||
|
||||
const serviceLabels = {
|
||||
'it-support': 'IT Support',
|
||||
'cybersecurity': 'Cybersecurity',
|
||||
'cloud': 'Cloud Services',
|
||||
'consulting': 'Business Consulting',
|
||||
'general': 'General Inquiry'
|
||||
};
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="message-details-header">
|
||||
<div>
|
||||
<h3>${name}</h3>
|
||||
<div class="meta">${formattedDate}</div>
|
||||
</div>
|
||||
<div class="message-actions-header">
|
||||
<button onclick="replyToMessage('${message.email}')">
|
||||
<i class="fas fa-reply"></i> Reply
|
||||
</button>
|
||||
<button class="delete" onclick="deleteMessage('${message.id}')">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-details-content">
|
||||
<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>
|
||||
<div class="contact-info-item">
|
||||
<label>Phone</label>
|
||||
<div class="value">${message.phone || 'Not provided'}</div>
|
||||
</div>
|
||||
<div class="contact-info-item">
|
||||
<label>Company</label>
|
||||
<div class="value">${message.company || 'Not provided'}</div>
|
||||
</div>
|
||||
<div class="contact-info-item">
|
||||
<label>Service Interest</label>
|
||||
<div class="value">${serviceLabels[message.service] || message.service || 'Not specified'}</div>
|
||||
</div>
|
||||
${message.budget ? `
|
||||
<div class="contact-info-item">
|
||||
<label>Budget</label>
|
||||
<div class="value">${message.budget}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="message-body">
|
||||
<h4>Message</h4>
|
||||
<p>${message.message.replace(/\n/g, '<br>')}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function replyToMessage(email) {
|
||||
window.location.href = `mailto:${email}`;
|
||||
}
|
||||
|
||||
async function deleteMessage(id) {
|
||||
if (!await MSPE.confirmDialog('Are you sure you want to delete this message? This action cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await MSPE.API.delete(`contact.php?id=${id}`);
|
||||
|
||||
if (response && response.success) {
|
||||
MSPE.showNotification('Message deleted successfully', 'success');
|
||||
|
||||
// Remove from local array
|
||||
allMessages = allMessages.filter(m => m.id !== id);
|
||||
|
||||
// Clear details panel
|
||||
selectedMessageId = null;
|
||||
document.getElementById('message-details').innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-envelope-open-text"></i>
|
||||
<p>Select a message to view details</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Re-render list
|
||||
renderMessagesList(allMessages);
|
||||
|
||||
// Update unread count
|
||||
const unreadCount = allMessages.filter(m => m.status === 'unread').length;
|
||||
updateUnreadCount(unreadCount);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete message:', error);
|
||||
MSPE.showNotification('Failed to delete message', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function filterMessages() {
|
||||
const status = document.getElementById('status-filter').value;
|
||||
const search = document.getElementById('search-input').value.toLowerCase();
|
||||
|
||||
let filtered = allMessages;
|
||||
|
||||
if (status) {
|
||||
filtered = filtered.filter(m => m.status === status);
|
||||
}
|
||||
|
||||
if (search) {
|
||||
filtered = filtered.filter(m =>
|
||||
m.first_name.toLowerCase().includes(search) ||
|
||||
m.last_name.toLowerCase().includes(search) ||
|
||||
m.email.toLowerCase().includes(search) ||
|
||||
m.message.toLowerCase().includes(search) ||
|
||||
(m.company && m.company.toLowerCase().includes(search))
|
||||
);
|
||||
}
|
||||
|
||||
renderMessagesList(filtered);
|
||||
}
|
||||
|
||||
function getTimeAgo(date) {
|
||||
const now = new Date();
|
||||
const diffMs = now - date;
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) return 'Just now';
|
||||
if (diffMins < 60) return `${diffMins}m ago`;
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
if (diffDays < 7) return `${diffDays}d ago`;
|
||||
|
||||
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+708
@@ -0,0 +1,708 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||
<title>News Management - MSPE Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha384-t1nt8BQoYMLFN5p42tRAtuAAFQaCQODekUVeKKZrEnEyp4H2R0RHFz0KWpmj7i8g" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/admin/css/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-wrapper">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<a href="/admin/dashboard.html" class="sidebar-logo">
|
||||
<i class="fas fa-cube"></i>
|
||||
<span>MSPE</span>
|
||||
</a>
|
||||
<button class="sidebar-toggle" id="sidebar-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Main</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/dashboard.html" class="nav-link">
|
||||
<i class="fas fa-th-large"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Content</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/pages.html" class="nav-link">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
<span>Pages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/services.html" class="nav-link">
|
||||
<i class="fas fa-concierge-bell"></i>
|
||||
<span>Services</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/portfolio.html" class="nav-link">
|
||||
<i class="fas fa-briefcase"></i>
|
||||
<span>Portfolio</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a href="/admin/news.html" class="nav-link">
|
||||
<i class="fas fa-newspaper"></i>
|
||||
<span>News & Events</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/testimonials.html" class="nav-link">
|
||||
<i class="fas fa-quote-right"></i>
|
||||
<span>Testimonials</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/team.html" class="nav-link">
|
||||
<i class="fas fa-users"></i>
|
||||
<span>Team</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Media</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/media.html" class="nav-link">
|
||||
<i class="fas fa-images"></i>
|
||||
<span>Media Library</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Inquiries</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/messages.html" class="nav-link">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<span>Messages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/subscribers.html" class="nav-link">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span>Subscribers</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/bookings.html" class="nav-link">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Settings</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings.html" class="nav-link">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span>Site Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/users.html" class="nav-link">
|
||||
<i class="fas fa-user-shield"></i>
|
||||
<span>Admin Users</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<a href="/index.html" class="view-site-btn" target="_blank">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
<span>View Website</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<!-- Header -->
|
||||
<header class="admin-header">
|
||||
<div class="header-left">
|
||||
<button class="mobile-toggle" id="mobile-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<h1 class="page-title">News & Events</h1>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div class="header-user">
|
||||
<div class="user-avatar">
|
||||
<img src="https://ui-avatars.com/api/?name=Admin&background=0ea5e9&color=fff" alt="Admin">
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<span class="user-name">Admin</span>
|
||||
<span class="user-role">Administrator</span>
|
||||
</div>
|
||||
<div class="user-dropdown">
|
||||
<button class="dropdown-toggle">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="/admin/settings.html"><i class="fas fa-cog"></i> Settings</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" id="logout-btn"><i class="fas fa-sign-out-alt"></i> Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="content-wrapper">
|
||||
<!-- List View -->
|
||||
<div id="list-view">
|
||||
<div class="content-header">
|
||||
<div class="content-filters">
|
||||
<div class="filter-group">
|
||||
<select id="category-filter">
|
||||
<option value="">All Categories</option>
|
||||
<option value="news">Company News</option>
|
||||
<option value="events">Events</option>
|
||||
<option value="insights">Industry Insights</option>
|
||||
<option value="updates">Product Updates</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<select id="status-filter">
|
||||
<option value="">All Status</option>
|
||||
<option value="published">Published</option>
|
||||
<option value="draft">Draft</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="search-box">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="text" id="search-input" placeholder="Search news...">
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" id="add-new-btn">
|
||||
<i class="fas fa-plus"></i> Add New
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="table-responsive">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="50">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="select-all">
|
||||
<span class="checkmark"></span>
|
||||
</label>
|
||||
</th>
|
||||
<th>Title</th>
|
||||
<th width="120">Category</th>
|
||||
<th width="100">Status</th>
|
||||
<th width="120">Date</th>
|
||||
<th width="120">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="news-table-body">
|
||||
<!-- Data will be loaded via JS -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="table-footer">
|
||||
<div class="bulk-actions">
|
||||
<select id="bulk-action">
|
||||
<option value="">Bulk Actions</option>
|
||||
<option value="publish">Publish</option>
|
||||
<option value="draft">Set as Draft</option>
|
||||
<option value="delete">Delete</option>
|
||||
</select>
|
||||
<button class="btn btn-secondary btn-sm" id="apply-bulk">Apply</button>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
<span class="pagination-info" id="pagination-info">Showing 0 articles</span>
|
||||
<div class="pagination-controls" id="pagination-controls">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit/Add Form -->
|
||||
<div id="form-view" style="display: none;">
|
||||
<div class="content-header">
|
||||
<button class="btn btn-secondary" id="back-to-list">
|
||||
<i class="fas fa-arrow-left"></i> Back to List
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form id="news-form" class="edit-form">
|
||||
<div class="form-grid">
|
||||
<div class="form-main">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 id="form-title">Add New Article</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<input type="hidden" id="article-id" name="id">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="title">Title *</label>
|
||||
<input type="text" id="title" name="title" required placeholder="Enter article title">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="slug">Slug</label>
|
||||
<input type="text" id="slug" name="slug" placeholder="auto-generated-from-title">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="excerpt">Excerpt *</label>
|
||||
<textarea id="excerpt" name="excerpt" rows="3" required placeholder="Brief description of the article"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="content">Content *</label>
|
||||
<textarea id="content" name="content" rows="15" required placeholder="Write your article content here..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-sidebar">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Publish</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="status">Status</label>
|
||||
<select id="status" name="status">
|
||||
<option value="draft">Draft</option>
|
||||
<option value="published">Published</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="publish-date">Publish Date</label>
|
||||
<input type="datetime-local" id="publish-date" name="publish_date">
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary btn-block" id="save-draft">
|
||||
Save Draft
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
<i class="fas fa-check"></i> Publish
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Category</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<select id="category" name="category" required>
|
||||
<option value="">Select Category</option>
|
||||
<option value="news">Company News</option>
|
||||
<option value="events">Events</option>
|
||||
<option value="insights">Industry Insights</option>
|
||||
<option value="updates">Product Updates</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Featured Image</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="image-upload" id="image-upload">
|
||||
<div class="upload-placeholder" id="upload-placeholder">
|
||||
<i class="fas fa-cloud-upload-alt"></i>
|
||||
<p>Drag & drop or click to upload</p>
|
||||
<span>Recommended: 800x500px</span>
|
||||
</div>
|
||||
<img id="image-preview" src="" alt="" style="display: none;">
|
||||
<input type="file" id="featured-image" name="featured_image" accept="image/*" hidden>
|
||||
<button type="button" class="btn btn-secondary btn-sm remove-image" id="remove-image" style="display: none;">
|
||||
<i class="fas fa-times"></i> Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Author</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<input type="text" id="author" name="author" value="Admin" placeholder="Author name">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal" id="delete-modal">
|
||||
<div class="modal-overlay"></div>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Confirm Delete</h3>
|
||||
<button class="modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete this article? This action cannot be undone.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary modal-cancel">Cancel</button>
|
||||
<button class="btn btn-danger" id="confirm-delete">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/admin/js/admin.js"></script>
|
||||
<script>
|
||||
let allArticles = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadArticles();
|
||||
setupEventListeners();
|
||||
|
||||
// Check for edit param in URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const editId = urlParams.get('edit');
|
||||
const action = urlParams.get('action');
|
||||
|
||||
if (editId) {
|
||||
editArticle(editId);
|
||||
} else if (action === 'add') {
|
||||
showForm();
|
||||
}
|
||||
});
|
||||
|
||||
function setupEventListeners() {
|
||||
document.getElementById('add-new-btn').addEventListener('click', showForm);
|
||||
document.getElementById('back-to-list').addEventListener('click', showList);
|
||||
document.getElementById('news-form').addEventListener('submit', saveArticle);
|
||||
|
||||
// Save draft button
|
||||
document.getElementById('save-draft').addEventListener('click', function() {
|
||||
document.getElementById('status').value = 'draft';
|
||||
document.getElementById('news-form').dispatchEvent(new Event('submit', { cancelable: true }));
|
||||
});
|
||||
|
||||
// Bulk actions
|
||||
document.getElementById('apply-bulk').addEventListener('click', applyBulkAction);
|
||||
|
||||
// Filters
|
||||
document.getElementById('category-filter').addEventListener('change', filterArticles);
|
||||
document.getElementById('status-filter').addEventListener('change', filterArticles);
|
||||
document.getElementById('search-input').addEventListener('input', MSPE.debounce(filterArticles, 300));
|
||||
|
||||
// Select all
|
||||
document.getElementById('select-all').addEventListener('change', function() {
|
||||
document.querySelectorAll('.row-select').forEach(cb => cb.checked = this.checked);
|
||||
});
|
||||
|
||||
// Image upload handling
|
||||
const imageUpload = document.getElementById('image-upload');
|
||||
const imageInput = document.getElementById('featured-image');
|
||||
const removeImageBtn = document.getElementById('remove-image');
|
||||
|
||||
imageUpload.addEventListener('click', function(e) {
|
||||
if (e.target !== removeImageBtn && !removeImageBtn.contains(e.target)) {
|
||||
imageInput.click();
|
||||
}
|
||||
});
|
||||
|
||||
imageInput.addEventListener('change', function() {
|
||||
if (this.files && this.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
document.getElementById('image-preview').src = e.target.result;
|
||||
document.getElementById('image-preview').style.display = 'block';
|
||||
document.getElementById('upload-placeholder').style.display = 'none';
|
||||
removeImageBtn.style.display = 'block';
|
||||
};
|
||||
reader.readAsDataURL(this.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
removeImageBtn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
imageInput.value = '';
|
||||
document.getElementById('image-preview').style.display = 'none';
|
||||
document.getElementById('image-preview').src = '';
|
||||
document.getElementById('upload-placeholder').style.display = 'flex';
|
||||
removeImageBtn.style.display = 'none';
|
||||
});
|
||||
|
||||
// Auto-slug
|
||||
document.getElementById('title').addEventListener('input', function() {
|
||||
const slug = this.value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/(^-|-$)/g, '');
|
||||
document.getElementById('slug').value = slug;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadArticles() {
|
||||
try {
|
||||
const response = await MSPE.API.get('news.php');
|
||||
if (response && response.success) {
|
||||
allArticles = response.data;
|
||||
renderArticles(allArticles);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load articles:', error);
|
||||
document.getElementById('news-table-body').innerHTML = `
|
||||
<tr><td colspan="6" class="text-center">Failed to load articles</td></tr>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderArticles(articles) {
|
||||
const tbody = document.getElementById('news-table-body');
|
||||
|
||||
// Update pagination info
|
||||
const infoEl = document.getElementById('pagination-info');
|
||||
if (infoEl) {
|
||||
infoEl.textContent = articles.length === 0
|
||||
? 'No articles'
|
||||
: `Showing ${articles.length} article${articles.length !== 1 ? 's' : ''}`;
|
||||
}
|
||||
|
||||
if (articles.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-newspaper"></i>
|
||||
<p>No articles found</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = articles.map(article => `
|
||||
<tr>
|
||||
<td>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" class="row-select" value="${article.id}">
|
||||
<span class="checkmark"></span>
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<div class="table-item">
|
||||
<img src="${article.featured_image || 'https://via.placeholder.com/60x40'}" alt="" style="object-fit: cover; width: 60px; height: 40px;">
|
||||
<span>${article.title}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="category-badge ${article.category}">${article.category}</span></td>
|
||||
<td><span class="status-badge ${article.status}">${article.status}</span></td>
|
||||
<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>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function filterArticles() {
|
||||
const category = document.getElementById('category-filter').value;
|
||||
const status = document.getElementById('status-filter').value;
|
||||
const search = document.getElementById('search-input').value.toLowerCase();
|
||||
|
||||
let filtered = allArticles;
|
||||
|
||||
if (category) filtered = filtered.filter(a => a.category === category);
|
||||
if (status) filtered = filtered.filter(a => a.status === status);
|
||||
if (search) filtered = filtered.filter(a => a.title.toLowerCase().includes(search));
|
||||
|
||||
renderArticles(filtered);
|
||||
}
|
||||
|
||||
function showForm() {
|
||||
document.getElementById('list-view').style.display = 'none';
|
||||
document.getElementById('form-view').style.display = 'block';
|
||||
document.getElementById('form-title').textContent = 'Add New Article';
|
||||
document.getElementById('news-form').reset();
|
||||
document.getElementById('article-id').value = '';
|
||||
|
||||
// Reset image
|
||||
document.getElementById('image-preview').style.display = 'none';
|
||||
document.getElementById('upload-placeholder').style.display = 'flex';
|
||||
document.getElementById('remove-image').style.display = 'none';
|
||||
}
|
||||
|
||||
function showList() {
|
||||
document.getElementById('form-view').style.display = 'none';
|
||||
document.getElementById('list-view').style.display = 'block';
|
||||
// Update URL without refresh
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.delete('edit');
|
||||
url.searchParams.delete('action');
|
||||
window.history.pushState({}, '', url);
|
||||
}
|
||||
|
||||
async function editArticle(id) {
|
||||
try {
|
||||
const response = await MSPE.API.get(`news.php?id=${id}`);
|
||||
if (response && response.success) {
|
||||
const article = response.data;
|
||||
|
||||
showForm();
|
||||
document.getElementById('form-title').textContent = 'Edit Article';
|
||||
document.getElementById('article-id').value = article.id;
|
||||
document.getElementById('title').value = article.title;
|
||||
document.getElementById('slug').value = article.slug || '';
|
||||
document.getElementById('excerpt').value = article.excerpt || '';
|
||||
document.getElementById('content').value = article.content || '';
|
||||
document.getElementById('category').value = article.category;
|
||||
document.getElementById('status').value = article.status;
|
||||
document.getElementById('author').value = article.author;
|
||||
|
||||
if (article.featured_image) {
|
||||
document.getElementById('image-preview').src = article.featured_image;
|
||||
document.getElementById('image-preview').style.display = 'block';
|
||||
document.getElementById('upload-placeholder').style.display = 'none';
|
||||
document.getElementById('remove-image').style.display = 'block';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load article:', error);
|
||||
MSPE.showNotification('Failed to load article details', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveArticle(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const formData = new FormData(form);
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Saving...';
|
||||
|
||||
try {
|
||||
const result = await MSPE.API.upload('news.php', formData);
|
||||
|
||||
if (result && result.success) {
|
||||
MSPE.showNotification('Article saved successfully', 'success');
|
||||
loadArticles();
|
||||
showList();
|
||||
} else {
|
||||
MSPE.showNotification(result?.message || 'Failed to save article', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Save error:', error);
|
||||
MSPE.showNotification('An error occurred while saving', 'error');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fas fa-check"></i> Publish';
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteArticle(id) {
|
||||
if (!await MSPE.confirmDialog('Are you sure you want to delete this article?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await MSPE.API.delete(`news.php?id=${id}`);
|
||||
if (response && response.success) {
|
||||
MSPE.showNotification('Article deleted successfully', 'success');
|
||||
loadArticles();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete error:', error);
|
||||
MSPE.showNotification('Failed to delete article', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function applyBulkAction() {
|
||||
const action = document.getElementById('bulk-action').value;
|
||||
if (!action) {
|
||||
MSPE.showNotification('Please select a bulk action', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const selected = Array.from(document.querySelectorAll('.row-select:checked')).map(cb => cb.value);
|
||||
if (selected.length === 0) {
|
||||
MSPE.showNotification('No articles selected', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'delete') {
|
||||
if (!await MSPE.confirmDialog(`Delete ${selected.length} selected article(s)?`)) return;
|
||||
let success = 0;
|
||||
for (const id of selected) {
|
||||
try {
|
||||
const res = await MSPE.API.delete(`news.php?id=${id}`);
|
||||
if (res && res.success) success++;
|
||||
} catch (e) { /* continue */ }
|
||||
}
|
||||
MSPE.showNotification(`${success} article(s) deleted`, 'success');
|
||||
} else {
|
||||
// publish or draft
|
||||
let success = 0;
|
||||
for (const id of selected) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('id', id);
|
||||
formData.append('status', action === 'publish' ? 'published' : 'draft');
|
||||
const res = await MSPE.API.upload('news.php', formData);
|
||||
if (res && res.success) success++;
|
||||
} catch (e) { /* continue */ }
|
||||
}
|
||||
MSPE.showNotification(`${success} article(s) updated`, 'success');
|
||||
}
|
||||
|
||||
document.getElementById('select-all').checked = false;
|
||||
loadArticles();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+512
@@ -0,0 +1,512 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||
<title>Pages Management - MSPE Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha384-t1nt8BQoYMLFN5p42tRAtuAAFQaCQODekUVeKKZrEnEyp4H2R0RHFz0KWpmj7i8g" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/admin/css/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-wrapper">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<a href="/admin/dashboard.html" class="sidebar-logo">
|
||||
<i class="fas fa-cube"></i>
|
||||
<span>MSPE</span>
|
||||
</a>
|
||||
<button class="sidebar-toggle" id="sidebar-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Main</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/dashboard.html" class="nav-link">
|
||||
<i class="fas fa-th-large"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Content</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item active">
|
||||
<a href="/admin/pages.html" class="nav-link">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
<span>Pages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/services.html" class="nav-link">
|
||||
<i class="fas fa-concierge-bell"></i>
|
||||
<span>Services</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/portfolio.html" class="nav-link">
|
||||
<i class="fas fa-briefcase"></i>
|
||||
<span>Portfolio</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/news.html" class="nav-link">
|
||||
<i class="fas fa-newspaper"></i>
|
||||
<span>News & Events</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/testimonials.html" class="nav-link">
|
||||
<i class="fas fa-quote-right"></i>
|
||||
<span>Testimonials</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/team.html" class="nav-link">
|
||||
<i class="fas fa-users"></i>
|
||||
<span>Team</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Media</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/media.html" class="nav-link">
|
||||
<i class="fas fa-images"></i>
|
||||
<span>Media Library</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Inquiries</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/messages.html" class="nav-link">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<span>Messages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/subscribers.html" class="nav-link">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span>Subscribers</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/bookings.html" class="nav-link">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Settings</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings.html" class="nav-link">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span>Site Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/users.html" class="nav-link">
|
||||
<i class="fas fa-user-shield"></i>
|
||||
<span>Admin Users</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<a href="/index.html" class="view-site-btn" target="_blank">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
<span>View Website</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<!-- Header -->
|
||||
<header class="admin-header">
|
||||
<div class="header-left">
|
||||
<button class="mobile-toggle" id="mobile-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<h1 class="page-title">Pages Management</h1>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div class="header-user">
|
||||
<div class="user-avatar">
|
||||
<img src="https://ui-avatars.com/api/?name=Admin&background=0ea5e9&color=fff" alt="Admin">
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<span class="user-name">Admin</span>
|
||||
<span class="user-role">Administrator</span>
|
||||
</div>
|
||||
<div class="user-dropdown">
|
||||
<button class="dropdown-toggle">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="/admin/settings.html"><i class="fas fa-cog"></i> Settings</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" id="logout-btn"><i class="fas fa-sign-out-alt"></i> Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="content-wrapper">
|
||||
<div class="content-header">
|
||||
<p class="content-description">Manage your website pages and their content sections</p>
|
||||
</div>
|
||||
|
||||
<!-- Pages Grid (loaded dynamically) -->
|
||||
<div class="pages-grid" id="pages-grid">
|
||||
<div style="grid-column:1/-1;text-align:center;padding:3rem;color:var(--gray-400);">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||
<p style="margin-top:1rem;">Loading pages...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Global Sections -->
|
||||
<div class="card" style="margin-top: 2rem;">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-layer-group"></i> Global Sections</h3>
|
||||
<p class="card-subtitle">These sections appear across multiple pages</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="global-sections-grid" id="global-sections-grid">
|
||||
<div style="text-align:center;padding:2rem;color:var(--gray-400);grid-column:1/-1;">
|
||||
<i class="fas fa-spinner fa-spin"></i> Loading...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Page Editor Modal -->
|
||||
<div class="modal" id="page-modal">
|
||||
<div class="modal-overlay" onclick="closePageModal()"></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>
|
||||
</div>
|
||||
<div class="modal-body" style="max-height:70vh;overflow-y:auto;">
|
||||
<form id="page-form">
|
||||
<input type="hidden" id="page-slug">
|
||||
|
||||
<!-- SEO & Meta -->
|
||||
<div style="margin-bottom:1.5rem;">
|
||||
<h4 style="font-size:.875rem;font-weight:600;color:var(--dark);margin-bottom:.75rem;display:flex;align-items:center;gap:.5rem;">
|
||||
<i class="fas fa-search" style="color:var(--primary);"></i> SEO & Metadata
|
||||
</h4>
|
||||
<div class="form-group" style="margin-bottom:.75rem;">
|
||||
<label>Meta Title</label>
|
||||
<input type="text" class="form-control" id="page-meta-title" placeholder="Page title for search engines">
|
||||
<small style="color:var(--gray-400);font-size:.75rem;">Recommended: 50–60 characters</small>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:.75rem;">
|
||||
<label>Meta Description</label>
|
||||
<textarea class="form-control" id="page-meta-desc" rows="2" placeholder="Brief description for search results"></textarea>
|
||||
<small style="color:var(--gray-400);font-size:.75rem;">Recommended: 150–160 characters</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Status</label>
|
||||
<select class="form-control" id="page-status">
|
||||
<option value="published">Published</option>
|
||||
<option value="draft">Draft</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sections -->
|
||||
<div id="page-sections-editor"></div>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Global Section Editor Modal -->
|
||||
<div class="modal" id="global-modal">
|
||||
<div class="modal-overlay" onclick="closeGlobalModal()"></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>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="global-form">
|
||||
<input type="hidden" id="global-slug">
|
||||
<div id="global-fields-editor"></div>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/admin/js/admin.js"></script>
|
||||
<script>
|
||||
let allPages = [];
|
||||
let globalSections = [];
|
||||
let currentPageData = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadPages();
|
||||
loadGlobalSections();
|
||||
});
|
||||
|
||||
// ── Load & Render ────────────────────────────────────
|
||||
|
||||
async function loadPages() {
|
||||
try {
|
||||
const res = await MSPE.API.get('pages.php');
|
||||
if (res && res.success) {
|
||||
allPages = res.data;
|
||||
renderPages(allPages);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading pages:', err);
|
||||
document.getElementById('pages-grid').innerHTML =
|
||||
'<div class="empty-state" style="grid-column:1/-1;"><i class="fas fa-exclamation-circle"></i><p>Failed to load pages</p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderPages(pages) {
|
||||
const grid = document.getElementById('pages-grid');
|
||||
if (!pages.length) {
|
||||
grid.innerHTML = '<div class="empty-state" style="grid-column:1/-1;"><i class="fas fa-file-alt"></i><p>No pages found</p></div>';
|
||||
return;
|
||||
}
|
||||
grid.innerHTML = pages.map(page => {
|
||||
const sectionTags = page.sections.map(s => `<span class="section-tag">${s.label || s}</span>`).join('');
|
||||
const statusClass = page.status === 'draft' ? 'draft' : 'published';
|
||||
const statusLabel = page.status === 'draft' ? 'Draft' : 'Published';
|
||||
const lastSaved = page.updated_at ? `<span style="font-size:.7rem;color:var(--gray-400);display:block;margin-top:.25rem;">Last saved: ${MSPE.formatDate(page.updated_at)}</span>` : '';
|
||||
return `
|
||||
<div class="page-admin-card">
|
||||
<div class="page-admin-header">
|
||||
<div class="page-icon"><i class="fas ${page.icon || 'fa-file-alt'}"></i></div>
|
||||
<div class="page-status ${statusClass}"><i class="fas fa-circle"></i> ${statusLabel}</div>
|
||||
</div>
|
||||
<h3>${page.title}</h3>
|
||||
<p>${page.description || ''}${lastSaved}</p>
|
||||
<div class="page-sections">${sectionTags}</div>
|
||||
<div class="page-admin-actions">
|
||||
<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}')">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function loadGlobalSections() {
|
||||
try {
|
||||
const res = await MSPE.API.get('pages.php?type=global');
|
||||
if (res && res.success) {
|
||||
globalSections = res.data;
|
||||
renderGlobalSections(globalSections);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading global sections:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function renderGlobalSections(sections) {
|
||||
const grid = document.getElementById('global-sections-grid');
|
||||
grid.innerHTML = sections.map(s => `
|
||||
<div class="global-section-item">
|
||||
<i class="fas ${s.icon}"></i>
|
||||
<div>
|
||||
<h4>${s.label}</h4>
|
||||
<p>${s.description}</p>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline" onclick="editGlobalSection('${s.slug}')">Edit</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// ── Page Editor ──────────────────────────────────────
|
||||
|
||||
async function editPage(slug) {
|
||||
try {
|
||||
const res = await MSPE.API.get('pages.php?id=' + slug);
|
||||
if (!res || !res.success) {
|
||||
MSPE.showNotification('Failed to load page data', 'error');
|
||||
return;
|
||||
}
|
||||
currentPageData = res.data;
|
||||
document.getElementById('page-modal-title').textContent = 'Edit: ' + currentPageData.title;
|
||||
document.getElementById('page-slug').value = currentPageData.slug;
|
||||
document.getElementById('page-meta-title').value = currentPageData.meta_title || '';
|
||||
document.getElementById('page-meta-desc').value = currentPageData.meta_description || '';
|
||||
document.getElementById('page-status').value = currentPageData.status || 'published';
|
||||
|
||||
// Build section editors
|
||||
const container = document.getElementById('page-sections-editor');
|
||||
container.innerHTML = currentPageData.sections.map((section, si) => {
|
||||
const fields = section.fields.map((f, fi) => {
|
||||
const inputId = 'sect-' + section.key + '-' + f.name;
|
||||
if (f.type === 'textarea') {
|
||||
return `<div class="form-group" style="margin-bottom:.75rem;">
|
||||
<label>${f.label}</label>
|
||||
<textarea class="form-control section-field" id="${inputId}" data-section="${section.key}" data-field="${f.name}" rows="2" placeholder="${f.label}">${f.value || ''}</textarea>
|
||||
</div>`;
|
||||
}
|
||||
return `<div class="form-group" style="margin-bottom:.75rem;">
|
||||
<label>${f.label}</label>
|
||||
<input type="text" class="form-control section-field" id="${inputId}" data-section="${section.key}" data-field="${f.name}" value="${(f.value || '').replace(/"/g, '"')}" placeholder="${f.label}">
|
||||
</div>`;
|
||||
}).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')">
|
||||
<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>
|
||||
<div class="sect-body" style="padding:1rem;">
|
||||
${fields.length ? fields : '<p style="color:var(--gray-400);font-size:.8rem;">No editable fields for this section.</p>'}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
document.getElementById('page-modal').classList.add('active');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
MSPE.showNotification('Error loading page', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function closePageModal() {
|
||||
document.getElementById('page-modal').classList.remove('active');
|
||||
currentPageData = null;
|
||||
}
|
||||
|
||||
async function savePage() {
|
||||
const slug = document.getElementById('page-slug').value;
|
||||
if (!slug) return;
|
||||
|
||||
// Gather section field values
|
||||
const sectionsData = {};
|
||||
document.querySelectorAll('.section-field').forEach(el => {
|
||||
const sKey = el.dataset.section;
|
||||
const fName = el.dataset.field;
|
||||
if (!sectionsData[sKey]) sectionsData[sKey] = {};
|
||||
sectionsData[sKey][fName] = el.value;
|
||||
});
|
||||
|
||||
const payload = {
|
||||
slug,
|
||||
meta_title: document.getElementById('page-meta-title').value,
|
||||
meta_description: document.getElementById('page-meta-desc').value,
|
||||
status: document.getElementById('page-status').value,
|
||||
sections_data: sectionsData,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await MSPE.API.post('pages.php', payload);
|
||||
if (res && res.success) {
|
||||
MSPE.showNotification('Page saved!', 'success');
|
||||
closePageModal();
|
||||
loadPages();
|
||||
} else {
|
||||
MSPE.showNotification(res?.message || 'Save failed', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
MSPE.showNotification('Error saving page', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Global Section Editor ────────────────────────────
|
||||
|
||||
function editGlobalSection(slug) {
|
||||
const section = globalSections.find(s => s.slug === slug);
|
||||
if (!section) return;
|
||||
|
||||
document.getElementById('global-modal-title').textContent = 'Edit: ' + section.label;
|
||||
document.getElementById('global-slug').value = slug;
|
||||
|
||||
const container = document.getElementById('global-fields-editor');
|
||||
container.innerHTML = section.fields.map(f => {
|
||||
const inputId = 'gf-' + slug + '-' + f.name;
|
||||
if (f.type === 'textarea') {
|
||||
return `<div class="form-group" style="margin-bottom:.75rem;">
|
||||
<label>${f.label}</label>
|
||||
<textarea class="form-control global-field" id="${inputId}" data-field="${f.name}" rows="2" placeholder="${f.label}">${f.value || ''}</textarea>
|
||||
</div>`;
|
||||
}
|
||||
return `<div class="form-group" style="margin-bottom:.75rem;">
|
||||
<label>${f.label}</label>
|
||||
<input type="text" class="form-control global-field" id="${inputId}" data-field="${f.name}" value="${(f.value || '').replace(/"/g, '"')}" placeholder="${f.label}">
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
document.getElementById('global-modal').classList.add('active');
|
||||
}
|
||||
|
||||
function closeGlobalModal() {
|
||||
document.getElementById('global-modal').classList.remove('active');
|
||||
}
|
||||
|
||||
async function saveGlobalSection() {
|
||||
const slug = document.getElementById('global-slug').value;
|
||||
if (!slug) return;
|
||||
|
||||
const fields = {};
|
||||
document.querySelectorAll('.global-field').forEach(el => {
|
||||
fields[el.dataset.field] = el.value;
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await MSPE.API.post('pages.php?type=global', { section_slug: slug, fields });
|
||||
if (res && res.success) {
|
||||
MSPE.showNotification('Section saved!', 'success');
|
||||
closeGlobalModal();
|
||||
loadGlobalSections();
|
||||
} else {
|
||||
MSPE.showNotification(res?.message || 'Save failed', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
MSPE.showNotification('Error saving section', 'error');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+708
@@ -0,0 +1,708 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||
<title>Portfolio Management - MSPE Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha384-t1nt8BQoYMLFN5p42tRAtuAAFQaCQODekUVeKKZrEnEyp4H2R0RHFz0KWpmj7i8g" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/admin/css/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-wrapper">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<a href="/admin/dashboard.html" class="sidebar-logo">
|
||||
<i class="fas fa-cube"></i>
|
||||
<span>MSPE</span>
|
||||
</a>
|
||||
<button class="sidebar-toggle" id="sidebar-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Main</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/dashboard.html" class="nav-link">
|
||||
<i class="fas fa-th-large"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Content</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/pages.html" class="nav-link">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
<span>Pages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/services.html" class="nav-link">
|
||||
<i class="fas fa-concierge-bell"></i>
|
||||
<span>Services</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a href="/admin/portfolio.html" class="nav-link">
|
||||
<i class="fas fa-briefcase"></i>
|
||||
<span>Portfolio</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/news.html" class="nav-link">
|
||||
<i class="fas fa-newspaper"></i>
|
||||
<span>News & Events</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/testimonials.html" class="nav-link">
|
||||
<i class="fas fa-quote-right"></i>
|
||||
<span>Testimonials</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/team.html" class="nav-link">
|
||||
<i class="fas fa-users"></i>
|
||||
<span>Team</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Media</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/media.html" class="nav-link">
|
||||
<i class="fas fa-images"></i>
|
||||
<span>Media Library</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Inquiries</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/messages.html" class="nav-link">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<span>Messages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/subscribers.html" class="nav-link">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span>Subscribers</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/bookings.html" class="nav-link">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Settings</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings.html" class="nav-link">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span>Site Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/users.html" class="nav-link">
|
||||
<i class="fas fa-user-shield"></i>
|
||||
<span>Admin Users</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<a href="/index.html" class="view-site-btn" target="_blank">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
<span>View Website</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<!-- Header -->
|
||||
<header class="admin-header">
|
||||
<div class="header-left">
|
||||
<button class="mobile-toggle" id="mobile-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<h1 class="page-title">Portfolio Management</h1>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div class="header-user">
|
||||
<div class="user-avatar">
|
||||
<img src="https://ui-avatars.com/api/?name=Admin&background=0ea5e9&color=fff" alt="Admin">
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<span class="user-name">Admin</span>
|
||||
<span class="user-role">Administrator</span>
|
||||
</div>
|
||||
<div class="user-dropdown">
|
||||
<button class="dropdown-toggle">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="/admin/settings.html"><i class="fas fa-cog"></i> Settings</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" id="logout-btn"><i class="fas fa-sign-out-alt"></i> Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="content-wrapper">
|
||||
<!-- List View -->
|
||||
<div id="list-view">
|
||||
<div class="content-header">
|
||||
<div class="content-filters">
|
||||
<div class="filter-group">
|
||||
<select id="category-filter">
|
||||
<option value="">All Categories</option>
|
||||
<option value="it-support">IT Support</option>
|
||||
<option value="cybersecurity">Cybersecurity</option>
|
||||
<option value="cloud">Cloud Services</option>
|
||||
<option value="consulting">Consulting</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<select id="status-filter">
|
||||
<option value="">All Status</option>
|
||||
<option value="published">Published</option>
|
||||
<option value="draft">Draft</option>
|
||||
<option value="featured">Featured</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="search-box">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="text" id="search-input" placeholder="Search projects...">
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" id="add-new-btn">
|
||||
<i class="fas fa-plus"></i> Add Project
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="portfolio-grid" id="portfolio-grid">
|
||||
<div style="grid-column:1/-1;text-align:center;padding:3rem;color:var(--gray-400);">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||
<p style="margin-top:1rem;">Loading projects...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editor View -->
|
||||
<div id="editor-view" style="display: none;">
|
||||
<div class="content-header">
|
||||
<button class="btn btn-outline" id="back-to-list">
|
||||
<i class="fas fa-arrow-left"></i> Back to List
|
||||
</button>
|
||||
<div class="editor-actions">
|
||||
<button class="btn btn-outline" id="save-draft-btn">Save Draft</button>
|
||||
<button class="btn btn-primary" id="publish-btn">Publish</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-grid">
|
||||
<div class="editor-main">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="project-title">Project Title</label>
|
||||
<input type="text" id="project-title" class="form-control" placeholder="Enter project title">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="project-description">Short Description</label>
|
||||
<textarea id="project-description" class="form-control" rows="3" placeholder="Brief project description..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="project-content">Full Description</label>
|
||||
<textarea id="project-content" class="form-control editor-textarea" rows="10" placeholder="Detailed project description..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="project-client">Client Name</label>
|
||||
<input type="text" id="project-client" class="form-control" placeholder="Client company name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="project-url">Project URL</label>
|
||||
<input type="url" id="project-url" class="form-control" placeholder="https://...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Technologies Used</label>
|
||||
<input type="text" id="project-technologies" class="form-control" placeholder="Azure, AWS, Docker, Kubernetes...">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Project Results</label>
|
||||
<div class="results-grid">
|
||||
<div class="result-item">
|
||||
<input type="text" id="result-metric-0" placeholder="Metric (e.g., 99.9%)" class="form-control">
|
||||
<input type="text" id="result-label-0" placeholder="Label (e.g., Uptime)" class="form-control">
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<input type="text" id="result-metric-1" placeholder="Metric (e.g., 50%)" class="form-control">
|
||||
<input type="text" id="result-label-1" placeholder="Label (e.g., Cost Reduction)" class="form-control">
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<input type="text" id="result-metric-2" placeholder="Metric" class="form-control">
|
||||
<input type="text" id="result-label-2" placeholder="Label" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-sidebar">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Project Settings</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="project-category">Category</label>
|
||||
<select id="project-category" class="form-control">
|
||||
<option value="">Select category</option>
|
||||
<option value="it-support">IT Support</option>
|
||||
<option value="cybersecurity">Cybersecurity</option>
|
||||
<option value="cloud">Cloud Services</option>
|
||||
<option value="consulting">Consulting</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="project-status">Status</label>
|
||||
<select id="project-status" class="form-control">
|
||||
<option value="draft">Draft</option>
|
||||
<option value="published">Published</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="checkbox-wrapper">
|
||||
<input type="checkbox" id="project-featured">
|
||||
<span>Featured Project</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="project-date">Completion Date</label>
|
||||
<input type="date" id="project-date" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Featured Image</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="image-upload-area" id="image-upload">
|
||||
<i class="fas fa-cloud-upload-alt"></i>
|
||||
<p>Click or drag to upload</p>
|
||||
<span>PNG, JPG up to 5MB</span>
|
||||
</div>
|
||||
<input type="file" id="image-input" accept="image/*" hidden>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Gallery Images</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="gallery-upload-area" id="gallery-upload">
|
||||
<i class="fas fa-images"></i>
|
||||
<p>Add gallery images</p>
|
||||
</div>
|
||||
<input type="file" id="gallery-input" accept="image/*" multiple hidden>
|
||||
<div class="gallery-preview" id="gallery-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/admin/js/admin.js"></script>
|
||||
<script>
|
||||
let allProjects = [];
|
||||
let galleryFiles = []; // new File objects queued for upload
|
||||
let existingGallery = []; // gallery URLs already saved in DB
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadProjects();
|
||||
setupEventListeners();
|
||||
|
||||
// Check URL params
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const editId = urlParams.get('edit');
|
||||
const action = urlParams.get('action');
|
||||
|
||||
if (editId) {
|
||||
editProject(editId);
|
||||
} else if (action === 'add') {
|
||||
showEditor();
|
||||
}
|
||||
});
|
||||
|
||||
function setupEventListeners() {
|
||||
document.getElementById('add-new-btn').addEventListener('click', showEditor);
|
||||
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'));
|
||||
|
||||
// Filters
|
||||
document.getElementById('category-filter').addEventListener('change', filterProjects);
|
||||
document.getElementById('status-filter').addEventListener('change', filterProjects);
|
||||
document.getElementById('search-input').addEventListener('input', MSPE.debounce(filterProjects, 300));
|
||||
|
||||
// Image upload
|
||||
const imageUpload = document.getElementById('image-upload');
|
||||
const imageInput = document.getElementById('image-input');
|
||||
|
||||
imageUpload.addEventListener('click', () => imageInput.click());
|
||||
imageInput.addEventListener('change', handleImageSelect);
|
||||
|
||||
// Gallery upload
|
||||
const galleryUpload = document.getElementById('gallery-upload');
|
||||
const galleryInput = document.getElementById('gallery-input');
|
||||
galleryUpload.addEventListener('click', () => galleryInput.click());
|
||||
galleryInput.addEventListener('change', handleGallerySelect);
|
||||
}
|
||||
|
||||
async function loadProjects() {
|
||||
try {
|
||||
const response = await MSPE.API.get('portfolio.php');
|
||||
if (response && response.success) {
|
||||
allProjects = response.data;
|
||||
renderProjects(allProjects);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load projects:', error);
|
||||
document.getElementById('portfolio-grid').innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<p>Failed to load projects</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderProjects(projects) {
|
||||
const container = document.getElementById('portfolio-grid');
|
||||
|
||||
if (projects.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-briefcase"></i>
|
||||
<p>No projects found</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = projects.map(project => `
|
||||
<div class="portfolio-admin-card" data-id="${project.id}">
|
||||
<div class="portfolio-admin-image">
|
||||
<img src="${project.image || 'https://via.placeholder.com/400x300'}" alt="${project.title}">
|
||||
<span class="portfolio-status ${project.status}">${project.status}</span>
|
||||
${project.featured ? '<span class="portfolio-featured"><i class="fas fa-star"></i></span>' : ''}
|
||||
</div>
|
||||
<div class="portfolio-admin-content">
|
||||
<h3>${project.title}</h3>
|
||||
<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}')">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline btn-danger" onclick="deleteProject('${project.id}')">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function filterProjects() {
|
||||
const category = document.getElementById('category-filter').value;
|
||||
const status = document.getElementById('status-filter').value;
|
||||
const search = document.getElementById('search-input').value.toLowerCase();
|
||||
|
||||
let filtered = allProjects;
|
||||
|
||||
if (category) filtered = filtered.filter(p => p.category === category);
|
||||
|
||||
if (status) {
|
||||
if (status === 'featured') {
|
||||
filtered = filtered.filter(p => p.featured);
|
||||
} else {
|
||||
filtered = filtered.filter(p => p.status === status);
|
||||
}
|
||||
}
|
||||
|
||||
if (search) {
|
||||
filtered = filtered.filter(p =>
|
||||
p.title.toLowerCase().includes(search) ||
|
||||
(p.client && p.client.toLowerCase().includes(search))
|
||||
);
|
||||
}
|
||||
|
||||
renderProjects(filtered);
|
||||
}
|
||||
|
||||
function showEditor() {
|
||||
document.getElementById('list-view').style.display = 'none';
|
||||
document.getElementById('editor-view').style.display = 'block';
|
||||
resetForm();
|
||||
}
|
||||
|
||||
function showList() {
|
||||
document.getElementById('editor-view').style.display = 'none';
|
||||
document.getElementById('list-view').style.display = 'block';
|
||||
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.delete('edit');
|
||||
url.searchParams.delete('action');
|
||||
window.history.pushState({}, '', url);
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
// Inputs
|
||||
['project-title', 'project-description', 'project-content', 'project-client', 'project-url', 'project-technologies', 'project-date'].forEach(id => {
|
||||
document.getElementById(id).value = '';
|
||||
});
|
||||
document.getElementById('project-category').value = '';
|
||||
document.getElementById('project-status').value = 'draft';
|
||||
document.getElementById('project-featured').checked = false;
|
||||
|
||||
// Image reset
|
||||
const uploadArea = document.getElementById('image-upload');
|
||||
uploadArea.innerHTML = `
|
||||
<i class="fas fa-cloud-upload-alt"></i>
|
||||
<p>Click or drag to upload</p>
|
||||
<span>PNG, JPG up to 5MB</span>
|
||||
`;
|
||||
uploadArea.style.backgroundImage = '';
|
||||
document.getElementById('image-input').value = '';
|
||||
|
||||
// Clear results grid
|
||||
[0, 1, 2].forEach(i => {
|
||||
document.getElementById(`result-metric-${i}`).value = '';
|
||||
document.getElementById(`result-label-${i}`).value = '';
|
||||
});
|
||||
|
||||
// Clear gallery
|
||||
galleryFiles = [];
|
||||
existingGallery = [];
|
||||
document.getElementById('gallery-preview').innerHTML = '';
|
||||
document.getElementById('gallery-input').value = '';
|
||||
|
||||
// Remove ID if any
|
||||
delete document.getElementById('editor-view').dataset.id;
|
||||
}
|
||||
|
||||
async function editProject(id) {
|
||||
try {
|
||||
const response = await MSPE.API.get(`portfolio.php?id=${id}`);
|
||||
if (response && response.success) {
|
||||
const project = response.data;
|
||||
|
||||
showEditor();
|
||||
document.getElementById('editor-view').dataset.id = project.id;
|
||||
|
||||
document.getElementById('project-title').value = project.title;
|
||||
document.getElementById('project-description').value = project.description || '';
|
||||
document.getElementById('project-content').value = project.content || '';
|
||||
document.getElementById('project-client').value = project.client || '';
|
||||
document.getElementById('project-url').value = project.url || '';
|
||||
document.getElementById('project-technologies').value = project.technologies || '';
|
||||
document.getElementById('project-date').value = project.completion_date || '';
|
||||
document.getElementById('project-category').value = project.category;
|
||||
document.getElementById('project-status').value = project.status;
|
||||
document.getElementById('project-featured').checked = project.featured == 1; // Loose comparison for string '1'
|
||||
|
||||
if (project.image) {
|
||||
const uploadArea = document.getElementById('image-upload');
|
||||
uploadArea.innerHTML = '';
|
||||
uploadArea.style.backgroundImage = `url(${project.image})`;
|
||||
uploadArea.style.backgroundSize = 'cover';
|
||||
uploadArea.style.backgroundPosition = 'center';
|
||||
}
|
||||
|
||||
// Restore results grid
|
||||
const rawResults = project.results;
|
||||
const results = Array.isArray(rawResults) ? rawResults :
|
||||
(rawResults ? JSON.parse(rawResults) : []);
|
||||
[0, 1, 2].forEach(i => {
|
||||
const r = results[i] || {};
|
||||
document.getElementById(`result-metric-${i}`).value = r.metric || '';
|
||||
document.getElementById(`result-label-${i}`).value = r.label || '';
|
||||
});
|
||||
|
||||
// Restore gallery
|
||||
existingGallery = Array.isArray(project.gallery) ? [...project.gallery] : [];
|
||||
galleryFiles = [];
|
||||
const galleryPreview = document.getElementById('gallery-preview');
|
||||
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>
|
||||
</div>`
|
||||
).join('');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load project:', error);
|
||||
MSPE.showNotification('Failed to load project details', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function handleImageSelect(e) {
|
||||
if (this.files && this.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
const uploadArea = document.getElementById('image-upload');
|
||||
uploadArea.innerHTML = '';
|
||||
uploadArea.style.backgroundImage = `url(${e.target.result})`;
|
||||
uploadArea.style.backgroundSize = 'cover';
|
||||
uploadArea.style.backgroundPosition = 'center';
|
||||
};
|
||||
reader.readAsDataURL(this.files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleGallerySelect() {
|
||||
const files = Array.from(this.files);
|
||||
if (!files.length) return;
|
||||
const preview = document.getElementById('gallery-preview');
|
||||
files.forEach(file => {
|
||||
const idx = galleryFiles.length;
|
||||
galleryFiles.push(file);
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(ev) {
|
||||
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>`;
|
||||
preview.appendChild(item);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
this.value = ''; // reset so same files can be re-selected
|
||||
}
|
||||
|
||||
function removeGalleryItem(btn) {
|
||||
const item = btn.closest('.gallery-preview-item');
|
||||
const idx = parseInt(item.dataset.galleryIdx);
|
||||
if (!isNaN(idx) && galleryFiles[idx]) galleryFiles[idx] = null;
|
||||
const url = item.dataset.url;
|
||||
if (url) existingGallery = existingGallery.filter(u => u !== url);
|
||||
item.remove();
|
||||
}
|
||||
|
||||
async function saveProject(status) {
|
||||
const id = document.getElementById('editor-view').dataset.id;
|
||||
const formData = new FormData();
|
||||
|
||||
if (id) formData.append('id', id);
|
||||
|
||||
formData.append('title', document.getElementById('project-title').value);
|
||||
formData.append('description', document.getElementById('project-description').value);
|
||||
formData.append('content', document.getElementById('project-content').value);
|
||||
formData.append('client', document.getElementById('project-client').value);
|
||||
formData.append('url', document.getElementById('project-url').value);
|
||||
formData.append('technologies', document.getElementById('project-technologies').value);
|
||||
formData.append('completion_date', document.getElementById('project-date').value);
|
||||
formData.append('category', document.getElementById('project-category').value);
|
||||
formData.append('status', status); // Use passed status
|
||||
formData.append('featured', document.getElementById('project-featured').checked ? 1 : 0);
|
||||
|
||||
const imageInput = document.getElementById('image-input');
|
||||
if (imageInput.files[0]) {
|
||||
formData.append('image', imageInput.files[0]);
|
||||
}
|
||||
|
||||
// Serialize results grid
|
||||
const results = [0, 1, 2].map(i => ({
|
||||
metric: document.getElementById(`result-metric-${i}`)?.value?.trim() || '',
|
||||
label: document.getElementById(`result-label-${i}`)?.value?.trim() || ''
|
||||
})).filter(r => r.metric || r.label);
|
||||
formData.append('results', JSON.stringify(results));
|
||||
|
||||
// Gallery — new uploads + existing to keep
|
||||
galleryFiles.forEach(file => { if (file) formData.append('gallery_images[]', file); });
|
||||
formData.append('gallery_keep', JSON.stringify(existingGallery));
|
||||
|
||||
try {
|
||||
const result = await MSPE.API.upload('portfolio.php', formData);
|
||||
|
||||
if (result && result.success) {
|
||||
MSPE.showNotification('Project saved successfully', 'success');
|
||||
loadProjects();
|
||||
showList();
|
||||
} else {
|
||||
MSPE.showNotification(result?.message || 'Failed to save project', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Save error:', error);
|
||||
MSPE.showNotification('An error occurred while saving', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteProject(id) {
|
||||
if (!await MSPE.confirmDialog('Are you sure you want to delete this project?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await MSPE.API.delete(`portfolio.php?id=${id}`);
|
||||
if (response && response.success) {
|
||||
MSPE.showNotification('Project deleted successfully', 'success');
|
||||
loadProjects();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete error:', error);
|
||||
MSPE.showNotification('Failed to delete project', 'error');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+147
@@ -0,0 +1,147 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||
<title>Reset Password - MSPE Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha384-t1nt8BQoYMLFN5p42tRAtuAAFQaCQODekUVeKKZrEnEyp4H2R0RHFz0KWpmj7i8g" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/admin/css/admin.css">
|
||||
</head>
|
||||
<body class="login-page">
|
||||
<div class="login-container">
|
||||
<div class="login-card" style="max-width: 520px;">
|
||||
<div class="login-header">
|
||||
<div class="login-logo">
|
||||
<i class="fas fa-key"></i>
|
||||
<span>MSPE</span>
|
||||
</div>
|
||||
<h1 id="page-title">Reset Password</h1>
|
||||
<p id="page-subtitle">Request a secure password reset link.</p>
|
||||
</div>
|
||||
|
||||
<form id="request-form" class="login-form">
|
||||
<div class="form-group">
|
||||
<label for="identity">Email or Username</label>
|
||||
<div class="input-icon">
|
||||
<i class="fas fa-user"></i>
|
||||
<input type="text" id="identity" required placeholder="admin@company.com or username">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
<span>Send Reset Link</span>
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form id="reset-form" class="login-form" style="display:none;">
|
||||
<div class="form-group">
|
||||
<label for="new-password">New Password</label>
|
||||
<div class="input-icon">
|
||||
<i class="fas fa-lock"></i>
|
||||
<input type="password" id="new-password" required minlength="8" placeholder="Minimum 8 characters">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirm-password">Confirm New Password</label>
|
||||
<div class="input-icon">
|
||||
<i class="fas fa-lock"></i>
|
||||
<input type="password" id="confirm-password" required minlength="8" placeholder="Repeat password">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
<span>Update Password</span>
|
||||
<i class="fas fa-check"></i>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="login-message" id="reset-message" style="display:none;"></div>
|
||||
|
||||
<div class="login-footer">
|
||||
<p><a href="/admin/index.html">Back to login</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const token = params.get('token');
|
||||
|
||||
const requestForm = document.getElementById('request-form');
|
||||
const resetForm = document.getElementById('reset-form');
|
||||
const message = document.getElementById('reset-message');
|
||||
|
||||
if (token) {
|
||||
document.getElementById('page-title').textContent = 'Set New Password';
|
||||
document.getElementById('page-subtitle').textContent = 'Enter your new password below.';
|
||||
requestForm.style.display = 'none';
|
||||
resetForm.style.display = 'block';
|
||||
}
|
||||
|
||||
function showMessage(text, type = 'success') {
|
||||
message.className = 'login-message ' + type;
|
||||
message.innerHTML = `<i class="fas fa-${type === 'success' ? 'check-circle' : 'exclamation-circle'}"></i> ${text}`;
|
||||
message.style.display = 'block';
|
||||
}
|
||||
|
||||
requestForm.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
const identity = document.getElementById('identity').value.trim();
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/auth.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'request_password_reset', identity })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
showMessage('If the account exists, a reset email has been sent.', 'success');
|
||||
} else {
|
||||
showMessage(data.message || 'Failed to request reset.', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showMessage('Network error. Please try again.', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
resetForm.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const newPassword = document.getElementById('new-password').value;
|
||||
const confirmPassword = document.getElementById('confirm-password').value;
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
showMessage('Passwords do not match.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/auth.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'reset_password',
|
||||
token,
|
||||
new_password: newPassword
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
showMessage('Password updated. Redirecting to login...', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/admin/index.html';
|
||||
}, 1200);
|
||||
} else {
|
||||
showMessage(data.message || 'Could not reset password.', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showMessage('Network error. Please try again.', 'error');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+411
@@ -0,0 +1,411 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||
<title>Services Management - MSPE Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha384-t1nt8BQoYMLFN5p42tRAtuAAFQaCQODekUVeKKZrEnEyp4H2R0RHFz0KWpmj7i8g" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/admin/css/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-wrapper">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<a href="/admin/dashboard.html" class="sidebar-logo">
|
||||
<i class="fas fa-cube"></i>
|
||||
<span>MSPE</span>
|
||||
</a>
|
||||
<button class="sidebar-toggle" id="sidebar-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Main</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/dashboard.html" class="nav-link">
|
||||
<i class="fas fa-th-large"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Content</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/pages.html" class="nav-link">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
<span>Pages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a href="/admin/services.html" class="nav-link">
|
||||
<i class="fas fa-concierge-bell"></i>
|
||||
<span>Services</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/portfolio.html" class="nav-link">
|
||||
<i class="fas fa-briefcase"></i>
|
||||
<span>Portfolio</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/news.html" class="nav-link">
|
||||
<i class="fas fa-newspaper"></i>
|
||||
<span>News & Events</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/testimonials.html" class="nav-link">
|
||||
<i class="fas fa-quote-right"></i>
|
||||
<span>Testimonials</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/team.html" class="nav-link">
|
||||
<i class="fas fa-users"></i>
|
||||
<span>Team</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Media</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/media.html" class="nav-link">
|
||||
<i class="fas fa-images"></i>
|
||||
<span>Media Library</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Inquiries</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/messages.html" class="nav-link">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<span>Messages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/subscribers.html" class="nav-link">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span>Subscribers</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/bookings.html" class="nav-link">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Settings</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings.html" class="nav-link">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span>Site Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/users.html" class="nav-link">
|
||||
<i class="fas fa-user-shield"></i>
|
||||
<span>Admin Users</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<a href="/index.html" class="view-site-btn" target="_blank">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
<span>View Website</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<!-- Header -->
|
||||
<header class="admin-header">
|
||||
<div class="header-left">
|
||||
<button class="mobile-toggle" id="mobile-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<h1 class="page-title">Services Management</h1>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div class="header-user">
|
||||
<div class="user-avatar">
|
||||
<img src="https://ui-avatars.com/api/?name=Admin&background=0ea5e9&color=fff" alt="Admin">
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<span class="user-name">Admin</span>
|
||||
<span class="user-role">Administrator</span>
|
||||
</div>
|
||||
<div class="user-dropdown">
|
||||
<button class="dropdown-toggle">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="/admin/settings.html"><i class="fas fa-cog"></i> Settings</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" id="logout-btn"><i class="fas fa-sign-out-alt"></i> Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="content-wrapper">
|
||||
<div class="content-header">
|
||||
<p class="content-description">Manage your service offerings displayed on the website</p>
|
||||
<button class="btn btn-primary" id="add-service-btn">
|
||||
<i class="fas fa-plus"></i> Add Service
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="services-admin-grid" id="services-admin-grid">
|
||||
<div style="grid-column:1/-1;text-align:center;padding:3rem;color:var(--gray-400);">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||
<p style="margin-top:1rem;">Loading services...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Service Edit Modal -->
|
||||
<div class="modal" id="service-modal">
|
||||
<div class="modal-overlay"></div>
|
||||
<div class="modal-content modal-lg">
|
||||
<div class="modal-header">
|
||||
<h2>Edit Service</h2>
|
||||
<button class="modal-close" onclick="closeModal()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="service-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Service Name</label>
|
||||
<input type="text" class="form-control" id="service-name" placeholder="Service name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Icon</label>
|
||||
<select class="form-control" id="service-icon">
|
||||
<option value="fa-laptop-code">Laptop Code</option>
|
||||
<option value="fa-shield-alt">Shield</option>
|
||||
<option value="fa-cloud">Cloud</option>
|
||||
<option value="fa-lightbulb">Lightbulb</option>
|
||||
<option value="fa-server">Server</option>
|
||||
<option value="fa-database">Database</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Short Description</label>
|
||||
<textarea class="form-control" id="service-description" rows="3" placeholder="Brief service description..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Features (one per line)</label>
|
||||
<textarea class="form-control" id="service-features" rows="6" placeholder="24/7 Remote Support
|
||||
On-Site Support
|
||||
Proactive Monitoring
|
||||
Network Management"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Starting Price</label>
|
||||
<input type="text" class="form-control" id="service-price" placeholder="$150/month or Custom">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>CTA Button Text</label>
|
||||
<input type="text" class="form-control" id="service-cta" placeholder="Contact Us">
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/admin/js/admin.js"></script>
|
||||
<script>
|
||||
let allServices = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadServices();
|
||||
document.getElementById('add-service-btn').addEventListener('click', openAddServiceModal);
|
||||
});
|
||||
|
||||
async function loadServices() {
|
||||
try {
|
||||
const response = await MSPE.API.get('services.php');
|
||||
if (response && response.success) {
|
||||
allServices = response.data;
|
||||
renderServices(allServices);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading services:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderServices(services) {
|
||||
const container = document.querySelector('#services-admin-grid');
|
||||
if (services.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-concierge-bell"></i>
|
||||
<p>No services found</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = services.map(service => `
|
||||
<div class="service-admin-card ${service.active ? '' : 'opacity-50'}">
|
||||
<div class="service-admin-header">
|
||||
<div class="service-icon" style="background: var(--primary); color: white;">
|
||||
<i class="fas ${service.icon || 'fa-cogs'}"></i>
|
||||
</div>
|
||||
<div class="service-actions">
|
||||
<button class="btn btn-sm btn-icon" onclick="editService('${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)">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
<button class="btn btn-sm btn-icon text-danger" onclick="deleteService('${service.id}')">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<h3>${service.name}</h3>
|
||||
<p>${service.description || ''}</p>
|
||||
${service.features ? `<ul class="service-features-list">${service.features.split('\n').filter(f => f.trim()).map(f => `<li><i class="fas fa-check"></i> ${f.trim()}</li>`).join('')}</ul>` : ''}
|
||||
<div class="service-meta">
|
||||
<span><i class="fas fa-dollar-sign"></i> ${service.price || 'Custom'}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function openAddServiceModal() {
|
||||
const modal = document.getElementById('service-modal');
|
||||
const form = document.getElementById('service-form');
|
||||
form.reset();
|
||||
form.dataset.id = '';
|
||||
modal.querySelector('.modal-header h2').textContent = 'Add Service';
|
||||
modal.classList.add('active');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('service-modal').classList.remove('active');
|
||||
}
|
||||
|
||||
function editService(id) {
|
||||
const service = allServices.find(s => s.id === id);
|
||||
if (!service) return;
|
||||
|
||||
const modal = document.getElementById('service-modal');
|
||||
const form = document.getElementById('service-form');
|
||||
|
||||
form.dataset.id = id;
|
||||
document.getElementById('service-name').value = service.name;
|
||||
document.getElementById('service-icon').value = service.icon || 'fa-laptop-code';
|
||||
document.getElementById('service-description').value = service.description || '';
|
||||
document.getElementById('service-features').value = service.features || '';
|
||||
document.getElementById('service-price').value = service.price || '';
|
||||
document.getElementById('service-cta').value = service.cta_text || '';
|
||||
|
||||
modal.querySelector('.modal-header h2').textContent = 'Edit Service';
|
||||
modal.classList.add('active');
|
||||
}
|
||||
|
||||
async function saveService() {
|
||||
const form = document.getElementById('service-form');
|
||||
const id = form.dataset.id;
|
||||
|
||||
const formData = new FormData();
|
||||
if (id) formData.append('id', id);
|
||||
|
||||
formData.append('name', document.getElementById('service-name').value);
|
||||
formData.append('icon', document.getElementById('service-icon').value);
|
||||
formData.append('description', document.getElementById('service-description').value);
|
||||
formData.append('features', document.getElementById('service-features').value);
|
||||
formData.append('price', document.getElementById('service-price').value);
|
||||
formData.append('cta_text', document.getElementById('service-cta').value);
|
||||
|
||||
try {
|
||||
const result = await MSPE.API.upload('services.php', formData);
|
||||
if (result && result.success) {
|
||||
MSPE.showNotification('Service saved successfully', 'success');
|
||||
closeModal();
|
||||
loadServices();
|
||||
} else {
|
||||
MSPE.showNotification(result?.message || 'Error saving service', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
MSPE.showNotification('Error saving service', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleService(id, active) {
|
||||
const formData = new FormData();
|
||||
formData.append('id', id);
|
||||
formData.append('active', active);
|
||||
|
||||
try {
|
||||
await MSPE.API.upload('services.php', formData);
|
||||
MSPE.showNotification(`Service ${active ? 'activated' : 'deactivated'}`, 'success');
|
||||
} catch (error) {
|
||||
MSPE.showNotification('Error updating status', 'error');
|
||||
loadServices();
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteService(id) {
|
||||
if (!await MSPE.confirmDialog('Delete this service?')) return;
|
||||
|
||||
try {
|
||||
const response = await MSPE.API.delete(`services.php?id=${id}`);
|
||||
if (response.success) {
|
||||
MSPE.showNotification('Service deleted', 'success');
|
||||
loadServices();
|
||||
}
|
||||
} catch (error) {
|
||||
MSPE.showNotification('Error deleting service', 'error');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+910
@@ -0,0 +1,910 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||
<title>Site Settings - MSPE Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha384-t1nt8BQoYMLFN5p42tRAtuAAFQaCQODekUVeKKZrEnEyp4H2R0RHFz0KWpmj7i8g" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/admin/css/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-wrapper">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<a href="/admin/dashboard.html" class="sidebar-logo">
|
||||
<i class="fas fa-cube"></i>
|
||||
<span>MSPE</span>
|
||||
</a>
|
||||
<button class="sidebar-toggle" id="sidebar-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Main</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/dashboard.html" class="nav-link">
|
||||
<i class="fas fa-th-large"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Content</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/pages.html" class="nav-link">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
<span>Pages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/services.html" class="nav-link">
|
||||
<i class="fas fa-concierge-bell"></i>
|
||||
<span>Services</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/portfolio.html" class="nav-link">
|
||||
<i class="fas fa-briefcase"></i>
|
||||
<span>Portfolio</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/news.html" class="nav-link">
|
||||
<i class="fas fa-newspaper"></i>
|
||||
<span>News & Events</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/testimonials.html" class="nav-link">
|
||||
<i class="fas fa-quote-right"></i>
|
||||
<span>Testimonials</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/team.html" class="nav-link">
|
||||
<i class="fas fa-users"></i>
|
||||
<span>Team</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Media</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/media.html" class="nav-link">
|
||||
<i class="fas fa-images"></i>
|
||||
<span>Media Library</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Inquiries</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/messages.html" class="nav-link">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<span>Messages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/subscribers.html" class="nav-link">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span>Subscribers</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/bookings.html" class="nav-link">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Settings</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item active">
|
||||
<a href="/admin/settings.html" class="nav-link">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span>Site Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/users.html" class="nav-link">
|
||||
<i class="fas fa-user-shield"></i>
|
||||
<span>Admin Users</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<a href="/index.html" class="view-site-btn" target="_blank">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
<span>View Website</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<!-- Header -->
|
||||
<header class="admin-header">
|
||||
<div class="header-left">
|
||||
<button class="mobile-toggle" id="mobile-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<h1 class="page-title">Site Settings</h1>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div class="header-user">
|
||||
<div class="user-avatar">
|
||||
<img src="https://ui-avatars.com/api/?name=Admin&background=0ea5e9&color=fff" alt="Admin">
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<span class="user-name">Admin</span>
|
||||
<span class="user-role">Administrator</span>
|
||||
</div>
|
||||
<div class="user-dropdown">
|
||||
<button class="dropdown-toggle">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="/admin/settings.html"><i class="fas fa-cog"></i> Settings</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" id="logout-btn"><i class="fas fa-sign-out-alt"></i> Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="content-wrapper">
|
||||
<!-- Settings Tabs -->
|
||||
<div class="settings-tabs">
|
||||
<button class="tab-btn active" data-tab="general">
|
||||
<i class="fas fa-globe"></i> General
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="contact">
|
||||
<i class="fas fa-address-card"></i> Contact Info
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="social">
|
||||
<i class="fas fa-share-alt"></i> Social Media
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="seo">
|
||||
<i class="fas fa-search"></i> SEO
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="appearance">
|
||||
<i class="fas fa-palette"></i> Appearance
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="email-auth">
|
||||
<i class="fas fa-envelope-open-text"></i> Email & Auth
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="integrations">
|
||||
<i class="fas fa-plug"></i> Integrations
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- General Settings -->
|
||||
<div class="tab-content active" id="general">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>General Settings</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label>Site Name</label>
|
||||
<input type="text" class="form-control" value="MSPE" id="site-name">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Site Tagline</label>
|
||||
<input type="text" class="form-control" value="Architects of Digital Resilience" id="site-tagline">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Site Description</label>
|
||||
<textarea class="form-control" rows="3" id="site-description">MSPE transforms complexity into competitive advantage through innovative technology solutions and uncompromising security.</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Site Logo</label>
|
||||
<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>
|
||||
</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'">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Timezone</label>
|
||||
<select class="form-control" id="timezone">
|
||||
<option value="UTC">UTC</option>
|
||||
<option value="America/New_York">Eastern Time (US)</option>
|
||||
<option value="America/Chicago">Central Time (US)</option>
|
||||
<option value="America/Denver">Mountain Time (US)</option>
|
||||
<option value="America/Los_Angeles">Pacific Time (US)</option>
|
||||
<option value="Europe/London">London</option>
|
||||
<option value="Europe/Paris">Paris</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" type="button">
|
||||
<i class="fas fa-save"></i> Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Info -->
|
||||
<div class="tab-content" id="contact">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Contact Information</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Email Address</label>
|
||||
<input type="email" class="form-control" value="info@mspe.pro" id="contact-email">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Phone Number</label>
|
||||
<input type="tel" class="form-control" value="" id="contact-phone" placeholder="+1 (555) 000-0000">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Address</label>
|
||||
<textarea class="form-control" rows="2" id="contact-address" placeholder="Enter your business address"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Business Hours</label>
|
||||
<input type="text" class="form-control" value="Mon - Fri: 9AM - 6PM" id="business-hours">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Support Hours</label>
|
||||
<input type="text" class="form-control" value="Mon - Fri: 9AM - 6PM (On-call by arrangement)" id="support-hours">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Google Maps Embed URL</label>
|
||||
<input type="url" class="form-control" id="maps-url" placeholder="https://www.google.com/maps/embed?...">
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" type="button">
|
||||
<i class="fas fa-save"></i> Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Social Media -->
|
||||
<div class="tab-content" id="social">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Social Media Links</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group social-input">
|
||||
<label><i class="fab fa-facebook"></i> Facebook</label>
|
||||
<input type="url" class="form-control" id="social-facebook" placeholder="https://facebook.com/yourpage">
|
||||
</div>
|
||||
|
||||
<div class="form-group social-input">
|
||||
<label><i class="fab fa-linkedin"></i> LinkedIn</label>
|
||||
<input type="url" class="form-control" id="social-linkedin" placeholder="https://linkedin.com/company/yourcompany">
|
||||
</div>
|
||||
|
||||
<div class="form-group social-input">
|
||||
<label><i class="fab fa-twitter"></i> Twitter / X</label>
|
||||
<input type="url" class="form-control" id="social-twitter" placeholder="https://twitter.com/yourhandle">
|
||||
</div>
|
||||
|
||||
<div class="form-group social-input">
|
||||
<label><i class="fab fa-instagram"></i> Instagram</label>
|
||||
<input type="url" class="form-control" id="social-instagram" placeholder="https://instagram.com/yourprofile">
|
||||
</div>
|
||||
|
||||
<div class="form-group social-input">
|
||||
<label><i class="fab fa-youtube"></i> YouTube</label>
|
||||
<input type="url" class="form-control" id="social-youtube" placeholder="https://youtube.com/@yourchannel">
|
||||
</div>
|
||||
|
||||
<div class="form-group social-input">
|
||||
<label><i class="fab fa-github"></i> GitHub</label>
|
||||
<input type="url" class="form-control" id="social-github" placeholder="https://github.com/yourorg">
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" type="button">
|
||||
<i class="fas fa-save"></i> Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SEO Settings -->
|
||||
<div class="tab-content" id="seo">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>SEO Settings</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label>Default Meta Title</label>
|
||||
<input type="text" class="form-control" value="MSPE - Architects of Digital Resilience" id="meta-title">
|
||||
<span class="form-hint">Recommended: 50-60 characters</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Default Meta Description</label>
|
||||
<textarea class="form-control" rows="3" id="meta-description">MSPE delivers enterprise-grade IT support, cybersecurity, and cloud solutions. Transform complexity into competitive advantage.</textarea>
|
||||
<span class="form-hint">Recommended: 150-160 characters</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Meta Keywords</label>
|
||||
<input type="text" class="form-control" id="meta-keywords" placeholder="IT support, cybersecurity, cloud services, managed IT">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Google Analytics ID</label>
|
||||
<input type="text" class="form-control" id="ga-id" placeholder="G-XXXXXXXXXX">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Google Tag Manager ID</label>
|
||||
<input type="text" class="form-control" id="gtm-id" placeholder="GTM-XXXXXXX">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Robots.txt Content</label>
|
||||
<textarea class="form-control code-textarea" rows="5" id="robots-txt">User-agent: *
|
||||
Allow: /
|
||||
Sitemap: https://mspe.pro/sitemap.xml</textarea>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" type="button">
|
||||
<i class="fas fa-save"></i> Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Appearance -->
|
||||
<div class="tab-content" id="appearance">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Theme & Colors</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Primary Color</label>
|
||||
<div class="color-picker">
|
||||
<input type="color" value="#0ea5e9" id="primary-color">
|
||||
<input type="text" class="form-control" value="#0ea5e9">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Secondary Color</label>
|
||||
<div class="color-picker">
|
||||
<input type="color" value="#0d9488" id="secondary-color">
|
||||
<input type="text" class="form-control" value="#0d9488">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Accent Color</label>
|
||||
<div class="color-picker">
|
||||
<input type="color" value="#f59e0b" id="accent-color">
|
||||
<input type="text" class="form-control" value="#f59e0b">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Dark Background</label>
|
||||
<div class="color-picker">
|
||||
<input type="color" value="#0a1628" id="dark-bg-color">
|
||||
<input type="text" class="form-control" value="#0a1628">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Font Family</label>
|
||||
<select class="form-control" id="font-family">
|
||||
<option value="DM Sans">DM Sans</option>
|
||||
<option value="Inter">Inter</option>
|
||||
<option value="Poppins">Poppins</option>
|
||||
<option value="Roboto">Roboto</option>
|
||||
<option value="Open Sans">Open Sans</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" type="button">
|
||||
<i class="fas fa-save"></i> Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email & Auth -->
|
||||
<div class="tab-content" id="email-auth">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Email Delivery & Password Reset</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Email Transport</label>
|
||||
<select class="form-control" id="email-transport">
|
||||
<option value="mail">PHP mail()</option>
|
||||
<option value="smtp">SMTP (recommended)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Admin Notification Email</label>
|
||||
<input type="email" class="form-control" id="admin-email" placeholder="alerts@yourdomain.com">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Sender Name</label>
|
||||
<input type="text" class="form-control" id="smtp-from-name" placeholder="MSPE">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Sender Email</label>
|
||||
<input type="email" class="form-control" id="smtp-from-email" placeholder="no-reply@yourdomain.com">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>SMTP Host</label>
|
||||
<input type="text" class="form-control" id="smtp-host" placeholder="smtp.hostinger.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>SMTP Port</label>
|
||||
<input type="number" class="form-control" id="smtp-port" placeholder="465 or 587">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Encryption</label>
|
||||
<select class="form-control" id="smtp-encryption">
|
||||
<option value="tls">TLS</option>
|
||||
<option value="ssl">SSL</option>
|
||||
<option value="none">None</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>SMTP Username</label>
|
||||
<input type="text" class="form-control" id="smtp-username" placeholder="mailbox@yourdomain.com">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>SMTP Password</label>
|
||||
<input type="password" class="form-control" id="smtp-password" placeholder="Mailbox password">
|
||||
<span class="form-hint">Stored in site settings. Use a dedicated mailbox/app password.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Password Reset Page URL</label>
|
||||
<input type="url" class="form-control" id="password-reset-url" placeholder="https://yourdomain.com/admin/reset-password.html">
|
||||
<span class="form-hint">Reset tokens will be appended as ?token=...</span>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Test Recipient</label>
|
||||
<input type="email" class="form-control" id="test-email-to" placeholder="you@yourdomain.com">
|
||||
</div>
|
||||
<div class="form-group" style="display:flex;align-items:flex-end;">
|
||||
<button class="btn btn-outline" id="send-test-email-btn" type="button" style="width:100%;">
|
||||
<i class="fas fa-paper-plane"></i> Send Test Email
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" type="button">
|
||||
<i class="fas fa-save"></i> Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Integrations -->
|
||||
<div class="tab-content" id="integrations">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Third-Party Integrations</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="integration-item">
|
||||
<div class="integration-info">
|
||||
<i class="fab fa-google" style="color: #4285f4;"></i>
|
||||
<div>
|
||||
<h4>Google reCAPTCHA</h4>
|
||||
<p>Protect forms from spam</p>
|
||||
</div>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="integration-recaptcha">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="integration-item">
|
||||
<div class="integration-info">
|
||||
<i class="fab fa-mailchimp" style="color: #ffe01b;"></i>
|
||||
<div>
|
||||
<h4>Mailchimp</h4>
|
||||
<p>Email marketing integration</p>
|
||||
</div>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="integration-mailchimp">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="integration-item">
|
||||
<div class="integration-info">
|
||||
<i class="fab fa-slack" style="color: #4a154b;"></i>
|
||||
<div>
|
||||
<h4>Slack Notifications</h4>
|
||||
<p>Get notified of new messages</p>
|
||||
</div>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="integration-slack">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="integration-item">
|
||||
<div class="integration-info">
|
||||
<i class="fas fa-calendar-alt" style="color: #0ea5e9;"></i>
|
||||
<div>
|
||||
<h4>Calendly</h4>
|
||||
<p>Booking integration</p>
|
||||
</div>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="integration-calendly">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="integration-settings" style="margin-top: 2rem; padding-top: 2rem; border-top: 1px solid var(--gray-200);">
|
||||
<h4>Calendly Settings</h4>
|
||||
<div class="form-group">
|
||||
<label>Calendly URL</label>
|
||||
<input type="url" class="form-control" id="calendly-url" placeholder="https://calendly.com/your-link">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" type="button" style="margin-top: 1rem;">
|
||||
<i class="fas fa-save"></i> Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/admin/js/admin.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadSettings();
|
||||
setupTabs();
|
||||
setupColorPickers();
|
||||
setupSaveButtons();
|
||||
setupEmailTest();
|
||||
});
|
||||
|
||||
function setupTabs() {
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const tabId = this.dataset.tab;
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
document.getElementById(tabId).classList.add('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setupColorPickers() {
|
||||
document.querySelectorAll('.color-picker input[type="color"]').forEach(picker => {
|
||||
picker.addEventListener('input', function() {
|
||||
this.nextElementSibling.value = this.value;
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.color-picker input[type="text"]').forEach(input => {
|
||||
input.addEventListener('input', function() {
|
||||
this.previousElementSibling.value = this.value;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setupSaveButtons() {
|
||||
document.querySelectorAll('.tab-content .btn-primary[type="button"]').forEach(btn => {
|
||||
if (btn.id !== 'send-test-email-btn') {
|
||||
btn.addEventListener('click', saveSettings);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setupEmailTest() {
|
||||
const btn = document.getElementById('send-test-email-btn');
|
||||
if (!btn) return;
|
||||
|
||||
btn.addEventListener('click', async function() {
|
||||
const to = document.getElementById('test-email-to').value || document.getElementById('admin-email').value || document.getElementById('contact-email').value;
|
||||
if (!to) {
|
||||
MSPE.showNotification('Please enter a test recipient email', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const original = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sending...';
|
||||
|
||||
try {
|
||||
const response = await MSPE.API.post('email-test.php', { to });
|
||||
if (response.success) {
|
||||
MSPE.showNotification(response.message || 'Test email sent', 'success');
|
||||
} else {
|
||||
MSPE.showNotification(response.message || 'Test email failed', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
MSPE.showNotification('Failed to send test email', 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = original;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const response = await MSPE.API.get('settings.php');
|
||||
if (response && response.success) {
|
||||
const settings = response.data;
|
||||
|
||||
// General
|
||||
setVal('site-name', settings.site_name);
|
||||
setVal('site-tagline', settings.site_tagline);
|
||||
setVal('site-description', settings.site_description);
|
||||
setVal('timezone', settings.timezone);
|
||||
|
||||
// Contact
|
||||
setVal('contact-email', settings.contact_email);
|
||||
setVal('contact-phone', settings.contact_phone);
|
||||
setVal('contact-address', settings.contact_address);
|
||||
setVal('business-hours', settings.business_hours);
|
||||
setVal('support-hours', settings.support_hours);
|
||||
setVal('maps-url', settings.maps_url);
|
||||
|
||||
// Email & Auth
|
||||
setVal('email-transport', settings.email_transport || 'mail');
|
||||
setVal('admin-email', settings.admin_email || settings.contact_email);
|
||||
setVal('smtp-from-name', settings.smtp_from_name || settings.site_name || 'MSPE');
|
||||
setVal('smtp-from-email', settings.smtp_from_email || settings.contact_email);
|
||||
setVal('smtp-host', settings.smtp_host);
|
||||
setVal('smtp-port', settings.smtp_port);
|
||||
setVal('smtp-encryption', settings.smtp_encryption || 'tls');
|
||||
setVal('smtp-username', settings.smtp_username);
|
||||
setVal('smtp-password', settings.smtp_password);
|
||||
setVal('password-reset-url', settings.password_reset_url);
|
||||
setVal('test-email-to', settings.admin_email || settings.contact_email);
|
||||
|
||||
// SEO
|
||||
setVal('meta-title', settings.meta_title);
|
||||
setVal('meta-description', settings.meta_description);
|
||||
setVal('meta-keywords', settings.meta_keywords);
|
||||
setVal('ga-id', settings.ga_id);
|
||||
setVal('gtm-id', settings.gtm_id);
|
||||
setVal('robots-txt', settings.robots_txt);
|
||||
|
||||
// Appearance
|
||||
setVal('primary-color', settings.primary_color);
|
||||
setVal('secondary-color', settings.secondary_color);
|
||||
setVal('accent-color', settings.accent_color);
|
||||
setVal('dark-bg-color', settings.dark_bg_color);
|
||||
setVal('font-family', settings.font_family);
|
||||
|
||||
// Social
|
||||
setVal('social-facebook', settings.social_facebook);
|
||||
setVal('social-linkedin', settings.social_linkedin);
|
||||
setVal('social-twitter', settings.social_twitter);
|
||||
setVal('social-instagram', settings.social_instagram);
|
||||
setVal('social-youtube', settings.social_youtube);
|
||||
setVal('social-github', settings.social_github);
|
||||
|
||||
// Integrations
|
||||
setCheck('integration-recaptcha', settings.integration_recaptcha);
|
||||
setCheck('integration-mailchimp', settings.integration_mailchimp);
|
||||
setCheck('integration-slack', settings.integration_slack);
|
||||
setCheck('integration-calendly', settings.integration_calendly);
|
||||
setVal('calendly-url', settings.calendly_url);
|
||||
|
||||
// Update color pickers text inputs
|
||||
updateColorInputs();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading settings:', error);
|
||||
MSPE.showNotification('Failed to load settings', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function setVal(id, val) {
|
||||
if (val !== undefined && val !== null && document.getElementById(id)) {
|
||||
document.getElementById(id).value = val;
|
||||
}
|
||||
}
|
||||
|
||||
function setCheck(id, val) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.checked = (val === true || val === '1' || val === 1);
|
||||
}
|
||||
|
||||
function updateColorInputs() {
|
||||
document.querySelectorAll('.color-picker input[type="color"]').forEach(picker => {
|
||||
if (picker.nextElementSibling) {
|
||||
picker.nextElementSibling.value = picker.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
const settings = {
|
||||
// General
|
||||
site_name: document.getElementById('site-name').value,
|
||||
site_tagline: document.getElementById('site-tagline').value,
|
||||
site_description: document.getElementById('site-description').value,
|
||||
timezone: document.getElementById('timezone').value,
|
||||
|
||||
// Contact
|
||||
contact_email: document.getElementById('contact-email').value,
|
||||
contact_phone: document.getElementById('contact-phone').value,
|
||||
contact_address: document.getElementById('contact-address').value,
|
||||
business_hours: document.getElementById('business-hours').value,
|
||||
support_hours: document.getElementById('support-hours').value,
|
||||
maps_url: document.getElementById('maps-url').value,
|
||||
|
||||
// Email & Auth
|
||||
email_transport: document.getElementById('email-transport').value,
|
||||
admin_email: document.getElementById('admin-email').value,
|
||||
smtp_from_name: document.getElementById('smtp-from-name').value,
|
||||
smtp_from_email: document.getElementById('smtp-from-email').value,
|
||||
smtp_host: document.getElementById('smtp-host').value,
|
||||
smtp_port: document.getElementById('smtp-port').value,
|
||||
smtp_encryption: document.getElementById('smtp-encryption').value,
|
||||
smtp_username: document.getElementById('smtp-username').value,
|
||||
smtp_password: document.getElementById('smtp-password').value,
|
||||
password_reset_url: document.getElementById('password-reset-url').value,
|
||||
|
||||
// SEO
|
||||
meta_title: document.getElementById('meta-title').value,
|
||||
meta_description: document.getElementById('meta-description').value,
|
||||
meta_keywords: document.getElementById('meta-keywords').value,
|
||||
ga_id: document.getElementById('ga-id').value,
|
||||
gtm_id: document.getElementById('gtm-id').value,
|
||||
robots_txt: document.getElementById('robots-txt').value,
|
||||
|
||||
// Appearance
|
||||
primary_color: document.getElementById('primary-color').value,
|
||||
secondary_color: document.getElementById('secondary-color').value,
|
||||
accent_color: document.getElementById('accent-color').value,
|
||||
dark_bg_color: document.getElementById('dark-bg-color').value,
|
||||
font_family: document.getElementById('font-family').value,
|
||||
|
||||
// Social
|
||||
social_facebook: document.getElementById('social-facebook').value,
|
||||
social_linkedin: document.getElementById('social-linkedin').value,
|
||||
social_twitter: document.getElementById('social-twitter').value,
|
||||
social_instagram: document.getElementById('social-instagram').value,
|
||||
social_youtube: document.getElementById('social-youtube').value,
|
||||
social_github: document.getElementById('social-github').value,
|
||||
|
||||
// Integrations
|
||||
integration_recaptcha: document.getElementById('integration-recaptcha').checked ? 1 : 0,
|
||||
integration_mailchimp: document.getElementById('integration-mailchimp').checked ? 1 : 0,
|
||||
integration_slack: document.getElementById('integration-slack').checked ? 1 : 0,
|
||||
integration_calendly: document.getElementById('integration-calendly').checked ? 1 : 0,
|
||||
calendly_url: document.getElementById('calendly-url').value
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await MSPE.API.post('settings.php', settings);
|
||||
if (response.success) {
|
||||
MSPE.showNotification('Settings saved successfully', 'success');
|
||||
} else {
|
||||
MSPE.showNotification('Error saving settings', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
MSPE.showNotification('Error saving settings', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Logo & Favicon upload handlers
|
||||
document.getElementById('logo-input')?.addEventListener('change', async function() {
|
||||
if (!this.files[0]) return;
|
||||
const formData = new FormData();
|
||||
formData.append('files[]', this.files[0]);
|
||||
formData.append('folder', 'logo');
|
||||
try {
|
||||
const result = await MSPE.API.upload('media.php', formData);
|
||||
if (result && result.success) {
|
||||
document.querySelector('.current-logo').src = '/' + (result.data?.[0]?.path || 'images/logo/logo.png');
|
||||
MSPE.showNotification('Logo uploaded successfully', 'success');
|
||||
} else {
|
||||
MSPE.showNotification(result?.message || 'Logo upload failed', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
MSPE.showNotification('Error uploading logo', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('favicon-input')?.addEventListener('change', async function() {
|
||||
if (!this.files[0]) return;
|
||||
const formData = new FormData();
|
||||
formData.append('files[]', this.files[0]);
|
||||
formData.append('folder', 'logo');
|
||||
try {
|
||||
const result = await MSPE.API.upload('media.php', formData);
|
||||
if (result && result.success) {
|
||||
document.querySelector('.current-favicon').src = '/' + (result.data?.[0]?.path || 'images/logo/logo.png');
|
||||
document.querySelector('.current-favicon').style.display = '';
|
||||
MSPE.showNotification('Favicon uploaded successfully', 'success');
|
||||
} else {
|
||||
MSPE.showNotification(result?.message || 'Favicon upload failed', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
MSPE.showNotification('Error uploading favicon', 'error');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+531
@@ -0,0 +1,531 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||
<title>Subscribers - MSPE Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha384-t1nt8BQoYMLFN5p42tRAtuAAFQaCQODekUVeKKZrEnEyp4H2R0RHFz0KWpmj7i8g" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/admin/css/admin.css">
|
||||
<style>
|
||||
.subscribers-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.stat-card h4 {
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-500);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-card .value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
.subscribers-table-container {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.table-header h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.subscribers-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.subscribers-table th {
|
||||
background: var(--gray-100);
|
||||
padding: 1rem 1.5rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: var(--gray-600);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.subscribers-table td {
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.subscribers-table tr:hover {
|
||||
background: var(--gray-50);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
margin-right: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-btn.delete {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.action-btn.delete:hover {
|
||||
background: #fecaca;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 4rem 2rem;
|
||||
text-align: center;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.export-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-wrapper">
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<a href="/admin/dashboard.html" class="sidebar-logo">
|
||||
<i class="fas fa-cube"></i>
|
||||
<span>MSPE</span>
|
||||
</a>
|
||||
<button class="sidebar-toggle" id="sidebar-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Main</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/dashboard.html" class="nav-link">
|
||||
<i class="fas fa-th-large"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Content</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/pages.html" class="nav-link">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
<span>Pages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/services.html" class="nav-link">
|
||||
<i class="fas fa-concierge-bell"></i>
|
||||
<span>Services</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/portfolio.html" class="nav-link">
|
||||
<i class="fas fa-briefcase"></i>
|
||||
<span>Portfolio</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/news.html" class="nav-link">
|
||||
<i class="fas fa-newspaper"></i>
|
||||
<span>News & Events</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/testimonials.html" class="nav-link">
|
||||
<i class="fas fa-quote-right"></i>
|
||||
<span>Testimonials</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/team.html" class="nav-link">
|
||||
<i class="fas fa-users"></i>
|
||||
<span>Team</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Media</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/media.html" class="nav-link">
|
||||
<i class="fas fa-images"></i>
|
||||
<span>Media Library</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Inquiries</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/messages.html" class="nav-link">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<span>Messages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a href="/admin/subscribers.html" class="nav-link">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span>Subscribers</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/bookings.html" class="nav-link">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Settings</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings.html" class="nav-link">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span>Site Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/users.html" class="nav-link">
|
||||
<i class="fas fa-user-shield"></i>
|
||||
<span>Admin Users</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<a href="/index.html" class="view-site-btn" target="_blank">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
<span>View Website</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="admin-header">
|
||||
<div class="header-left">
|
||||
<button class="mobile-toggle" id="mobile-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<h1 class="page-title">Newsletter Subscribers</h1>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<button class="btn btn-primary export-btn" onclick="exportSubscribers()">
|
||||
<i class="fas fa-download"></i> Export CSV
|
||||
</button>
|
||||
|
||||
<div class="header-user">
|
||||
<div class="user-avatar">
|
||||
<img src="https://ui-avatars.com/api/?name=Admin&background=0ea5e9&color=fff" alt="Admin">
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<span class="user-name">Admin</span>
|
||||
<span class="user-role">Administrator</span>
|
||||
</div>
|
||||
<div class="user-dropdown">
|
||||
<button class="dropdown-toggle">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="/admin/settings.html"><i class="fas fa-cog"></i> Settings</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" id="logout-btn"><i class="fas fa-sign-out-alt"></i> Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="stats-row">
|
||||
<div class="stat-card">
|
||||
<h4>Total Subscribers</h4>
|
||||
<div class="value" id="total-count">0</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h4>Active Subscribers</h4>
|
||||
<div class="value" id="active-count">0</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h4>This Month</h4>
|
||||
<div class="value" id="month-count">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="subscribers-table-container">
|
||||
<div class="table-header">
|
||||
<h3>Subscriber List</h3>
|
||||
<div class="header-search" style="max-width: 250px;">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="text" placeholder="Search subscribers..." id="search-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="subscribers-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Subscribed</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="subscribers-table-body">
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
<p>Loading subscribers...</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/admin/js/admin.js"></script>
|
||||
<script>
|
||||
let allSubscribers = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadSubscribers();
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
function setupEventListeners() {
|
||||
document.getElementById('search-input').addEventListener('input', MSPE.debounce(filterSubscribers, 300));
|
||||
}
|
||||
|
||||
async function loadSubscribers() {
|
||||
try {
|
||||
const response = await MSPE.API.get('subscribe.php');
|
||||
|
||||
if (response && response.success) {
|
||||
allSubscribers = response.data || [];
|
||||
updateStats();
|
||||
renderSubscribers(allSubscribers);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load subscribers:', error);
|
||||
document.getElementById('subscribers-table-body').innerHTML = `
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<p>Failed to load subscribers</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
const total = allSubscribers.length;
|
||||
const active = allSubscribers.filter(s => s.status === 'active').length;
|
||||
|
||||
// This month
|
||||
const now = new Date();
|
||||
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const thisMonth = allSubscribers.filter(s =>
|
||||
new Date(s.created_at) >= monthStart
|
||||
).length;
|
||||
|
||||
document.getElementById('total-count').textContent = total;
|
||||
document.getElementById('active-count').textContent = active;
|
||||
document.getElementById('month-count').textContent = thisMonth;
|
||||
}
|
||||
|
||||
function renderSubscribers(subscribers) {
|
||||
const tbody = document.getElementById('subscribers-table-body');
|
||||
|
||||
if (!subscribers || subscribers.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-users"></i>
|
||||
<p>No subscribers yet</p>
|
||||
<span style="font-size: 0.875rem;">Subscribers from your newsletter signup will appear here</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = subscribers.map(subscriber => {
|
||||
const date = new Date(subscriber.created_at);
|
||||
const formattedDate = date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
});
|
||||
const statusClass = subscriber.status === 'active' ? 'status-active' : 'status-inactive';
|
||||
|
||||
return `
|
||||
<tr data-id="${subscriber.id}">
|
||||
<td>${subscriber.name || 'Unknown'}</td>
|
||||
<td>
|
||||
<a href="mailto:${subscriber.email}" style="color: var(--primary);">
|
||||
${subscriber.email}
|
||||
</a>
|
||||
</td>
|
||||
<td>${formattedDate}</td>
|
||||
<td>
|
||||
<span class="status-badge ${statusClass}">${subscriber.status}</span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="action-btn delete" onclick="deleteSubscriber('${subscriber.id}')">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function filterSubscribers() {
|
||||
const search = document.getElementById('search-input').value.toLowerCase();
|
||||
|
||||
if (!search) {
|
||||
renderSubscribers(allSubscribers);
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = allSubscribers.filter(s =>
|
||||
(s.name && s.name.toLowerCase().includes(search)) ||
|
||||
s.email.toLowerCase().includes(search)
|
||||
);
|
||||
|
||||
renderSubscribers(filtered);
|
||||
}
|
||||
|
||||
async function deleteSubscriber(id) {
|
||||
if (!await MSPE.confirmDialog('Are you sure you want to delete this subscriber?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await MSPE.API.delete(`subscribe.php?id=${id}`);
|
||||
|
||||
if (response && response.success) {
|
||||
MSPE.showNotification('Subscriber deleted successfully', 'success');
|
||||
allSubscribers = allSubscribers.filter(s => s.id !== id);
|
||||
updateStats();
|
||||
renderSubscribers(allSubscribers);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete subscriber:', error);
|
||||
MSPE.showNotification('Failed to delete subscriber', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function exportSubscribers() {
|
||||
if (allSubscribers.length === 0) {
|
||||
MSPE.showNotification('No subscribers to export', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const activeSubscribers = allSubscribers.filter(s => s.status === 'active');
|
||||
|
||||
const csv = [
|
||||
['Name', 'Email', 'Subscribed Date', 'Status'].join(','),
|
||||
...activeSubscribers.map(s => [
|
||||
`"${s.name || ''}"`,
|
||||
`"${s.email}"`,
|
||||
`"${new Date(s.created_at).toLocaleDateString()}"`,
|
||||
`"${s.status}"`
|
||||
].join(','))
|
||||
].join('\n');
|
||||
|
||||
const blob = new Blob([csv], { type: 'text/csv' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `subscribers-${new Date().toISOString().split('T')[0]}.csv`;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
MSPE.showNotification(`Exported ${activeSubscribers.length} subscribers`, 'success');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+508
@@ -0,0 +1,508 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||
<title>Team Management - MSPE Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha384-t1nt8BQoYMLFN5p42tRAtuAAFQaCQODekUVeKKZrEnEyp4H2R0RHFz0KWpmj7i8g" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/admin/css/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-wrapper">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<a href="/admin/dashboard.html" class="sidebar-logo">
|
||||
<i class="fas fa-cube"></i>
|
||||
<span>MSPE</span>
|
||||
</a>
|
||||
<button class="sidebar-toggle" id="sidebar-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Main</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/dashboard.html" class="nav-link">
|
||||
<i class="fas fa-th-large"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Content</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/pages.html" class="nav-link">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
<span>Pages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/services.html" class="nav-link">
|
||||
<i class="fas fa-concierge-bell"></i>
|
||||
<span>Services</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/portfolio.html" class="nav-link">
|
||||
<i class="fas fa-briefcase"></i>
|
||||
<span>Portfolio</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/news.html" class="nav-link">
|
||||
<i class="fas fa-newspaper"></i>
|
||||
<span>News & Events</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/testimonials.html" class="nav-link">
|
||||
<i class="fas fa-quote-right"></i>
|
||||
<span>Testimonials</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a href="/admin/team.html" class="nav-link">
|
||||
<i class="fas fa-users"></i>
|
||||
<span>Team</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Media</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/media.html" class="nav-link">
|
||||
<i class="fas fa-images"></i>
|
||||
<span>Media Library</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Inquiries</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/messages.html" class="nav-link">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<span>Messages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/subscribers.html" class="nav-link">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span>Subscribers</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/bookings.html" class="nav-link">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Settings</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings.html" class="nav-link">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span>Site Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/users.html" class="nav-link">
|
||||
<i class="fas fa-user-shield"></i>
|
||||
<span>Admin Users</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<a href="/index.html" class="view-site-btn" target="_blank">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
<span>View Website</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<!-- Header -->
|
||||
<header class="admin-header">
|
||||
<div class="header-left">
|
||||
<button class="mobile-toggle" id="mobile-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<h1 class="page-title">Team Management</h1>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<button class="btn btn-primary" onclick="openAddMemberModal()">
|
||||
<i class="fas fa-plus"></i> Add Team Member
|
||||
</button>
|
||||
|
||||
<div class="header-user">
|
||||
<div class="user-avatar">
|
||||
<img src="https://ui-avatars.com/api/?name=Admin&background=0ea5e9&color=fff" alt="Admin">
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<span class="user-name">Admin</span>
|
||||
<span class="user-role">Administrator</span>
|
||||
</div>
|
||||
<div class="user-dropdown">
|
||||
<button class="dropdown-toggle">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="/admin/settings.html"><i class="fas fa-cog"></i> Settings</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" id="logout-btn"><i class="fas fa-sign-out-alt"></i> Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="content-wrapper">
|
||||
<div class="content-header">
|
||||
<p class="content-description">Manage your team members displayed on the website</p>
|
||||
<div class="filter-bar">
|
||||
<select class="form-select" id="department-filter">
|
||||
<option value="">All Departments</option>
|
||||
<option value="management">Management</option>
|
||||
<option value="engineering">Engineering</option>
|
||||
<option value="support">Support</option>
|
||||
<option value="sales">Sales</option>
|
||||
</select>
|
||||
<div class="view-toggle">
|
||||
<button class="view-btn active" data-view="grid"><i class="fas fa-th"></i></button>
|
||||
<button class="view-btn" data-view="list"><i class="fas fa-list"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Team Grid -->
|
||||
<div class="team-admin-grid" id="team-grid">
|
||||
<div style="grid-column:1/-1;text-align:center;padding:3rem;color:var(--gray-400);">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||
<p style="margin-top:1rem;">Loading team members...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Team Member Modal -->
|
||||
<div class="modal" id="member-modal">
|
||||
<div class="modal-backdrop"></div>
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h2 id="modal-title">Add Team Member</h2>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="member-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Full Name *</label>
|
||||
<input type="text" class="form-control" name="name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Job Title *</label>
|
||||
<input type="text" class="form-control" name="role" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Department *</label>
|
||||
<select class="form-select" name="department" required>
|
||||
<option value="">Select Department</option>
|
||||
<option value="management">Management</option>
|
||||
<option value="engineering">Engineering</option>
|
||||
<option value="support">Support</option>
|
||||
<option value="sales">Sales</option>
|
||||
<option value="marketing">Marketing</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Email</label>
|
||||
<input type="email" class="form-control" name="email">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Bio</label>
|
||||
<textarea class="form-control" name="bio" rows="3" placeholder="Brief description..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Profile Photo</label>
|
||||
<div class="file-upload-area">
|
||||
<i class="fas fa-cloud-upload-alt"></i>
|
||||
<p>Drag & drop or click to upload</p>
|
||||
<span>Recommended: 400x400px, JPG or PNG</span>
|
||||
<input type="file" name="photo" accept="image/*">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Social Links</label>
|
||||
<div class="social-inputs">
|
||||
<div class="input-icon-group">
|
||||
<i class="fab fa-linkedin"></i>
|
||||
<input type="url" class="form-control" name="linkedin" placeholder="LinkedIn URL">
|
||||
</div>
|
||||
<div class="input-icon-group">
|
||||
<i class="fab fa-twitter"></i>
|
||||
<input type="url" class="form-control" name="twitter" placeholder="Twitter URL">
|
||||
</div>
|
||||
<div class="input-icon-group">
|
||||
<i class="fab fa-github"></i>
|
||||
<input type="url" class="form-control" name="github" placeholder="GitHub URL">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Display Order</label>
|
||||
<input type="number" class="form-control" name="order" value="1" min="1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Status</label>
|
||||
<select class="form-select" name="status">
|
||||
<option value="active">Active (Visible)</option>
|
||||
<option value="hidden">Hidden</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" onclick="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="saveMember()">
|
||||
<i class="fas fa-save"></i> Save Member
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/admin/js/admin.js"></script>
|
||||
<script>
|
||||
let teamMembers = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadTeam();
|
||||
document.getElementById('department-filter').addEventListener('change', filterTeam);
|
||||
|
||||
// View toggle (grid / list)
|
||||
document.querySelectorAll('.view-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
const grid = document.getElementById('team-grid');
|
||||
if (this.dataset.view === 'list') {
|
||||
grid.style.gridTemplateColumns = '1fr';
|
||||
} else {
|
||||
grid.style.gridTemplateColumns = '';
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadTeam() {
|
||||
try {
|
||||
const response = await MSPE.API.get('team.php');
|
||||
if (response && response.success) {
|
||||
teamMembers = response.data;
|
||||
renderTeam(teamMembers);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading team:', error);
|
||||
document.getElementById('team-grid').innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<p>Failed to load team members</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderTeam(members) {
|
||||
const container = document.getElementById('team-grid');
|
||||
if (members.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-users"></i>
|
||||
<p>No team members found</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = members.map(member => `
|
||||
<div class="team-admin-card ${member.status === 'hidden' ? 'opacity-50' : ''}">
|
||||
<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}')">
|
||||
<i class="fas fa-camera"></i> Change
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="team-member-info">
|
||||
<h3>${member.name}</h3>
|
||||
<span class="team-role">${member.role}</span>
|
||||
<span class="team-dept">${member.department}</span>
|
||||
</div>
|
||||
<div class="team-socials">
|
||||
${member.linkedin ? `<a href="${member.linkedin}" class="social-icon linkedin"><i class="fab fa-linkedin"></i></a>` : ''}
|
||||
${member.twitter ? `<a href="${member.twitter}" class="social-icon twitter"><i class="fab fa-twitter"></i></a>` : ''}
|
||||
${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}')">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteMember('${member.id}')">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="team-order">
|
||||
<span>Order: ${member.order || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function filterTeam() {
|
||||
const dept = document.getElementById('department-filter').value;
|
||||
const filtered = dept ? teamMembers.filter(m => m.department.toLowerCase() === dept) : teamMembers;
|
||||
renderTeam(filtered);
|
||||
}
|
||||
|
||||
function resetMemberPhotoArea(form) {
|
||||
const area = form.querySelector('.file-upload-area');
|
||||
if (!area) return;
|
||||
area.style.position = '';
|
||||
area.innerHTML = `
|
||||
<i class="fas fa-cloud-upload-alt"></i>
|
||||
<p>Drag & drop or click to upload</p>
|
||||
<span>Recommended: 400x400px, JPG or PNG</span>
|
||||
<input type="file" name="photo" accept="image/*">`;
|
||||
}
|
||||
|
||||
function openAddMemberModal() {
|
||||
document.getElementById('modal-title').textContent = 'Add Team Member';
|
||||
const form = document.getElementById('member-form');
|
||||
form.reset();
|
||||
form.dataset.id = '';
|
||||
resetMemberPhotoArea(form);
|
||||
document.getElementById('member-modal').classList.add('active');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('member-modal').classList.remove('active');
|
||||
}
|
||||
|
||||
function editMember(id) {
|
||||
const member = teamMembers.find(m => m.id === id);
|
||||
if (!member) return;
|
||||
|
||||
document.getElementById('modal-title').textContent = 'Edit Team Member';
|
||||
const form = document.getElementById('member-form');
|
||||
form.dataset.id = id;
|
||||
|
||||
form.querySelector('[name="name"]').value = member.name;
|
||||
form.querySelector('[name="role"]').value = member.role;
|
||||
form.querySelector('[name="department"]').value = member.department.toLowerCase(); // Ensure lowercase match
|
||||
form.querySelector('[name="email"]').value = member.email || '';
|
||||
form.querySelector('[name="bio"]').value = member.bio || '';
|
||||
form.querySelector('[name="linkedin"]').value = member.linkedin || '';
|
||||
form.querySelector('[name="twitter"]').value = member.twitter || '';
|
||||
form.querySelector('[name="github"]').value = member.github || '';
|
||||
form.querySelector('[name="order"]').value = member.order || 1;
|
||||
form.querySelector('[name="status"]').value = member.status || 'active';
|
||||
|
||||
// Restore photo preview
|
||||
const photoArea = form.querySelector('.file-upload-area');
|
||||
if (member.photo) {
|
||||
photoArea.style.position = 'relative';
|
||||
photoArea.innerHTML = `
|
||||
<img src="/${member.photo}" alt="${member.name}" style="max-height:80px;border-radius:50%;margin-bottom:0.5rem;">
|
||||
<p style="font-size:0.85rem;color:var(--gray-500);">Click to replace photo</p>
|
||||
<input type="file" name="photo" accept="image/*" style="position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer;">`;
|
||||
} else {
|
||||
resetMemberPhotoArea(form);
|
||||
}
|
||||
|
||||
document.getElementById('member-modal').classList.add('active');
|
||||
}
|
||||
|
||||
async function saveMember() {
|
||||
const form = document.getElementById('member-form');
|
||||
const formData = new FormData(form);
|
||||
const id = form.dataset.id;
|
||||
|
||||
if (id) formData.append('id', id);
|
||||
|
||||
try {
|
||||
const result = await MSPE.API.upload('team.php', formData);
|
||||
|
||||
if (result && result.success) {
|
||||
MSPE.showNotification(id ? 'Updated successfully' : 'Created successfully', 'success');
|
||||
closeModal();
|
||||
loadTeam();
|
||||
} else {
|
||||
MSPE.showNotification(result?.message || 'Error saving member', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
MSPE.showNotification('Error saving member', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteMember(id) {
|
||||
if (!await MSPE.confirmDialog('Remove this team member?')) return;
|
||||
|
||||
try {
|
||||
const response = await MSPE.API.delete(`team.php?id=${id}`);
|
||||
if (response.success) {
|
||||
MSPE.showNotification('Removed successfully', 'success');
|
||||
loadTeam();
|
||||
}
|
||||
} catch (error) {
|
||||
MSPE.showNotification('Error removing member', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function changePhoto(id) {
|
||||
// In a real app, this would trigger a dedicated file input or focus the modal's file input
|
||||
editMember(id);
|
||||
}
|
||||
|
||||
function moveMember(id, direction) {
|
||||
// For now, manual order editing in modal is sufficient
|
||||
MSPE.showNotification('Please edit the "Order" field in the edit modal', 'info');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+546
@@ -0,0 +1,546 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||
<title>Testimonials - MSPE Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha384-t1nt8BQoYMLFN5p42tRAtuAAFQaCQODekUVeKKZrEnEyp4H2R0RHFz0KWpmj7i8g" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/admin/css/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-wrapper">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<a href="/admin/dashboard.html" class="sidebar-logo">
|
||||
<i class="fas fa-cube"></i>
|
||||
<span>MSPE</span>
|
||||
</a>
|
||||
<button class="sidebar-toggle" id="sidebar-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Main</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/dashboard.html" class="nav-link">
|
||||
<i class="fas fa-th-large"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Content</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/pages.html" class="nav-link">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
<span>Pages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/services.html" class="nav-link">
|
||||
<i class="fas fa-concierge-bell"></i>
|
||||
<span>Services</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/portfolio.html" class="nav-link">
|
||||
<i class="fas fa-briefcase"></i>
|
||||
<span>Portfolio</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/news.html" class="nav-link">
|
||||
<i class="fas fa-newspaper"></i>
|
||||
<span>News & Events</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a href="/admin/testimonials.html" class="nav-link">
|
||||
<i class="fas fa-quote-right"></i>
|
||||
<span>Testimonials</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/team.html" class="nav-link">
|
||||
<i class="fas fa-users"></i>
|
||||
<span>Team</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Media</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/media.html" class="nav-link">
|
||||
<i class="fas fa-images"></i>
|
||||
<span>Media Library</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Inquiries</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/messages.html" class="nav-link">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<span>Messages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/subscribers.html" class="nav-link">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span>Subscribers</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/bookings.html" class="nav-link">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Settings</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings.html" class="nav-link">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span>Site Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/users.html" class="nav-link">
|
||||
<i class="fas fa-user-shield"></i>
|
||||
<span>Admin Users</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<a href="/index.html" class="view-site-btn" target="_blank">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
<span>View Website</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<!-- Header -->
|
||||
<header class="admin-header">
|
||||
<div class="header-left">
|
||||
<button class="mobile-toggle" id="mobile-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<h1 class="page-title">Testimonials</h1>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<button class="btn btn-primary" onclick="openAddTestimonialModal()">
|
||||
<i class="fas fa-plus"></i> Add Testimonial
|
||||
</button>
|
||||
|
||||
<div class="header-user">
|
||||
<div class="user-avatar">
|
||||
<img src="https://ui-avatars.com/api/?name=Admin&background=0ea5e9&color=fff" alt="Admin">
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<span class="user-name">Admin</span>
|
||||
<span class="user-role">Administrator</span>
|
||||
</div>
|
||||
<div class="user-dropdown">
|
||||
<button class="dropdown-toggle">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="/admin/settings.html"><i class="fas fa-cog"></i> Settings</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" id="logout-btn"><i class="fas fa-sign-out-alt"></i> Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="content-wrapper">
|
||||
<div class="content-header">
|
||||
<p class="content-description">Manage client testimonials and reviews displayed on the website</p>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="stats-row" style="margin-bottom: 2rem;">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background: rgba(14, 165, 233, 0.15); color: #0ea5e9;">
|
||||
<i class="fas fa-quote-right"></i>
|
||||
</div>
|
||||
<div class="stat-details">
|
||||
<span class="stat-value" id="stat-total">0</span>
|
||||
<span class="stat-label">Total Testimonials</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background: rgba(16, 185, 129, 0.15); color: #10b981;">
|
||||
<i class="fas fa-eye"></i>
|
||||
</div>
|
||||
<div class="stat-details">
|
||||
<span class="stat-value" id="stat-published">0</span>
|
||||
<span class="stat-label">Published</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background: rgba(245, 158, 11, 0.15); color: #f59e0b;">
|
||||
<i class="fas fa-star"></i>
|
||||
</div>
|
||||
<div class="stat-details">
|
||||
<span class="stat-value" id="stat-avg-rating">0</span>
|
||||
<span class="stat-label">Avg Rating</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background: rgba(139, 92, 246, 0.15); color: #8b5cf6;">
|
||||
<i class="fas fa-star-half-alt"></i>
|
||||
</div>
|
||||
<div class="stat-details">
|
||||
<span class="stat-value" id="stat-pending">0</span>
|
||||
<span class="stat-label">Pending Review</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Testimonials List -->
|
||||
<div class="testimonials-admin-grid" id="testimonials-grid">
|
||||
<div style="grid-column:1/-1;text-align:center;padding:3rem;color:var(--gray-400);">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||
<p style="margin-top:1rem;">Loading testimonials...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Testimonial Modal -->
|
||||
<div class="modal" id="testimonial-modal">
|
||||
<div class="modal-backdrop"></div>
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h2 id="modal-title">Add Testimonial</h2>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="testimonial-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Client Name *</label>
|
||||
<input type="text" class="form-control" name="name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Job Title / Position</label>
|
||||
<input type="text" class="form-control" name="position">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Company</label>
|
||||
<input type="text" class="form-control" name="company">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Rating *</label>
|
||||
<select class="form-select" name="rating" required>
|
||||
<option value="5">5 Stars - Excellent</option>
|
||||
<option value="4.5">4.5 Stars</option>
|
||||
<option value="4">4 Stars - Very Good</option>
|
||||
<option value="3.5">3.5 Stars</option>
|
||||
<option value="3">3 Stars - Good</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Testimonial Text *</label>
|
||||
<textarea class="form-control" name="text" rows="4" required placeholder="Enter the client's testimonial..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Client Photo</label>
|
||||
<div class="file-upload-area">
|
||||
<i class="fas fa-cloud-upload-alt"></i>
|
||||
<p>Drag & drop or click to upload</p>
|
||||
<span>Recommended: 200x200px, JPG or PNG</span>
|
||||
<input type="file" name="photo" accept="image/*">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Display On</label>
|
||||
<div class="checkbox-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="pages[]" value="home" checked> Home Page
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="pages[]" value="about"> About Page
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="pages[]" value="services"> Services Page
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Status</label>
|
||||
<select class="form-select" name="status">
|
||||
<option value="published">Published</option>
|
||||
<option value="pending">Pending Review</option>
|
||||
<option value="hidden">Hidden</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" onclick="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="saveTestimonial()">
|
||||
<i class="fas fa-save"></i> Save Testimonial
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/admin/js/admin.js"></script>
|
||||
<script>
|
||||
let allTestimonials = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadTestimonials();
|
||||
});
|
||||
|
||||
async function loadTestimonials() {
|
||||
try {
|
||||
const response = await MSPE.API.get('testimonials.php');
|
||||
if (response && response.success) {
|
||||
allTestimonials = response.data;
|
||||
renderTestimonials(allTestimonials);
|
||||
updateStats(allTestimonials);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading testimonials:', error);
|
||||
document.querySelector('#testimonials-grid').innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<p>Failed to load testimonials</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderTestimonials(items) {
|
||||
const container = document.querySelector('#testimonials-grid');
|
||||
if (items.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-quote-right"></i>
|
||||
<p>No testimonials yet</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = items.map(item => `
|
||||
<div class="testimonial-admin-card ${item.status}">
|
||||
<div class="testimonial-header">
|
||||
<div class="testimonial-author">
|
||||
<img src="${item.photo || 'https://ui-avatars.com/api/?name=' + encodeURIComponent(item.name) + '&background=random'}" alt="${item.name}">
|
||||
<div>
|
||||
<h4>${item.name}</h4>
|
||||
<span>${item.position || ''} ${item.company ? '@ ' + item.company : ''}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="testimonial-status ${item.status}">
|
||||
<i class="fas fa-${item.status === 'published' ? 'check-circle' : 'clock'}"></i> ${item.status}
|
||||
</div>
|
||||
</div>
|
||||
<div class="testimonial-rating">
|
||||
${renderStars(item.rating)}
|
||||
</div>
|
||||
<p class="testimonial-text">"${item.text}"</p>
|
||||
<div class="testimonial-meta">
|
||||
<span><i class="fas fa-calendar"></i> ${MSPE.formatDate(item.created_at)}</span>
|
||||
</div>
|
||||
<div class="testimonial-admin-actions">
|
||||
${item.status === 'pending' ? `
|
||||
<button class="btn btn-sm btn-primary" onclick="updateStatus('${item.id}', 'published')">
|
||||
<i class="fas fa-check"></i> Approve
|
||||
</button>
|
||||
` : `
|
||||
<button class="btn btn-sm btn-outline" onclick="updateStatus('${item.id}', 'pending')">
|
||||
<i class="fas fa-eye-slash"></i> Unpublish
|
||||
</button>
|
||||
`}
|
||||
<button class="btn btn-sm btn-outline" onclick="editTestimonial('${item.id}')">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteTestimonial('${item.id}')">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function renderStars(rating) {
|
||||
let stars = '';
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
if (i <= rating) {
|
||||
stars += '<i class="fas fa-star"></i>';
|
||||
} else if (i - 0.5 <= rating) {
|
||||
stars += '<i class="fas fa-star-half-alt"></i>';
|
||||
} else {
|
||||
stars += '<i class="far fa-star"></i>';
|
||||
}
|
||||
}
|
||||
return stars;
|
||||
}
|
||||
|
||||
function updateStats(items) {
|
||||
const total = items.length;
|
||||
const published = items.filter(i => i.status === 'published').length;
|
||||
const pending = items.filter(i => i.status === 'pending').length;
|
||||
const avgRating = total > 0 ? (items.reduce((acc, i) => acc + parseFloat(i.rating), 0) / total).toFixed(1) : 0;
|
||||
|
||||
document.getElementById('stat-total').textContent = total;
|
||||
document.getElementById('stat-published').textContent = published;
|
||||
document.getElementById('stat-avg-rating').textContent = avgRating;
|
||||
document.getElementById('stat-pending').textContent = pending;
|
||||
}
|
||||
|
||||
function resetPhotoArea(form) {
|
||||
const area = form.querySelector('.file-upload-area');
|
||||
if (!area) return;
|
||||
area.style.position = '';
|
||||
area.innerHTML = `
|
||||
<i class="fas fa-cloud-upload-alt"></i>
|
||||
<p>Drag & drop or click to upload</p>
|
||||
<span>Recommended: 200x200px, JPG or PNG</span>
|
||||
<input type="file" name="photo" accept="image/*">`;
|
||||
}
|
||||
|
||||
function openAddTestimonialModal() {
|
||||
document.getElementById('modal-title').textContent = 'Add Testimonial';
|
||||
const form = document.getElementById('testimonial-form');
|
||||
form.reset();
|
||||
form.dataset.id = '';
|
||||
resetPhotoArea(form);
|
||||
document.getElementById('testimonial-modal').classList.add('active');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('testimonial-modal').classList.remove('active');
|
||||
}
|
||||
|
||||
function editTestimonial(id) {
|
||||
const item = allTestimonials.find(i => i.id === id);
|
||||
if (!item) return;
|
||||
|
||||
document.getElementById('modal-title').textContent = 'Edit Testimonial';
|
||||
const form = document.getElementById('testimonial-form');
|
||||
form.dataset.id = id;
|
||||
|
||||
form.querySelector('[name="name"]').value = item.name;
|
||||
form.querySelector('[name="position"]').value = item.position || '';
|
||||
form.querySelector('[name="company"]').value = item.company || '';
|
||||
form.querySelector('[name="rating"]').value = item.rating;
|
||||
form.querySelector('[name="text"]').value = item.text;
|
||||
form.querySelector('[name="status"]').value = item.status;
|
||||
|
||||
// Restore photo preview
|
||||
const photoArea = form.querySelector('.file-upload-area');
|
||||
if (item.photo) {
|
||||
photoArea.style.position = 'relative';
|
||||
photoArea.innerHTML = `
|
||||
<img src="/${item.photo}" alt="${item.name}" style="max-height:80px;border-radius:50%;margin-bottom:0.5rem;">
|
||||
<p style="font-size:0.85rem;color:var(--gray-500);">Click to replace photo</p>
|
||||
<input type="file" name="photo" accept="image/*" style="position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer;">`;
|
||||
} else {
|
||||
resetPhotoArea(form);
|
||||
}
|
||||
|
||||
document.getElementById('testimonial-modal').classList.add('active');
|
||||
}
|
||||
|
||||
async function saveTestimonial() {
|
||||
const form = document.getElementById('testimonial-form');
|
||||
const formData = new FormData(form);
|
||||
const id = form.dataset.id;
|
||||
|
||||
if (id) formData.append('id', id);
|
||||
|
||||
try {
|
||||
const result = await MSPE.API.upload('testimonials.php', formData);
|
||||
|
||||
if (result && result.success) {
|
||||
MSPE.showNotification(id ? 'Updated successfully' : 'Created successfully', 'success');
|
||||
closeModal();
|
||||
loadTestimonials();
|
||||
} else {
|
||||
MSPE.showNotification(result?.message || 'Error saving testimonial', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Save error:', error);
|
||||
MSPE.showNotification('Error saving testimonial', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateStatus(id, status) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('id', id);
|
||||
formData.append('status', status);
|
||||
|
||||
const result = await MSPE.API.upload('testimonials.php', formData);
|
||||
|
||||
if (result && result.success) {
|
||||
MSPE.showNotification(`Testimonial ${status}`, 'success');
|
||||
loadTestimonials();
|
||||
} else {
|
||||
MSPE.showNotification(result?.message || 'Error updating status', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
MSPE.showNotification('Error updating status', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTestimonial(id) {
|
||||
if (!await MSPE.confirmDialog('Delete this testimonial?')) return;
|
||||
|
||||
try {
|
||||
const response = await MSPE.API.delete(`testimonials.php?id=${id}`);
|
||||
if (response.success) {
|
||||
MSPE.showNotification('Deleted successfully', 'success');
|
||||
loadTestimonials();
|
||||
}
|
||||
} catch (error) {
|
||||
MSPE.showNotification('Error deleting testimonial', 'error');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+603
@@ -0,0 +1,603 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||
<title>Admin Users - MSPE Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha384-t1nt8BQoYMLFN5p42tRAtuAAFQaCQODekUVeKKZrEnEyp4H2R0RHFz0KWpmj7i8g" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/admin/css/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-wrapper">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<a href="/admin/dashboard.html" class="sidebar-logo">
|
||||
<i class="fas fa-cube"></i>
|
||||
<span>MSPE</span>
|
||||
</a>
|
||||
<button class="sidebar-toggle" id="sidebar-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Main</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/dashboard.html" class="nav-link">
|
||||
<i class="fas fa-th-large"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Content</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/pages.html" class="nav-link">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
<span>Pages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/services.html" class="nav-link">
|
||||
<i class="fas fa-concierge-bell"></i>
|
||||
<span>Services</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/portfolio.html" class="nav-link">
|
||||
<i class="fas fa-briefcase"></i>
|
||||
<span>Portfolio</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/news.html" class="nav-link">
|
||||
<i class="fas fa-newspaper"></i>
|
||||
<span>News & Events</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/testimonials.html" class="nav-link">
|
||||
<i class="fas fa-quote-right"></i>
|
||||
<span>Testimonials</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/team.html" class="nav-link">
|
||||
<i class="fas fa-users"></i>
|
||||
<span>Team</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Media</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/media.html" class="nav-link">
|
||||
<i class="fas fa-images"></i>
|
||||
<span>Media Library</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Inquiries</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/messages.html" class="nav-link">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<span>Messages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/subscribers.html" class="nav-link">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span>Subscribers</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/bookings.html" class="nav-link">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<span class="nav-section-title">Settings</span>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings.html" class="nav-link">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span>Site Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a href="/admin/users.html" class="nav-link">
|
||||
<i class="fas fa-user-shield"></i>
|
||||
<span>Admin Users</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<a href="/index.html" class="view-site-btn" target="_blank">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
<span>View Website</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<!-- Header -->
|
||||
<header class="admin-header">
|
||||
<div class="header-left">
|
||||
<button class="mobile-toggle" id="mobile-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<h1 class="page-title">Admin Users</h1>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<button class="btn btn-primary" onclick="openAddUserModal()">
|
||||
<i class="fas fa-user-plus"></i> Add User
|
||||
</button>
|
||||
|
||||
<div class="header-user">
|
||||
<div class="user-avatar">
|
||||
<img src="https://ui-avatars.com/api/?name=Admin&background=0ea5e9&color=fff" alt="Admin">
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<span class="user-name">Admin</span>
|
||||
<span class="user-role">Administrator</span>
|
||||
</div>
|
||||
<div class="user-dropdown">
|
||||
<button class="dropdown-toggle">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="/admin/settings.html"><i class="fas fa-cog"></i> Settings</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" id="logout-btn"><i class="fas fa-sign-out-alt"></i> Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="content-wrapper">
|
||||
<div class="content-header">
|
||||
<p class="content-description">Manage admin user accounts and permissions</p>
|
||||
</div>
|
||||
|
||||
<!-- Users Table -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-users-cog"></i> User Accounts</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>Last Login</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="6" style="text-align:center;padding:2rem;color:var(--gray-400);">
|
||||
<i class="fas fa-spinner fa-spin"></i> Loading users...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Roles & Permissions -->
|
||||
<div class="card" style="margin-top: 2rem;">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-user-tag"></i> Roles & Permissions</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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>
|
||||
</div>
|
||||
<ul class="permissions-list">
|
||||
<li><i class="fas fa-check"></i> Full system access</li>
|
||||
<li><i class="fas fa-check"></i> Manage all users</li>
|
||||
<li><i class="fas fa-check"></i> Site settings</li>
|
||||
<li><i class="fas fa-check"></i> Delete content</li>
|
||||
<li><i class="fas fa-check"></i> View analytics</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="role-card">
|
||||
<div class="role-header">
|
||||
<span class="role-badge editor">Editor</span>
|
||||
<span class="role-count">1 user</span>
|
||||
</div>
|
||||
<ul class="permissions-list">
|
||||
<li><i class="fas fa-check"></i> Create/edit content</li>
|
||||
<li><i class="fas fa-check"></i> Publish content</li>
|
||||
<li><i class="fas fa-check"></i> Manage media</li>
|
||||
<li><i class="fas fa-check"></i> View messages</li>
|
||||
<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>
|
||||
|
||||
<!-- Activity Log -->
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit User Modal -->
|
||||
<div class="modal" id="user-modal">
|
||||
<div class="modal-backdrop"></div>
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h2 id="modal-title">Add Admin User</h2>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="user-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Full Name *</label>
|
||||
<input type="text" class="form-control" name="name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Username *</label>
|
||||
<input type="text" class="form-control" name="username" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Email Address *</label>
|
||||
<input type="email" class="form-control" name="email" required>
|
||||
</div>
|
||||
|
||||
<div class="form-row" id="password-fields">
|
||||
<div class="form-group">
|
||||
<label>Password *</label>
|
||||
<input type="password" class="form-control" name="password" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Confirm Password *</label>
|
||||
<input type="password" class="form-control" name="password_confirm" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<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>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Status</label>
|
||||
<select class="form-select" name="status">
|
||||
<option value="active">Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="send_welcome" checked>
|
||||
Send welcome email with login details
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" onclick="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="saveUser()">
|
||||
<i class="fas fa-save"></i> Save User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Change Password Modal -->
|
||||
<div class="modal" id="password-modal">
|
||||
<div class="modal-backdrop"></div>
|
||||
<div class="modal-container modal-sm">
|
||||
<div class="modal-header">
|
||||
<h2>Change Password</h2>
|
||||
<button class="modal-close" onclick="closePasswordModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="password-form">
|
||||
<div class="form-group">
|
||||
<label>New Password *</label>
|
||||
<input type="password" class="form-control" name="new_password" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Confirm New Password *</label>
|
||||
<input type="password" class="form-control" name="confirm_password" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="force_logout">
|
||||
Force logout from all sessions
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" onclick="closePasswordModal()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="updatePassword()">
|
||||
<i class="fas fa-key"></i> Update Password
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/admin/js/admin.js"></script>
|
||||
<script>
|
||||
let allUsers = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadUsers();
|
||||
});
|
||||
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const response = await MSPE.API.get('users.php');
|
||||
if (response && response.success) {
|
||||
allUsers = response.data;
|
||||
renderUsers(allUsers);
|
||||
} else {
|
||||
document.querySelector('tbody').innerHTML = '<tr><td colspan="6" class="text-center">Failed to load users</td></tr>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading users:', error);
|
||||
document.querySelector('tbody').innerHTML = '<tr><td colspan="6" class="text-center">Failed to load users</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderUsers(users) {
|
||||
const tbody = document.querySelector('tbody');
|
||||
if (users.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="text-center">No users found</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = users.map(user => `
|
||||
<tr>
|
||||
<td>
|
||||
<div class="user-info">
|
||||
<img src="https://ui-avatars.com/api/?name=${encodeURIComponent(user.name)}&background=random" alt="${user.name}">
|
||||
<div>
|
||||
<span class="name">${user.name}</span>
|
||||
<span class="username">${user.username}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>${user.email}</td>
|
||||
<td><span class="role-badge ${user.role}">${user.role}</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>
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-sm btn-outline" onclick="editUser('${user.id}')">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline" onclick="changePassword('${user.id}')">
|
||||
<i class="fas fa-key"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteUser('${user.id}')">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function openAddUserModal() {
|
||||
document.getElementById('modal-title').textContent = 'Add Admin User';
|
||||
document.getElementById('user-form').reset();
|
||||
document.getElementById('user-form').dataset.id = '';
|
||||
document.getElementById('password-fields').style.display = 'flex';
|
||||
document.getElementById('user-modal').classList.add('active');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('user-modal').classList.remove('active');
|
||||
}
|
||||
|
||||
function editUser(id) {
|
||||
const user = allUsers.find(u => u.id === id);
|
||||
if (!user) return;
|
||||
|
||||
document.getElementById('modal-title').textContent = 'Edit User';
|
||||
const form = document.getElementById('user-form');
|
||||
form.dataset.id = id;
|
||||
|
||||
form.querySelector('[name="name"]').value = user.name;
|
||||
form.querySelector('[name="username"]').value = user.username;
|
||||
form.querySelector('[name="email"]').value = user.email;
|
||||
form.querySelector('[name="role"]').value = user.role;
|
||||
form.querySelector('[name="status"]').value = user.status;
|
||||
|
||||
// Hide password fields for edit
|
||||
document.getElementById('password-fields').style.display = 'none';
|
||||
|
||||
document.getElementById('user-modal').classList.add('active');
|
||||
}
|
||||
|
||||
function deleteUser(id) {
|
||||
MSPE.confirmDialog('Are you sure you want to delete this user?').then(async confirmed => {
|
||||
if (confirmed) {
|
||||
try {
|
||||
const response = await MSPE.API.delete(`users.php?id=${id}`);
|
||||
if (response.success) {
|
||||
MSPE.showNotification('User deleted', 'success');
|
||||
loadUsers();
|
||||
} else {
|
||||
MSPE.showNotification(response.message, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
MSPE.showNotification('Error deleting user', 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function changePassword(id) {
|
||||
document.getElementById('password-form').dataset.id = id;
|
||||
document.getElementById('password-modal').classList.add('active');
|
||||
}
|
||||
|
||||
function closePasswordModal() {
|
||||
document.getElementById('password-modal').classList.remove('active');
|
||||
}
|
||||
|
||||
async function saveUser() {
|
||||
const form = document.getElementById('user-form');
|
||||
const formData = new FormData(form);
|
||||
const id = form.dataset.id;
|
||||
|
||||
if (id) formData.append('id', id);
|
||||
|
||||
try {
|
||||
const result = await MSPE.API.upload('users.php', formData);
|
||||
|
||||
if (result && result.success) {
|
||||
MSPE.showNotification(id ? 'Updated successfully' : 'Created successfully', 'success');
|
||||
closeModal();
|
||||
loadUsers();
|
||||
} else {
|
||||
MSPE.showNotification(result?.message || 'Error saving user', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
MSPE.showNotification('Error saving user', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function updatePassword() {
|
||||
const form = document.getElementById('password-form');
|
||||
const id = form.dataset.id;
|
||||
const newPass = form.querySelector('[name="new_password"]').value;
|
||||
const confirmPass = form.querySelector('[name="confirm_password"]').value;
|
||||
|
||||
if (newPass !== confirmPass) {
|
||||
MSPE.showNotification('Passwords do not match', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('id', id);
|
||||
formData.append('password', newPass);
|
||||
|
||||
try {
|
||||
const result = await MSPE.API.upload('users.php', formData);
|
||||
|
||||
if (result && result.success) {
|
||||
MSPE.showNotification('Password updated', 'success');
|
||||
closePasswordModal();
|
||||
} else {
|
||||
MSPE.showNotification(result?.message || 'Error updating password', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
MSPE.showNotification('Error updating password', 'error');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user