mirror of
https://github.com/myronblair/epictravelexpeditions
synced 2026-06-30 17:50:08 -05:00
209 lines
8.2 KiB
JavaScript
209 lines
8.2 KiB
JavaScript
(function () {
|
|
const API = 'https://epictravelexpeditions.com/api';
|
|
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
#et-admin-section { padding: 32px 0; }
|
|
#et-admin-section h3 { font-size: 1.4rem; font-weight: 700; color: #111827; margin-bottom: 16px; }
|
|
.et-admin-card {
|
|
background: #fff;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
margin-bottom: 16px;
|
|
display: flex;
|
|
gap: 16px;
|
|
align-items: flex-start;
|
|
}
|
|
.et-admin-avatar {
|
|
width: 48px; height: 48px; border-radius: 50%;
|
|
object-fit: cover; flex-shrink: 0; background: #e5e7eb;
|
|
}
|
|
.et-admin-avatar-ph {
|
|
width: 48px; height: 48px; border-radius: 50%;
|
|
background: #2563eb; color: #fff;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-weight: 700; font-size: 18px; flex-shrink: 0;
|
|
}
|
|
.et-admin-body { flex: 1; min-width: 0; }
|
|
.et-admin-name { font-weight: 700; font-size: 14px; }
|
|
.et-admin-loc { font-size: 12px; color: #6b7280; margin-bottom: 6px; }
|
|
.et-admin-msg { font-size: 13px; color: #374151; margin-bottom: 10px; }
|
|
.et-admin-status {
|
|
display: inline-block;
|
|
padding: 2px 10px; border-radius: 999px;
|
|
font-size: 11px; font-weight: 600; text-transform: uppercase;
|
|
margin-bottom: 10px;
|
|
}
|
|
.et-status-pending { background: #fef3c7; color: #92400e; }
|
|
.et-status-approved { background: #d1fae5; color: #065f46; }
|
|
.et-status-denied { background: #fee2e2; color: #991b1b; }
|
|
.et-admin-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
.et-btn {
|
|
padding: 6px 14px; border-radius: 6px; font-size: 12px; font-weight: 600;
|
|
border: none; cursor: pointer; transition: opacity .15s;
|
|
}
|
|
.et-btn:hover { opacity: .8; }
|
|
.et-btn-approve { background: #16a34a; color: #fff; }
|
|
.et-btn-deny { background: #dc2626; color: #fff; }
|
|
.et-btn-delete { background: #6b7280; color: #fff; }
|
|
.et-btn-save { background: #2563eb; color: #fff; }
|
|
.et-edit-area {
|
|
width: 100%; box-sizing: border-box;
|
|
border: 1px solid #d1d5db; border-radius: 6px;
|
|
padding: 8px 10px; font-size: 13px;
|
|
resize: vertical; min-height: 60px; margin-bottom: 6px;
|
|
}
|
|
.et-tabs { display: flex; gap: 4px; margin-bottom: 20px; }
|
|
.et-tab {
|
|
padding: 6px 16px; border-radius: 6px; font-size: 13px; font-weight: 600;
|
|
cursor: pointer; border: 1px solid #d1d5db; background: #fff; color: #374151;
|
|
}
|
|
.et-tab.active { background: #2563eb; color: #fff; border-color: #2563eb; }
|
|
.et-empty { color: #9ca3af; font-size: 14px; text-align: center; padding: 32px 0; }
|
|
`;
|
|
document.head.appendChild(style);
|
|
|
|
function statusBadge(s) {
|
|
return `<span class="et-admin-status et-status-${s}">${s}</span>`;
|
|
}
|
|
|
|
function avatarHtml(t) {
|
|
if (t.image_path) return `<img class="et-admin-avatar" src="${t.image_path}" alt="">`;
|
|
return `<div class="et-admin-avatar-ph">${t.full_name.charAt(0).toUpperCase()}</div>`;
|
|
}
|
|
|
|
async function applyAction(id, payload) {
|
|
const token = localStorage.getItem('auth_token');
|
|
const res = await fetch(`${API}/testimonials/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
return res.json();
|
|
}
|
|
|
|
async function deleteTestimonial(id) {
|
|
const token = localStorage.getItem('auth_token');
|
|
await fetch(`${API}/testimonials/${id}`, {
|
|
method: 'DELETE',
|
|
headers: { Authorization: `Bearer ${token}` }
|
|
});
|
|
}
|
|
|
|
function renderCards(list, container, reload) {
|
|
container.innerHTML = '';
|
|
if (!list.length) {
|
|
container.innerHTML = '<p class="et-empty">No testimonials in this category.</p>';
|
|
return;
|
|
}
|
|
list.forEach(t => {
|
|
const card = document.createElement('div');
|
|
card.className = 'et-admin-card';
|
|
card.dataset.id = t.id;
|
|
card.innerHTML = `
|
|
${avatarHtml(t)}
|
|
<div class="et-admin-body">
|
|
<div class="et-admin-name">${t.full_name}</div>
|
|
<div class="et-admin-loc">${t.location}</div>
|
|
${statusBadge(t.status)}
|
|
<div class="et-admin-msg">${t.message}</div>
|
|
<textarea class="et-edit-area" data-orig="${t.message.replace(/"/g,'"')}">${t.message}</textarea>
|
|
<div class="et-admin-actions">
|
|
${t.status !== 'approved' ? '<button class="et-btn et-btn-approve" data-action="approve">Approve</button>' : ''}
|
|
${t.status !== 'denied' ? '<button class="et-btn et-btn-deny" data-action="deny">Deny</button>' : ''}
|
|
<button class="et-btn et-btn-save" data-action="save">Save Edit</button>
|
|
<button class="et-btn et-btn-delete" data-action="delete">Delete</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
card.querySelectorAll('[data-action]').forEach(btn => {
|
|
btn.addEventListener('click', async () => {
|
|
const action = btn.dataset.action;
|
|
const textarea = card.querySelector('.et-edit-area');
|
|
try {
|
|
if (action === 'approve') await applyAction(t.id, { status: 'approved' });
|
|
else if (action === 'deny') await applyAction(t.id, { status: 'denied' });
|
|
else if (action === 'save') await applyAction(t.id, { message: textarea.value });
|
|
else if (action === 'delete') { if (!confirm('Delete this testimonial?')) return; await deleteTestimonial(t.id); }
|
|
reload();
|
|
} catch (e) { alert('Error: ' + e.message); }
|
|
});
|
|
});
|
|
|
|
container.appendChild(card);
|
|
});
|
|
}
|
|
|
|
async function buildAdminPanel(mountEl) {
|
|
const token = localStorage.getItem('auth_token');
|
|
const res = await fetch(`${API}/testimonials/all`, {
|
|
headers: { Authorization: `Bearer ${token}` }
|
|
});
|
|
const all = await res.json();
|
|
|
|
const section = document.createElement('div');
|
|
section.id = 'et-admin-section';
|
|
section.innerHTML = `
|
|
<h3>Testimonials</h3>
|
|
<div class="et-tabs">
|
|
<button class="et-tab active" data-filter="pending">Pending (${all.filter(t=>t.status==='pending').length})</button>
|
|
<button class="et-tab" data-filter="approved">Approved (${all.filter(t=>t.status==='approved').length})</button>
|
|
<button class="et-tab" data-filter="denied">Denied (${all.filter(t=>t.status==='denied').length})</button>
|
|
<button class="et-tab" data-filter="all">All (${all.length})</button>
|
|
</div>
|
|
<div id="et-admin-cards"></div>
|
|
`;
|
|
|
|
let currentFilter = 'pending';
|
|
const cardsEl = section.querySelector('#et-admin-cards');
|
|
|
|
async function reload() {
|
|
const res = await fetch(`${API}/testimonials/all`, {
|
|
headers: { Authorization: `Bearer ${token}` }
|
|
});
|
|
const fresh = await res.json();
|
|
const filtered = currentFilter === 'all' ? fresh : fresh.filter(t => t.status === currentFilter);
|
|
renderCards(filtered, cardsEl, reload);
|
|
// Update counts
|
|
section.querySelectorAll('.et-tab').forEach(tab => {
|
|
const f = tab.dataset.filter;
|
|
const count = f === 'all' ? fresh.length : fresh.filter(t => t.status === f).length;
|
|
tab.textContent = `${f.charAt(0).toUpperCase()+f.slice(1)} (${count})`;
|
|
if (f === currentFilter) tab.classList.add('active');
|
|
else tab.classList.remove('active');
|
|
});
|
|
}
|
|
|
|
section.querySelectorAll('.et-tab').forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
currentFilter = tab.dataset.filter;
|
|
reload();
|
|
});
|
|
});
|
|
|
|
const initialFiltered = all.filter(t => t.status === 'pending');
|
|
renderCards(initialFiltered, cardsEl, reload);
|
|
mountEl.appendChild(section);
|
|
}
|
|
|
|
// Watch for admin dashboard
|
|
let adminDone = false;
|
|
const observer = new MutationObserver(() => {
|
|
if (adminDone) return;
|
|
if (!window.location.pathname.startsWith('/admin/dashboard')) return;
|
|
if (!localStorage.getItem('isAdminAuthenticated')) return;
|
|
|
|
// Find the main content area of the admin panel
|
|
const main = document.querySelector('[class*="max-w-7xl"]');
|
|
if (!main) return;
|
|
if (document.getElementById('et-admin-section')) return;
|
|
|
|
adminDone = true;
|
|
observer.disconnect();
|
|
buildAdminPanel(main);
|
|
});
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
})();
|