Initial commit

This commit is contained in:
2026-05-22 12:52:45 +00:00
commit 0f11edc62e
34 changed files with 3095 additions and 0 deletions
+313
View File
@@ -0,0 +1,313 @@
(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">&ldquo;${t.message}&rdquo;</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();
}
})();