(function () {
const API = 'https://epictravelexpeditions.com/api';
/* ── Styles ── */
const style = document.createElement('style');
style.textContent = `
.et-scroll-track { overflow: hidden; position: relative; }
.et-scroll-track::before,
.et-scroll-track::after {
content: '';
position: absolute;
top: 0; bottom: 0;
width: 80px;
z-index: 2;
pointer-events: none;
}
.et-scroll-track::before { left: 0; background: linear-gradient(to right, white, transparent); }
.et-scroll-track::after { right: 0; background: linear-gradient(to left, white, transparent); }
.et-scroll-inner {
display: flex;
gap: 24px;
width: max-content;
animation: et-slide 40s linear infinite;
}
.et-scroll-inner:hover { animation-play-state: paused; }
@keyframes et-slide {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
.et-card {
width: 300px;
flex-shrink: 0;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0,0,0,.06);
transition: box-shadow .2s;
}
.et-card:hover { box-shadow: 0 6px 20px rgba(0,0,0,.12); }
.et-avatar {
width: 56px; height: 56px;
border-radius: 50%;
object-fit: cover;
background: #e5e7eb;
}
.et-avatar-placeholder {
width: 56px; height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, #3b82f6, #2563eb);
display: flex; align-items: center; justify-content: center;
color: #fff; font-size: 22px; font-weight: 700;
}
.et-name { font-weight: 700; font-size: 15px; color: #111827; margin: 0; }
.et-loc { font-size: 13px; color: #6b7280; margin: 2px 0 12px; }
.et-msg { font-size: 14px; color: #374151; line-height: 1.6; font-style: italic; }
/* ── Form ── */
#et-form-section {
background: #f9fafb;
padding: 64px 24px;
text-align: center;
}
#et-form-section h2 {
font-size: 2rem; font-weight: 700; color: #111827; margin-bottom: 8px;
}
#et-form-section p {
color: #6b7280; margin-bottom: 32px;
}
#et-form {
max-width: 560px;
margin: 0 auto;
text-align: left;
display: flex;
flex-direction: column;
gap: 16px;
}
#et-form input, #et-form textarea {
width: 100%; box-sizing: border-box;
padding: 10px 14px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
color: #111827;
background: #fff;
outline: none;
transition: border-color .15s;
}
#et-form input:focus, #et-form textarea:focus { border-color: #3b82f6; }
#et-form textarea { resize: vertical; min-height: 100px; }
.et-file-label {
display: flex; align-items: center; gap: 10px;
padding: 10px 14px;
border: 1px dashed #d1d5db;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
color: #6b7280;
background: #fff;
transition: border-color .15s;
}
.et-file-label:hover { border-color: #3b82f6; color: #3b82f6; }
#et-file-input { display: none; }
#et-preview { width: 64px; height: 64px; border-radius: 50%; object-fit: cover; display: none; }
#et-submit {
padding: 12px 24px;
background: #2563eb;
color: #fff;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: background .15s;
align-self: flex-end;
}
#et-submit:hover { background: #1d4ed8; }
#et-submit:disabled { background: #93c5fd; cursor: not-allowed; }
#et-msg { font-size: 14px; text-align: center; }
.et-success { color: #16a34a; }
.et-error { color: #dc2626; }
#et-img-row { display: flex; align-items: center; gap: 12px; }
`;
document.head.appendChild(style);
/* ── Build a testimonial card ── */
function buildCard(t) {
const card = document.createElement('div');
card.className = 'et-card';
let avatarHtml;
if (t.image_path) {
avatarHtml = ``;
} else {
const initial = t.full_name.charAt(0).toUpperCase();
avatarHtml = `
${t.full_name}
${t.location}
“${t.message}”
`; return card; } /* ── Replace static testimonials section ── */ function replaceTestimonials(section, testimonials) { const inner = document.createElement('div'); inner.className = 'et-scroll-inner'; const cards = testimonials.map(buildCard); cards.forEach(c => inner.appendChild(c)); // Duplicate for seamless loop cards.forEach(c => inner.appendChild(c.cloneNode(true))); const track = document.createElement('div'); track.className = 'et-scroll-track'; track.appendChild(inner); // Keep the heading, replace the grid const heading = section.querySelector('[class*="grid"]') || section.lastElementChild; if (heading) heading.replaceWith(track); else section.appendChild(track); } /* ── Upload image (no auth required for testimonials) ── */ async function uploadImage(file) { const fd = new FormData(); fd.append('file', file); const res = await fetch(`${API}/testimonials/upload`, { method: 'POST', body: fd }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Upload failed'); return data.url; } /* ── Submission form ── */ function buildForm() { const section = document.createElement('section'); section.id = 'et-form-section'; section.innerHTML = `Traveled with us? We'd love to hear about it.
`; setTimeout(() => { const form = document.getElementById('et-form'); const nameEl = document.getElementById('et-name'); const locEl = document.getElementById('et-location'); const msgEl = document.getElementById('et-msg-input'); const fileEl = document.getElementById('et-file-input'); const preview = document.getElementById('et-preview'); const statusEl = document.getElementById('et-msg'); const submitBtn = document.getElementById('et-submit'); fileEl.addEventListener('change', () => { const f = fileEl.files[0]; if (f) { preview.src = URL.createObjectURL(f); preview.style.display = 'block'; } }); form.addEventListener('submit', async (e) => { e.preventDefault(); statusEl.textContent = ''; const name = nameEl.value.trim(); const loc = locEl.value.trim(); const msg = msgEl.value.trim(); if (!name || !loc || !msg) { statusEl.textContent = 'Please fill in all required fields.'; statusEl.className = 'et-error'; return; } submitBtn.disabled = true; submitBtn.textContent = 'Submitting…'; try { let imagePath = null; if (fileEl.files[0]) { imagePath = await uploadImage(fileEl.files[0]); } const res = await fetch(`${API}/testimonials`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ full_name: name, location: loc, message: msg, image_path: imagePath }) }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Submission failed'); statusEl.textContent = 'Thank you! Your testimonial has been submitted for review.'; statusEl.className = 'et-success'; form.reset(); preview.style.display = 'none'; } catch (err) { statusEl.textContent = err.message; statusEl.className = 'et-error'; } finally { submitBtn.disabled = false; submitBtn.textContent = 'Submit Testimonial'; } }); }, 0); return section; } /* ── Main: observe DOM until static testimonials appear ── */ async function init() { let testimonials = []; try { const res = await fetch(`${API}/testimonials`); testimonials = await res.json(); } catch (_) {} let done = false; const observer = new MutationObserver(() => { if (done) return; const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); let node; while ((node = walker.nextNode())) { if (node.nodeValue && node.nodeValue.includes('What Our Travelers Say')) { // Walk up to the section/div container let el = node.parentElement; for (let i = 0; i < 6; i++) { if (el && (el.tagName === 'SECTION' || (el.className && el.className.includes('py-')))) break; el = el ? el.parentElement : null; } if (!el) break; done = true; observer.disconnect(); if (testimonials.length > 0) { replaceTestimonials(el, testimonials); } // Inject form after the section const formSection = buildForm(); el.after(formSection); break; } } }); observer.observe(document.body, { childList: true, subtree: true }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();