beachPartyBalloons/main-site/contact-form.js
chris aee1f10179 Sync native contact form to main-site, replace iframe
Replaces the third-party iframe form on both the homepage and contact page
with the self-hosted form: drag-and-drop photo upload, honeypot, rate
limiting, inline validation, auto-reply email. Adds multer/sharp/nodemailer
dependencies and the /api/contact endpoint to server.js.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 20:32:05 -04:00

176 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(function () {
const form = document.getElementById('contactForm');
if (!form) return;
const dropzone = document.getElementById('dropzone');
const photoInput = document.getElementById('photoInput');
const photoPreview = document.getElementById('photoPreview');
const photoError = document.getElementById('photoError');
const formAlert = document.getElementById('formAlert');
const submitBtn = document.getElementById('submitBtn');
const textarea = form.querySelector('textarea[name="message"]');
const phoneInput = form.querySelector('input[name="phone"]');
let selectedFiles = [];
// Textarea auto-resize
textarea.style.overflow = 'hidden';
textarea.addEventListener('input', function () {
this.style.height = 'auto';
this.style.height = this.scrollHeight + 'px';
});
// Phone auto-format
phoneInput.addEventListener('input', function () {
const digits = this.value.replace(/\D/g, '').slice(0, 10);
if (digits.length >= 7) this.value = '(' + digits.slice(0,3) + ') ' + digits.slice(3,6) + '-' + digits.slice(6);
else if (digits.length >= 4) this.value = '(' + digits.slice(0,3) + ') ' + digits.slice(3);
else this.value = digits;
});
// Clear inline errors on input
form.querySelectorAll('.input, .textarea').forEach(el => {
el.addEventListener('input', () => clearErr(el));
});
function setErr(input, msg) {
input.classList.add('is-danger');
const el = document.getElementById('err-' + input.name);
if (el) { el.textContent = msg; el.style.display = ''; }
}
function clearErr(input) {
input.classList.remove('is-danger');
const el = document.getElementById('err-' + input.name);
if (el) el.style.display = 'none';
}
function validate() {
let ok = true;
const name = form.querySelector('[name="name"]');
if (!name.value.trim()) { setErr(name, 'Please enter your name.'); ok = false; }
else clearErr(name);
const email = form.querySelector('[name="email"]');
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value.trim())) {
setErr(email, 'Please enter a valid email address.'); ok = false;
} else clearErr(email);
const phone = form.querySelector('[name="phone"]');
if (phone.value.replace(/\D/g, '').length < 10) {
setErr(phone, 'Please enter a valid 10-digit phone number.'); ok = false;
} else clearErr(phone);
const msg = form.querySelector('[name="message"]');
if (!msg.value.trim()) { setErr(msg, 'Please enter a message.'); ok = false; }
else clearErr(msg);
return ok;
}
// Dropzone
dropzone.addEventListener('click', () => photoInput.click());
dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.classList.add('drag-over'); });
dropzone.addEventListener('dragleave', () => dropzone.classList.remove('drag-over'));
dropzone.addEventListener('drop', e => {
e.preventDefault();
dropzone.classList.remove('drag-over');
addFiles(Array.from(e.dataTransfer.files));
});
photoInput.addEventListener('change', () => {
addFiles(Array.from(photoInput.files));
photoInput.value = '';
});
function addFiles(newFiles) {
const images = newFiles.filter(f => f.type.startsWith('image/'));
const tooBig = images.filter(f => f.size > 10 * 1024 * 1024);
if (tooBig.length) showAlert('One or more photos exceed 10 MB and were skipped.', 'is-warning');
const ok = images.filter(f => f.size <= 10 * 1024 * 1024);
const combined = [...selectedFiles, ...ok];
photoError.style.display = combined.length > 3 ? '' : 'none';
selectedFiles = combined.slice(0, 3);
renderPreviews();
}
function renderPreviews() {
photoPreview.innerHTML = '';
selectedFiles.forEach((file, i) => {
const url = URL.createObjectURL(file);
const wrap = document.createElement('div');
wrap.className = 'preview-wrap';
const img = document.createElement('img');
img.src = url;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'remove-btn';
btn.innerHTML = '&times;';
btn.addEventListener('click', () => {
URL.revokeObjectURL(url);
selectedFiles.splice(i, 1);
photoError.style.display = 'none';
renderPreviews();
});
wrap.appendChild(img);
wrap.appendChild(btn);
photoPreview.appendChild(wrap);
});
}
// Submit
form.addEventListener('submit', async e => {
e.preventDefault();
formAlert.style.display = 'none';
if (!validate()) {
showAlert('Please correct the errors below.', 'is-danger');
// Scroll to first error
const first = form.querySelector('.is-danger');
if (first) first.scrollIntoView({ behavior: 'smooth', block: 'center' });
return;
}
const fd = new FormData();
fd.append('name', form.querySelector('[name="name"]').value.trim());
fd.append('email', form.querySelector('[name="email"]').value.trim());
fd.append('phone', form.querySelector('[name="phone"]').value.trim());
fd.append('message', form.querySelector('[name="message"]').value.trim());
const et = form.querySelector('[name="eventType"]');
if (et) fd.append('eventType', et.value);
const ed = form.querySelector('[name="eventDate"]');
if (ed) fd.append('eventDate', ed.value);
const hp = form.querySelector('[name="website"]');
if (hp) fd.append('website', hp.value);
selectedFiles.forEach(f => fd.append('photos', f));
submitBtn.classList.add('is-loading');
submitBtn.disabled = true;
try {
const res = await fetch('/api/contact', { method: 'POST', body: fd });
const data = await res.json();
if (data.success) {
showAlert("Message sent! Well get back to you soon. Check your email for a confirmation.", 'is-success');
form.reset();
selectedFiles = [];
renderPreviews();
textarea.style.height = 'auto';
} else {
showAlert(data.message || 'Something went wrong. Please try again.', 'is-danger');
}
} catch {
showAlert('Network error. Please try again or email us directly.', 'is-danger');
} finally {
submitBtn.classList.remove('is-loading');
submitBtn.disabled = false;
}
});
function showAlert(msg, cls) {
formAlert.className = 'notification ' + cls;
formAlert.textContent = msg;
formAlert.style.display = '';
formAlert.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
})();