mirror of
https://github.com/myronblair/epictravelexpeditions
synced 2026-06-30 17:50:08 -05:00
Initial commit
This commit is contained in:
@@ -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">“${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();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user