// public/js/portal.js // Standalone client portal logic – no imports from main app JS to avoid DOM coupling. let portal = null; let portalFormDone = false; // --- Portal helpers: folder + download flag ----------------- function portalFolder() { if (!portal) return 'root'; return portal.folder || portal.targetFolder || portal.path || 'root'; } function portalCanDownload() { if (!portal) return false; // Prefer explicit flags if present if (typeof portal.allowDownload !== 'undefined') { return !!portal.allowDownload; } if (typeof portal.allowDownloads !== 'undefined') { return !!portal.allowDownloads; } // Fallback: uploadOnly = true => no downloads if (typeof portal.uploadOnly !== 'undefined') { return !portal.uploadOnly; } // Default: allow downloads return true; } // ----------------- DOM helpers / status ----------------- function qs(id) { return document.getElementById(id); } function setStatus(msg, isError = false) { const el = qs('portalStatus'); if (!el) return; el.textContent = msg || ''; el.classList.toggle('text-danger', !!isError); if (!isError) { el.classList.add('text-muted'); } } // ----------------- Form submit ----------------- async function submitPortalForm(slug, formData) { const payload = { slug, form: formData }; const headers = { 'X-CSRF-Token': getCsrfToken() || '' }; const res = await sendRequest('/api/pro/portals/submitForm.php', 'POST', payload, headers); if (!res || !res.success) { throw new Error((res && res.error) || 'Error saving form.'); } } // ----------------- Toast ----------------- function showToast(message) { const toast = document.getElementById('customToast'); if (!toast) { console.warn('Toast:', message); return; } toast.textContent = message; toast.style.display = 'block'; // Force reflow void toast.offsetWidth; toast.classList.add('show'); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => { toast.style.display = 'none'; }, 200); }, 2500); } // ----------------- Fetch wrapper ----------------- async function sendRequest(url, method = 'GET', data = null, customHeaders = {}) { const options = { method, credentials: 'include', headers: { ...customHeaders } }; if (data && !(data instanceof FormData)) { options.headers['Content-Type'] = options.headers['Content-Type'] || 'application/json'; options.body = JSON.stringify(data); } else if (data instanceof FormData) { options.body = data; } const res = await fetch(url, options); const text = await res.text(); let payload; try { payload = JSON.parse(text); } catch { payload = text; } if (!res.ok) { throw payload; } return payload; } // ----------------- Portal form wiring ----------------- function setupPortalForm(slug) { const formSection = qs('portalFormSection'); const uploadSection = qs('portalUploadSection'); if (!portal || !portal.requireForm) { if (formSection) formSection.style.display = 'none'; if (uploadSection) uploadSection.style.opacity = '1'; return; } const key = 'portalFormDone:' + slug; if (sessionStorage.getItem(key) === '1') { portalFormDone = true; if (formSection) formSection.style.display = 'none'; if (uploadSection) uploadSection.style.opacity = '1'; return; } portalFormDone = false; if (formSection) formSection.style.display = 'block'; if (uploadSection) uploadSection.style.opacity = '0.5'; const nameEl = qs('portalFormName'); const emailEl = qs('portalFormEmail'); const refEl = qs('portalFormReference'); const notesEl = qs('portalFormNotes'); const submitBtn = qs('portalFormSubmit'); const fd = portal.formDefaults || {}; if (nameEl && fd.name && !nameEl.value) { nameEl.value = fd.name; } if (emailEl && fd.email && !emailEl.value) { emailEl.value = fd.email; } else if (emailEl && portal.clientEmail && !emailEl.value) { // fallback to clientEmail emailEl.value = portal.clientEmail; } if (refEl && fd.reference && !refEl.value) { refEl.value = fd.reference; } if (notesEl && fd.notes && !notesEl.value) { notesEl.value = fd.notes; } if (!submitBtn) return; submitBtn.onclick = async () => { const name = nameEl ? nameEl.value.trim() : ''; const email = emailEl ? emailEl.value.trim() : ''; const reference = refEl ? refEl.value.trim() : ''; const notes = notesEl ? notesEl.value.trim() : ''; const req = portal.formRequired || {}; const missing = []; if (req.name && !name) missing.push('name'); if (req.email && !email) missing.push('email'); if (req.reference && !reference) missing.push('reference'); if (req.notes && !notes) missing.push('notes'); if (missing.length) { showToast('Please fill in: ' + missing.join(', ') + '.'); return; } // default behavior when no specific required flags: if (!req.name && !req.email && !req.reference && !req.notes) { if (!name && !email) { showToast('Please provide at least a name or email.'); return; } } try { await submitPortalForm(slug, { name, email, reference, notes }); portalFormDone = true; sessionStorage.setItem(key, '1'); if (formSection) formSection.style.display = 'none'; if (uploadSection) uploadSection.style.opacity = '1'; showToast('Thank you. You can now upload files.'); } catch (e) { console.error(e); showToast('Error saving your info. Please try again.'); } }; } // ----------------- CSRF helpers ----------------- function setCsrfToken(token) { if (!token) return; window.csrfToken = token; try { localStorage.setItem('csrf', token); } catch { // ignore } let meta = document.querySelector('meta[name="csrf-token"]'); if (!meta) { meta = document.createElement('meta'); meta.name = 'csrf-token'; document.head.appendChild(meta); } meta.content = token; } function getCsrfToken() { return window.csrfToken || (document.querySelector('meta[name="csrf-token"]')?.content) || ''; } async function loadCsrfToken() { const res = await fetch('/api/auth/token.php', { method: 'GET', credentials: 'include' }); const hdr = res.headers.get('X-CSRF-Token'); if (hdr) setCsrfToken(hdr); let body = {}; try { body = await res.json(); } catch { body = {}; } const token = body.csrf_token || getCsrfToken(); setCsrfToken(token); } // ----------------- Auth ----------------- async function ensureAuthenticated() { try { const data = await sendRequest('/api/auth/checkAuth.php', 'GET'); if (!data || !data.username) { // redirect to main UI/login; after login, user can re-open portal link const target = encodeURIComponent(window.location.href); window.location.href = '/portal-login.html?redirect=' + target; return null; } const lbl = qs('portalUserLabel'); if (lbl) { lbl.textContent = data.username || ''; } return data; } catch (e) { const target = encodeURIComponent(window.location.href); window.location.href = '/portal-login.html?redirect=' + target; return null; } } // ----------------- Portal fetch + render ----------------- async function fetchPortal(slug) { setStatus('Loading portal details…'); try { const data = await sendRequest('/api/pro/portals/get.php?slug=' + encodeURIComponent(slug), 'GET'); if (!data || !data.success || !data.portal) { throw new Error((data && data.error) || 'Portal not found.'); } portal = data.portal; return portal; } catch (e) { console.error(e); setStatus('This portal could not be found or is no longer available.', true); showToast('Portal not found or expired.'); return null; } } function renderPortalInfo() { if (!portal) return; const titleEl = qs('portalTitle'); const descEl = qs('portalDescription'); const subtitleEl = qs('portalSubtitle'); const brandEl = document.getElementById('portalBrandHeading'); const footerEl = document.getElementById('portalFooter'); const drop = qs('portalDropzone'); const card = document.querySelector('.portal-card'); const formBtn = qs('portalFormSubmit'); const refreshBtn = qs('portalRefreshBtn'); const filesSection = qs('portalFilesSection'); const heading = portal.title && portal.title.trim() ? portal.title.trim() : (portal.label || portal.slug || 'Client portal'); if (titleEl) titleEl.textContent = heading; if (brandEl) brandEl.textContent = heading; if (descEl) { if (portal.introText && portal.introText.trim()) { descEl.textContent = portal.introText.trim(); } else { const folder = portalFolder(); descEl.textContent = 'Files you upload here go directly into: ' + folder; } } if (subtitleEl) { const parts = []; if (portal.uploadOnly) parts.push('upload only'); if (portalCanDownload()) parts.push('download allowed'); subtitleEl.textContent = parts.length ? parts.join(' • ') : ''; } if (footerEl) { footerEl.textContent = portal.footerText && portal.footerText.trim() ? portal.footerText.trim() : ''; } const color = portal.brandColor && portal.brandColor.trim(); if (color) { // expose brand color as a CSS variable for gallery styling document.documentElement.style.setProperty('--portal-accent', color); if (drop) { drop.style.borderColor = color; } if (card) { card.style.borderTop = '3px solid ' + color; } if (formBtn) { formBtn.style.backgroundColor = color; formBtn.style.borderColor = color; } if (refreshBtn) { refreshBtn.style.borderColor = color; refreshBtn.style.color = color; } } // Show/hide files section based on download capability if (filesSection) { filesSection.style.display = portalCanDownload() ? 'block' : 'none'; } } // ----------------- File helpers for gallery ----------------- function formatFileSizeLabel(f) { // API currently returns f.size as a human-readable string, so prefer that if (f && f.size) return f.size; return ''; } function fileExtLabel(name) { if (!name) return 'FILE'; const parts = name.split('.'); if (parts.length < 2) return 'FILE'; const ext = parts.pop().trim().toUpperCase(); if (!ext) return 'FILE'; return ext.length <= 4 ? ext : ext.slice(0, 4); } function isImageName(name) { if (!name) return false; return /\.(jpe?g|png|gif|bmp|webp|svg)$/i.test(name); } // ----------------- Load files for portal gallery ----------------- async function loadPortalFiles() { if (!portal || !portalCanDownload()) return; const listEl = qs('portalFilesList'); if (!listEl) return; listEl.innerHTML = '