document.addEventListener('DOMContentLoaded', () => { const form = document.getElementById('projectForm'); const overlay = document.getElementById('overlay'); const overlayMessage = document.getElementById('overlay-message'); const overlayClose = document.getElementById('overlay-close'); const materialSelect = document.getElementById('material'); const thicknessSelect = document.getElementById('thickness'); const fileInput = document.getElementById('fileInput'); const fileList = document.getElementById('fileList'); const dropZone = document.getElementById('dropZone'); const deadlineInput = document.getElementById('deadline'); const uploadProgress = document.getElementById('uploadProgress'); const uploadBar = document.getElementById('uploadBar'); const fileCount = document.getElementById('fileCount'); const MAX_FILES = 10; const MAX_FILE_SIZE = 1 * 1024 * 1024; const allowedExt = ['dxf','dwg','pdf','svg','3dm','stp','step','igs','iges','stl','jpg','jpeg','png','gif','bmp']; let storedFiles = []; let rushFeeAcknowledged = false; function ext(name) { return name.slice(name.lastIndexOf('.') + 1).toLowerCase(); } function render(files) { fileList.innerHTML = ''; files.forEach((f, i) => { const item = document.createElement('div'); item.className = 'file-item'; const remove = document.createElement('button'); remove.type = 'button'; remove.className = 'file-remove'; remove.textContent = '✕'; remove.onclick = () => { storedFiles.splice(i, 1); updateFileInput(); }; const name = document.createElement('span'); name.textContent = f.name; name.style.flexGrow = '1'; name.style.overflow = 'hidden'; name.style.textOverflow = 'ellipsis'; name.style.whiteSpace = 'nowrap'; item.appendChild(remove); item.appendChild(name); fileList.appendChild(item); }); fileCount.textContent = `${files.length} of ${MAX_FILES} files selected`; } function updateFileInput() { const dt = new DataTransfer(); storedFiles.forEach(f => dt.items.add(f)); fileInput.files = dt.files; render(storedFiles); } function handleFiles(newFiles) { const errs = []; const seen = new Set(storedFiles.map(f => f.name + '|' + f.size)); const combined = [...storedFiles]; Array.from(newFiles).forEach(f => { if (combined.length >= MAX_FILES) { errs.push(`Maximum of ${MAX_FILES} files allowed.`); return; } const key = f.name + '|' + f.size; const e = ext(f.name); const isImage = f.type.startsWith('image/'); const ok = isImage || allowedExt.includes(e); if (!ok) { errs.push(`${f.name} not supported`); return; } if (f.size > MAX_FILE_SIZE) { errs.push(`${f.name} exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB`); return; } if (!seen.has(key)) { combined.push(f); seen.add(key); } }); storedFiles = combined; updateFileInput(); if (errs.length) alert(errs.join('\n')); } ['dragenter','dragover'].forEach(evt => { dropZone.addEventListener(evt, e => { e.preventDefault(); dropZone.classList.add('dragover'); }); }); ['dragleave','drop'].forEach(evt => { dropZone.addEventListener(evt, e => { e.preventDefault(); dropZone.classList.remove('dragover'); }); }); window.addEventListener('drop', e => { if (!e.target.closest('.drop-zone')) { e.preventDefault(); } }); window.addEventListener('dragover', e => { if (!e.target.closest('.drop-zone')) { e.preventDefault(); } }); dropZone.addEventListener('drop', e => handleFiles(e.dataTransfer.files)); fileInput.addEventListener('change', () => handleFiles(fileInput.files)); function showOverlay(msg) { overlayMessage.textContent = msg; overlay.style.display = 'block'; } overlayClose.addEventListener('click', () => { overlay.style.display = 'none'; }); // Custom validation bubble functions function createValidationBubble(element, message, canDismiss = false) { // Remove any existing bubble removeValidationBubble(); const bubble = document.createElement('div'); bubble.id = 'custom-validation-bubble'; bubble.style.cssText = ` position: absolute; background: #333; color: white; padding: 8px 12px; border-radius: 4px; font-size: 12px; max-width: 200px; z-index: 1000; box-shadow: 0 2px 8px rgba(0,0,0,0.3); ${canDismiss ? 'padding-right: 30px;' : ''} `; const messageSpan = document.createElement('span'); messageSpan.textContent = message; bubble.appendChild(messageSpan); if (canDismiss) { const closeBtn = document.createElement('button'); closeBtn.textContent = '×'; closeBtn.style.cssText = ` position: absolute; right: 4px; top: 2px; background: none; border: none; color: white; font-size: 16px; cursor: pointer; padding: 0; width: 20px; height: 20px; line-height: 16px; `; closeBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); removeValidationBubble(); if (element === deadlineInput) { rushFeeAcknowledged = true; } }; bubble.appendChild(closeBtn); } // Position the bubble relative to the element's parent container const elementRect = element.getBoundingClientRect(); const container = element.closest('.form-group'); // Insert bubble into the same container as the form field container.style.position = 'relative'; bubble.style.position = 'absolute'; bubble.style.left = (elementRect.width + 10) + 'px'; bubble.style.top = '0px'; container.appendChild(bubble); // Auto-remove after delay if not dismissible if (!canDismiss) { setTimeout(() => removeValidationBubble(), 3000); } } function removeValidationBubble() { const existing = document.getElementById('custom-validation-bubble'); if (existing) { existing.remove(); } } // Date validation function function validateDeadline(dateStr) { if (!dateStr) return { valid: true }; // Empty is OK const deadlineDate = new Date(dateStr); const today = new Date(); today.setHours(0, 0, 0, 0); // Set to start of day deadlineDate.setHours(0, 0, 0, 0); if (isNaN(deadlineDate.getTime())) { return { valid: false, message: 'Please enter a valid date.' }; } if (deadlineDate < today) { return { valid: false, message: 'Deadline must be in the future.' }; } // Check if deadline is less than a week away const oneWeekFromNow = new Date(today); oneWeekFromNow.setDate(today.getDate() + 7); if (deadlineDate < oneWeekFromNow) { return { valid: true, warning: 'Lead times of under a week may be subject to a rush fee.' }; } return { valid: true }; } // Add deadline validation on change deadlineInput.addEventListener('change', function() { const validation = validateDeadline(this.value); rushFeeAcknowledged = false; // Reset acknowledgment on change removeValidationBubble(); // Remove any existing bubble if (!validation.valid) { this.setCustomValidity(validation.message); } else { this.setCustomValidity(''); } }); form.addEventListener('submit', async e => { e.preventDefault(); // Validate deadline before submission const deadlineValidation = validateDeadline(deadlineInput.value); if (!deadlineValidation.valid) { deadlineInput.setCustomValidity(deadlineValidation.message); deadlineInput.reportValidity(); return; } else if (deadlineValidation.warning && !rushFeeAcknowledged) { // Show custom dismissible rush fee warning createValidationBubble(deadlineInput, deadlineValidation.warning, true); return; } else { deadlineInput.setCustomValidity(''); } // Validate custom thickness format for "other" materials const materialValue = materialSelect.value; const thicknessOther = document.getElementById('thickness_other'); if (materialValue === 'other' && thicknessOther.value.trim()) { const thicknessPattern = /^\d*\.?\d+\s*(mm|cm|in|ft|"|')?$/i; if (!thicknessPattern.test(thicknessOther.value.trim())) { alert('Please enter a valid thickness (e.g., 1.5mm, 0.25", 2cm, 1in, 2ft, 3\')'); return; } } const formData = new FormData(); [...new FormData(form).entries()].forEach(([k, v]) => { if (k !== 'files[]') formData.append(k, v); }); storedFiles.forEach(f => formData.append('files[]', f)); uploadProgress.style.display = 'block'; uploadBar.style.width = '0%'; try { const xhr = new XMLHttpRequest(); xhr.open('POST', form.action, true); xhr.upload.onprogress = e => { if (e.lengthComputable) { const percent = (e.loaded / e.total) * 100; uploadBar.style.width = percent.toFixed(1) + '%'; } }; xhr.onload = () => { uploadProgress.style.display = 'none'; if (xhr.status === 200) { const data = JSON.parse(xhr.responseText); showOverlay(data.message); form.reset(); storedFiles = []; updateFileInput(); deadlineInput.value = ''; updateThickness(); deadlineInput.setCustomValidity(''); // Clear any validation state rushFeeAcknowledged = false; // Reset acknowledgment removeValidationBubble(); // Remove any bubbles } else { showOverlay('An error occurred. Please email us at service@nealscnc.com or call 510-783-3156.'); } }; xhr.onerror = () => { uploadProgress.style.display = 'none'; showOverlay('Network error. Please try again.'); }; xhr.send(formData); } catch (err) { uploadProgress.style.display = 'none'; showOverlay('Unexpected error.'); } }); // Clean up bubbles when clicking elsewhere document.addEventListener('click', (e) => { const bubble = document.getElementById('custom-validation-bubble'); if (bubble && !bubble.contains(e.target) && e.target !== deadlineInput) { removeValidationBubble(); } }); // Generate thickness options from PHP data in form_data.php const thicknessOptions = {"plywood":["1\/8\"","1\/4\"","3\/8\"","1\/2\"","3\/4\"","1\""],"solidwood":["1\"","1-1\/4\"","1-1\/2\"","1-3\/4\"","2\""],"acrylic":["1\/8\"","3\/16\"","1\/4\"","3\/8\"","1\/2\"","3\/4\"","1\""],"polycarb":["1\/8\"","3\/16\"","1\/4\"","3\/8\"","1\/2\"","3\/4\"","1\""],"hdpe":["3\/8\"","1\/2\"","3\/4\"","1\"","1-1\/4\"","1-1\/2\"","1-3\/4\"","2\""],"abs":["1\/16\"","1\/8\"","3\/16\"","1\/4\"","3\/8\"","1\/2\"","3\/4\""],"delrin":["1\/4\" or less","3\/8\"","1\/2\"","3\/4\"","1\"","1-1\/4\"","1-1\/2\"","1-3\/4\"","2\""],"aluminum":["1\/16\" or less","1\/8\"","1\/4\"","1\/2\"","3\/4\""],"brass":["1\/16\" or less","1\/8\"","1\/4\"","1\/2\"","3\/4\""],"hdu":["3\/8\"","1\/2\"","3\/4\"","1\"","1-1\/4\"","1-1\/2\"","1-3\/4\"","2\"","2-1\/2\"","3\""],"sintra":["1\/8\"","1\/4\"","3\/8\"","1\/2\"","3\/4\"","1\""],"foamcore":["1\/4\"","3\/8\"","1\/2\"","3\/4\"","1\""],"ssp":["1\/4\"","3\/8\"","1\/2\""],"acm":["3mm","4mm","6mm"],"lam":[],"fiberglass":["1\/16\" or less","1\/8\"","3\/16\"","1\/4\""],"other":[]}; function updateThickness() { const materialKey = materialSelect.value; const opts = thicknessOptions[materialKey] || []; thicknessSelect.innerHTML = ''; if (opts.length) { thicknessSelect.disabled = false; thicknessSelect.append(new Option('Select thickness', '')); opts.forEach(o => thicknessSelect.append(new Option(o, o))); } else { thicknessSelect.disabled = true; thicknessSelect.append(new Option('N/A', '')); } } materialSelect.addEventListener('change', updateThickness); updateThickness(); // Job Role "Other" toggle document.getElementById('job_role').addEventListener('change', function() { const jobRoleOther = document.getElementById('job_role_other'); jobRoleOther.style.display = this.value === 'other' ? 'block' : 'none'; }); // Material "Other" toggle and thickness handling - FIXED document.getElementById('material').addEventListener('change', function() { const isOther = this.value === 'other'; const materialOther = document.getElementById('material_other'); const thicknessOther = document.getElementById('thickness_other'); const thicknessSelect = document.getElementById('thickness'); // Show/hide material other input materialOther.style.display = isOther ? 'block' : 'none'; // Show/hide thickness inputs based on material selection if (isOther) { // For "other" materials, hide dropdown and show custom input thicknessSelect.style.display = 'none'; thicknessOther.style.display = 'block'; } else { // For standard materials, show dropdown and hide custom input thicknessSelect.style.display = 'block'; thicknessOther.style.display = 'none'; updateThickness(); } }); // CAD Program "Other" toggle document.getElementById('cad_program').addEventListener('change', function() { const cadOther = document.getElementById('cad_program_other'); cadOther.style.display = this.value === 'other' ? 'block' : 'none'; }); // Delivery address toggle document.querySelectorAll('input[name="pickup_delivery"]').forEach(radio => { radio.addEventListener('change', function() { document.getElementById('delivery_address_section').style.display = this.value === 'Delivery' ? 'block' : 'none'; }); }); });