mirror of
https://github.com/myronblair/epictravelexpeditions
synced 2026-06-30 17:50:08 -05:00
314 lines
10 KiB
JavaScript
314 lines
10 KiB
JavaScript
(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 = `<img class="et-avatar" src="${t.image_path}" alt="${t.full_name}" onerror="this.style.display='none'">`;
|
|
} else {
|
|
const initial = t.full_name.charAt(0).toUpperCase();
|
|
avatarHtml = `<div class="et-avatar-placeholder">${initial}</div>`;
|
|
}
|
|
|
|
card.innerHTML = `
|
|
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">
|
|
${avatarHtml}
|
|
<div>
|
|
<p class="et-name">${t.full_name}</p>
|
|
<p class="et-loc">${t.location}</p>
|
|
</div>
|
|
</div>
|
|
<p class="et-msg">“${t.message}”</p>
|
|
`;
|
|
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 = `
|
|
<h2>Share Your Experience</h2>
|
|
<p>Traveled with us? We'd love to hear about it.</p>
|
|
<form id="et-form" novalidate>
|
|
<input id="et-name" type="text" placeholder="Full Name *" required>
|
|
<input id="et-location" type="text" placeholder="City, State / Country *" required>
|
|
<textarea id="et-msg-input" placeholder="Tell us about your trip *" required></textarea>
|
|
<div id="et-img-row">
|
|
<label class="et-file-label" for="et-file-input">
|
|
📷 Add a photo (optional)
|
|
</label>
|
|
<input id="et-file-input" type="file" accept="image/jpeg,image/png,image/webp">
|
|
<img id="et-preview" src="" alt="preview">
|
|
</div>
|
|
<div id="et-msg"></div>
|
|
<button id="et-submit" type="submit">Submit Testimonial</button>
|
|
</form>
|
|
`;
|
|
|
|
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();
|
|
}
|
|
})();
|