Files
epictravelexpeditions/static/js/admin-testimonials.js
T
2026-05-22 12:52:45 +00:00

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,'&quot;')}">${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 });
})();