Contact Us

We respond within 2 business days. All fields are required.

Phone
Mon–Fri, 9:00–17:00
Email
Support & billing
Address
27 Market St, Suite 210
San Francisco, CA 94105

All fields are required. We’ll use your email to reply. Your message is not sent in this demo.

At least 2 characters.
We’ll reply to this address.
Digits and symbols: + ( ) - spaces. Min 7 characters.
Helps us route your message.
Minimum 10 characters.
0/1000
By sending, you agree to our Privacy Policy.
Expected response window
Next review in: 00:00
A small timer to indicate when we next check incoming messages (demo-only).

Before you message us

Course access issues
Include the email used at checkout and the course name. If possible, add an approximate purchase date.
Invoices & billing
Let us know the order reference and whether you need an invoice for a company or for personal records.
Partnerships
Share your website and how you’d like to collaborate: co-created lessons, bulk seats, or workshops.
Accessibility
If anything is difficult to use, tell us what device/browser you’re on and what you expected to happen.
Please check the form
Correct the highlighted fields, then try again.
Message ready
Your message has been prepared. Since this is a demo, it won't be sent to a server.
Preview
Review your message details before sending.
From
·
Topic
Message
Preview is local and not submitted.
String(raw || '').trim(); if (!s) return ''; const cleaned = s.replace(/[^\d+]/g,''); if (cleaned.startsWith('+')) return '+' + cleaned.slice(1).replace(/\D/g,''); return cleaned.replace(/\D/g,''); } function validateAll(form) { const errs = []; const name = form.name.value.trim(); const email = form.email.value.trim(); const phone = form.phone.value.trim(); const message = form.message.value.trim(); const topic = (form.topic?.value || '').trim(); setInlineError('name',''); setInlineError('email',''); setInlineError('phone',''); setInlineError('message',''); if (!name) { errs.push('Name is required.'); setInlineError('name','Please enter your name.'); } else if (name.length < 2) { errs.push('Name looks too short.'); setInlineError('name','Name should be at least 2 characters.'); } else if (name.length > 80) { errs.push('Name is too long.'); setInlineError('name','Name must be 80 characters or fewer.'); } if (!email) { errs.push('A valid email is required.'); setInlineError('email','Please enter your email address.'); } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { errs.push('A valid email is required.'); setInlineError('email','Please enter a valid email (e.g., [email protected]).'); } else if (email.length > 120) { errs.push('Email is too long.'); setInlineError('email','Email must be 120 characters or fewer.'); } if (phone) { const np = normalizePhone(phone); const digits = np.replace(/^\+/,''); if (digits.length < 7 || digits.length > 15) { errs.push('Phone number appears invalid (7–15 digits).'); setInlineError('phone','Phone should contain 7–15 digits (with optional +country code).'); } } if (!message) { errs.push('Message cannot be empty.'); setInlineError('message','Please add a message so we can help.'); } else if (message.length < 10) { errs.push('Message is too short.'); setInlineError('message','Please provide at least 10 characters.'); } else if (message.length > 1200) { errs.push('Message is too long (max 1200).'); setInlineError('message','Please keep it under 1200 characters.'); } if (topic && topic.length > 40) errs.push('Topic value is invalid.'); return errs; } function renderErrorModal(errs) { const ul = document.getElementById('error-list'); ul.innerHTML = errs.map(x => `
  • ${String(x).replaceAll('&','&').replaceAll('<','<').replaceAll('>','>')}
  • `).join(''); document.getElementById('error-modal').showModal(); } function updateMsgCount(form) { const n = (form.message.value || '').length; $('#msgCount').textContent = String(n); if (n > 1200) { $('#msgCount').classList.add('text-red-600','font-semibold'); } else { $('#msgCount').classList.remove('text-red-600','font-semibold'); } } function setAutosaveState(t) { const el = $('#autosaveState'); el.textContent = t; } function setDraftIndicator(hasDraft, metaText) { const dot = $('#draft-dot'); const text = $('#draft-text'); if (hasDraft) { dot.classList.remove('bg-slate-300'); dot.classList.add('bg-amber-400'); text.textContent = metaText || 'Draft saved'; } else { dot.classList.add('bg-slate-300'); dot.classList.remove('bg-amber-400'); text.textContent = 'No draft'; } } function escapeText(s) { return String(s ?? '').replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('"','"').replaceAll("'","'"); } function fmtTs(ts) { const d = new Date(ts); if (Number.isNaN(d.getTime())) return 'Unknown time'; return d.toLocaleString(undefined, {year:'numeric', month:'short', day:'2-digit', hour:'2-digit', minute:'2-digit'}); } const x7p1c = { draftKey: 'sc_contact_draft_v1', historyKey: 'sc_contact_history_v1', maxVersions: 10, saveDebounce: 450, historyIntervalMs: 12000, lastInputAt: 0, debounceTimer: null, historyTimer: null, draftId: null, }; function readDraft() { return safeJsonParse(localStorage.getItem(x7p1c.draftKey) || '{}', {}); } function writeDraft(payload) { localStorage.setItem(x7p1c.draftKey, JSON.stringify(payload)); } function clearDraft() { localStorage.removeItem(x7p1c.draftKey); } function readHistory() { const arr = safeJsonParse(localStorage.getItem(x7p1c.historyKey) || '[]', []); return Array.isArray(arr) ? arr : []; } function writeHistory(arr) { localStorage.setItem(x7p1c.historyKey, JSON.stringify(arr)); } function addHistoryVersion(version) { const arr = readHistory(); arr.unshift(version); const trimmed = arr.slice(0, x7p1c.maxVersions); writeHistory(trimmed); renderHistoryList(trimmed); } function snapshotFromForm(form) { const fd = new FormData(form); const obj = Object.fromEntries(fd.entries()); obj.name = (obj.name || '').toString(); obj.email = (obj.email || '').toString(); obj.phone = (obj.phone || '').toString(); obj.topic = (obj.topic || '').toString(); obj.message = (obj.message || '').toString(); obj.consent = form.consent?.checked ? 'yes' : 'no'; obj.client_tz = $('#client_tz').value || ''; obj.draft_id = $('#draft_id').value || ''; return obj; } function restoreToForm(form, obj) { form.name.value = (obj?.name ?? '').toString(); form.email.value = (obj?.email ?? '').toString(); form.phone.value = (obj?.phone ?? '').toString(); if (form.topic) form.topic.value = (obj?.topic ?? 'Courses').toString(); form.message.value = (obj?.message ?? '').toString(); if (form.consent) form.consent.checked = (obj?.consent === 'yes' || obj?.consent === true); updateMsgCount(form); ['name','email','phone','message'].forEach(n => setInlineError(n,'')); } function renderHistoryList(arr) { const host = $('#historyList'); const list = Array.isArray(arr) ? arr : []; if (!list.length) { host.innerHTML = '
    No versions yet. Start typing to create history.
    '; return; } host.innerHTML = list.map((v, idx) => { const title = (v?.data?.message || '').toString().trim().slice(0, 80) || '(empty message)'; const who = (v?.data?.name || '').toString().trim() || 'Unknown'; const when = fmtTs(v?.ts); const id = escapeText(v?.id || ''); return `
    ${escapeText(who)} ${escapeText(when)}
    ${escapeText(title)}
    v${list.length - idx} topic: ${escapeText((v?.data?.topic || '—').toString())} draft: ${escapeText((v?.draftId || '—').toString())}
    `; }).join(''); } function initContact() { const form = document.getElementById('contactForm'); const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || 'Your Region'; $('#tz').textContent = tz; $('#client_tz').value = tz; x7p1c.draftId = uid(); $('#draft_id').value = x7p1c.draftId; $('#maxVersions').textContent = String(x7p1c.maxVersions); const saved = readDraft(); if (saved && typeof saved === 'object' && Object.keys(saved).length) { if (saved.draft_id) { x7p1c.draftId = String(saved.draft_id); $('#draft_id').value = x7p1c.draftId; } restoreToForm(form, saved); setDraftIndicator(true, saved._meta?.updatedAt ? `Draft • ${fmtTs(saved._meta.updatedAt)}` : 'Draft restored'); } else { setDraftIndicator(false); } updateMsgCount(form); const history = readHistory(); renderHistoryList(history); function persistDraft(withHistoryMaybe=false) { setAutosaveState('saving…'); const data = snapshotFromForm(form); const payload = { ...data, _meta: { updatedAt: Date.now(), tz, ua: navigator.userAgent.slice(0, 180) } }; writeDraft(payload); setAutosaveState('saved'); setDraftIndicator(true, `Draft • ${fmtTs(payload._meta.updatedAt)}`); if (withHistoryMaybe) { const meaningful = (payload.name || payload.email || payload.message || payload.phone || '').toString().trim().length > 0; if (meaningful) { addHistoryVersion({ id: uid(), ts: Date.now(), draftId: x7p1c.draftId, data: { name: payload.name || '', email: payload.email || '', phone: payload.phone || '', topic: payload.topic || '', message: payload.message || '', consent: payload.consent || 'no' } }); } } } function scheduleSave() { x7p1c.lastInputAt = Date.now(); setAutosaveState('typing…'); clearTimeout(x7p1c.debounceTimer); x7p1c.debounceTimer = setTimeout(() => { persistDraft(false); }, x7p1c.saveDebounce); } x7p1c.historyTimer = setInterval(() => { const since = Date.now() - x7p1c.lastInputAt; if (since < x7p1c.historyIntervalMs) return; const current = snapshotFromForm(form); const meaningful = (current.name || current.email || current.message || current.phone || '').toString().trim().length > 0; if (!meaningful) return; const hist = readHistory(); const latest = hist[0]?.data?.message || ''; const currMsg = (current.message || '').toString(); if (currMsg.trim() && currMsg.trim() !== String(latest).trim()) { persistDraft(true); } }, 2500); form.addEventListener('input', (e) => { if (e.target && e.target.name === 'message') updateMsgCount(form); scheduleSave(); }); form.addEventListener('change', () => { scheduleSave(); }); form.addEventListener('reset', () => { setTimeout(() => { updateMsgCount(form); ['name','email','phone','message'].forEach(n => setInlineError(n,'')); setAutosaveState('idle'); clearDraft(); setDraftIndicator(false); }, 0); }); ['name','email','phone','message'].forEach((n) => { const field = form[n]; if (!field) return; field.addEventListener('blur', () => { const errs = validateAll(form); if (!errs.length) setAutosaveState('saved'); }); }); function openPreview() { const data = snapshotFromForm(form); $('#pvName').textContent = data.name?.trim() || '—'; $('#pvEmail').textContent = data.email?.trim() || '—'; $('#pvPhone').textContent = data.phone?.trim() || '—'; $('#pvTopic').textContent = data.topic?.trim() || '—'; $('#pvMessage').textContent = data.message?.trim() || '—'; $('#copyHint').textContent = ''; document.getElementById('preview-modal').showModal(); } $('#previewBtn').addEventListener('click', () => { const errs = validateAll(form); if (errs.length) { renderErrorModal(errs); return; } openPreview(); }); $('#copyPreviewBtn').addEventListener('click', async () => { const data = snapshotFromForm(form); const block = [ `Name: ${data.name || ''}`, `Email: ${data.email || ''}`, `Phone: ${data.phone || ''}`, `Topic: ${data.topic || ''}`, `Timezone: ${data.client_tz || ''}`, '', (data.message || '') ].join('\n'); try { await navigator.clipboard.writeText(block); $('#copyHint').textContent = 'Copied to clipboard.'; } catch { $('#copyHint').textContent = 'Clipboard not available. Select and copy manually.'; } }); $('#sendFromPreviewBtn').addEventListener('click', () => { document.getElementById('preview-modal').close(); form.requestSubmit(); }); $('#clearDraftBtn').addEventListener('click', () => { clearDraft(); setDraftIndicator(false); setAutosaveState('idle'); }); $('#openHistoryBtn').addEventListener('click', () => { renderHistoryList(readHistory()); document.getElementById('history-modal').showModal(); }); $('#historyList').addEventListener('click', (e) => { const rid = e.target.closest('[data-restore-id]')?.getAttribute('data-restore-id'); const did = e.target.closest('[data-delete-id]')?.getAttribute('data-delete-id'); if (rid) { const arr = readHistory(); const v = arr.find(x => x.id === rid); if (v?.data) { restoreToForm(form, v.data); persistDraft(false); document.getElementById('history-modal').close(); } } if (did) { const arr = readHistory().filter(x => x.id !== did); writeHistory(arr); renderHistoryList(arr); } }); $('#exportHistoryBtn').addEventListener('click', async () => { const arr = readHistory(); const payload = JSON.stringify({exportedAt: nowIso(), versions: arr}, null, 2); try { await navigator.clipboard.writeText(payload); $('#exportHistoryBtn').textContent = 'Exported'; setTimeout(() => $('#exportHistoryBtn').textContent = 'Export', 900); } catch { const blob = new Blob([payload], {type:'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'soapcraft-contact-history.json'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } }); $('#wipeHistoryBtn').addEventListener('click', () => { document.getElementById('confirm-wipe-modal').showModal(); }); $('#confirmWipeBtn').addEventListener('click', () => { localStorage.removeItem(x7p1c.historyKey); renderHistoryList([]); document.getElementById('confirm-wipe-modal').close(); }); form.addEventListener('submit', (e) => { e.preventDefault(); const errs = validateAll(form); if (errs.length) { renderErrorModal(errs); return; } persistDraft(true); clearDraft(); setDraftIndicator(false); setAutosaveState('idle'); document.getElementById('success-modal').showModal(); form.reset(); }); window.addEventListener('beforeunload', () => { try { const data = snapshotFromForm(form); const meaningful = (data.name || data.email || data.message || data.phone || '').toString().trim().length > 0; if (meaningful) writeDraft({...data, _meta: {updatedAt: Date.now(), tz}}); } catch {} }); } injectPartials(); document.addEventListener('DOMContentLoaded', initContact);