feat(boc): Complete Business Operations Center v1.0

- Go backend API with full CRUD for all modules (CRM, Sales, Finance, HR, Legal, Marketing, Support, Purchase, Inventory, Projects, Automation, Analytics)
- Rust analytics service with parallel report generation
- C runtime with POSIX shared memory IPC
- PostgreSQL schema with 30+ tables, full migrations
- Redis cache, sessions, pub/sub
- Kafka event streaming with Zookeeper
- WebSocket hub for real-time updates
- Automation engine with cron jobs, workflows, event triggers
- JWT authentication, multi-tenant from start
- Docker Compose with all services
- Nginx reverse proxy with rate limiting
- Integration tests passing
- Feature gap analysis against Fortnox/Odoo/Visma

Refs: BOC-001
This commit is contained in:
Bernt
2026-07-12 12:41:35 +00:00
parent 4789a7fb48
commit 58ca4e68db
26666 changed files with 575891 additions and 2074516 deletions
+105 -80
View File
@@ -19,12 +19,12 @@
<meta name="color-scheme" content="light">
<title>quiXzoom — Earn Money Taking Photos | Flexible Work</title>
<title>quiXzoom — Order Field Photography | Raw Geotagged Data, Delivered in Hours</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="192x192" href="/apple-touch-icon.png">
<meta name="description" content="quiXzoom rewards you per approved mission with QX Credits — redeemable as cash. No platform fee. No commission. Launching August 2026.">
<meta name="description" content="Get raw geotagged photos from verified field contributors. No analysis — just verified visual data delivered in hours. Per mission, per photo, or per project.">
<link rel="canonical" href="https://www.quixzoom.com/">
<!-- hreflang — canonical paths -->
<link rel="alternate" hreflang="x-default" href="https://www.quixzoom.com/" />
@@ -35,8 +35,8 @@
<meta name="keywords" content="quiXzoom, earn money taking photos, crowdsourced field data, photo missions, zoomer platform, fältdatainsamling, tjäna pengar foton">
<meta property="og:type" content="website">
<meta property="og:title" content="quiXzoom — Capture the World. Get Paid.">
<meta property="og:description" content="Take photo missions near you. Get paid every Monday. No platform fee — you keep 100% of your earned mission pay. Launching August 1, 2026.">
<meta property="og:title" content="quiXzoom — Order Field Photography. Raw Data Delivered.">
<meta property="og:description" content="Get raw geotagged photos from verified field contributors. No analysis — just verified visual data delivered in hours.">
<meta property="og:url" content="https://www.quixzoom.com/">
<meta property="og:image" content="https://www.quixzoom.com/images/hero-zoomer-city.jpg">
<meta property="og:image:width" content="1200">
@@ -628,8 +628,8 @@ html,body{overflow-x:clip}
.market-selector-current{font-variant:all-small-caps}
.market-dropdown{position:absolute;top:calc(100% + 6px);right:0;background:#fff;border:1px solid rgba(0,0,0,.1);border-radius:12px;padding:8px;min-width:160px;z-index:400;display:none;flex-direction:column;gap:2px;box-shadow:0 16px 40px rgba(0,0,0,.15)}
.market-selector.open .market-dropdown{display:flex}
.market-dropdown button{background:none;border:none;color:rgba(10,10,10,.75);font-size:.8125rem;font-weight:500;padding:8px 12px;border-radius:8px;text-align:left;cursor:pointer;transition:.12s}
.market-dropdown button:hover{background:rgba(10,10,10,.08);color:var(--text-dark)}
.market-dropdown button, .market-dropdown a.market-option{background:none;border:none;color:rgba(10,10,10,.75);font-size:.8125rem;font-weight:500;padding:8px 12px;border-radius:8px;text-align:left;cursor:pointer;transition:.12s;text-decoration:none;display:block;width:100%}
.market-dropdown button:hover, .market-dropdown a.market-option:hover{background:rgba(10,10,10,.08);color:var(--text-dark)}
/* Earnings Calculator */
@@ -689,50 +689,33 @@ html,body{overflow-x:clip}
<div class="nav-inner">
<a href="/" class="nav-logo">qui<span class="x">X</span>zoom</a>
<ul class="nav-links">
<!-- Platform dropdown -->
<li>
<button class="nav-drop-toggle" onclick="toggleDrop(this)">
Platform <svg viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 1l4 4 4-4"/></svg>
</button>
<div class="nav-dropdown">
<a href="#how"><span class="nd-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/></svg></span><span class="nd-text"><span class="nd-title">How it works</span><span class="nd-sub">Mission flow end-to-end</span></span></a>
<a href="#missions"><span class="nd-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg></span><span class="nd-text"><span class="nd-title">Mission types</span><span class="nd-sub">Infrastructure, real estate, environment</span></span></a>
<a href="#earnings"><span class="nd-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg></span><span class="nd-text"><span class="nd-title">Zoomer earnings</span><span class="nd-sub">Compensation & tiers</span></span></a>
<a href="/markets/se/"><span class="nd-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15 15 0 0 1 0 20M12 2a15 15 0 0 0 0 20"/></svg></span><span class="nd-text"><span class="nd-title">Sverige</span><span class="nd-sub">Launching August 2026</span></span></a>
</div>
</li>
<!-- Solutions dropdown -->
<li>
<button class="nav-drop-toggle" onclick="toggleDrop(this)">
Solutions <svg viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 1l4 4 4-4"/></svg>
</button>
<div class="nav-dropdown">
<a href="/for-organisations/"><span class="nd-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9M15 21V9"/></svg></span><span class="nd-text"><span class="nd-title">For Organisations</span><span class="nd-sub">Municipalities, infrastructure, insurance</span></span></a>
<a href="/for-organisations/#use-cases"><span class="nd-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span><span class="nd-text"><span class="nd-title">Use cases</span><span class="nd-sub">Facade, road, coastal, property</span></span></a>
<a href="/for-organisations/#enterprise"><span class="nd-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg></span><span class="nd-text"><span class="nd-title">Enterprise</span><span class="nd-sub">Custom contracts & SLA</span></span></a>
</div>
</li>
<li><a href="/pricing">Pricing</a></li>
<li><a href="/order/">Order Field Data</a></li>
<li><a href="/pricing/">Pricing</a></li>
<li><a href="/earn/">Earn as Zoomer</a></li>
<li><a href="/for-organisations/">For Organisations</a></li>
<li><a href="/about">About</a></li>
</ul>
<div style="display:flex;align-items:center;gap:10px;flex-shrink:0">
<a href="/support" style="font-size:.875rem;color:var(--text-dark);text-decoration:none;font-weight:500">Support</a>
<button class="btn-nav" onclick="document.getElementById('wl-email').focus();document.getElementById('wl-email').scrollIntoView({behavior:'smooth'})">Join waitlist</button>
<a href="/order/" class="btn-nav" style="display:inline-flex;align-items:center;gap:6px;text-decoration:none">Order Field Data</a>
</div>
<div class="market-selector" id="market-selector">
<button class="market-selector-btn" onclick="document.getElementById('market-selector').classList.toggle('open')" aria-label="Select market" aria-haspopup="listbox">
<span class="market-selector-current">SE</span>
<button class="market-selector-btn" onclick="document.getElementById('market-selector').classList.toggle('open')" aria-label="Select language/region" aria-haspopup="listbox">
<span class="market-selector-current">EN</span>
<svg width="10" height="6" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 1l4 4 4-4"/></svg>
</button>
<div class="market-dropdown" role="listbox">
<button onclick="QX_setMarket('SE')" role="option">&#127480;&#127466; Sweden — Launching Aug 2026</button>
<button onclick="QX_setMarket('NL')" role="option">&#127475;&#127473; Netherlands — Launching Q1 2027</button>
<button onclick="QX_setMarket('TH')" role="option">&#127481;&#127469; Thailand — Launching Q3 2026</button>
<button onclick="QX_setMarket('NG')" role="option">&#127475;&#127468; Nigeria — Coming 2026</button>
<button onclick="QX_setMarket('KE')" role="option">&#127472;&#127466; Kenya — Coming 2026</button>
<a href="/" role="option" class="market-option">&#127482;&#127480; English (US)</a>
<a href="/sv-se/" role="option" class="market-option">&#127480;&#127466; Svenska (Sverige)</a>
<a href="/nl-nl/" role="option" class="market-option">&#127475;&#127473; Nederlands (Nederland)</a>
<a href="/de-de/" role="option" class="market-option">&#127465;&#127466; Deutsch (Deutschland)</a>
<a href="/fr-fr/" role="option" class="market-option">&#127467;&#127479; Français (France)</a>
<a href="/es-es/" role="option" class="market-option">&#127466;&#127480; Español (España)</a>
<a href="/it-it/" role="option" class="market-option">&#127470;&#127481; Italiano (Italia)</a>
<a href="/th/" role="option" class="market-option">&#127481;&#127469; ไทย (ประเทศไทย)</a>
<a href="/ja/" role="option" class="market-option">&#127471;&#127477; 日本語 (日本)</a>
<a href="/ko/" role="option" class="market-option">&#127472;&#127479; 한국어 (대한민국)</a>
<a href="/zh-cn/" role="option" class="market-option">&#127464;&#127475; 简体中文 (中国)</a>
</div>
</div>
<button class="hamburger" id="hamburger" aria-label="Menu" onclick="toggleMenu()">
@@ -742,13 +725,13 @@ html,body{overflow-x:clip}
</div>
</nav>
<div class="mobile-menu" id="mobile-menu">
<a href="#how" onclick="closeMenu()">How it works</a>
<a href="#missions" onclick="closeMenu()">Missions</a>
<a href="#earnings" onclick="closeMenu()">Earnings</a>
<a href="#recognition" onclick="closeMenu()">Recognition</a>
<a href="/order/" onclick="closeMenu()">Order Field Photography</a>
<a href="/pricing/" onclick="closeMenu()">Pricing</a>
<a href="/earn/" onclick="closeMenu()">Earn as a Zoomer</a>
<a href="/for-organisations/" onclick="closeMenu()">For Organisations</a>
<a href="/about" onclick="closeMenu()">About</a>
<a href="/support" onclick="closeMenu()">Support</a>
<a class="btn-mobile" onclick="closeMenu();document.getElementById('wl-email').focus();document.getElementById('wl-email').scrollIntoView({behavior:'smooth'})">Register as a Zoomer</a>
<a class="btn-mobile" href="/order/" onclick="closeMenu()">Order Field Data</a>
</div>
<main id="main-content">
@@ -760,30 +743,26 @@ html,body{overflow-x:clip}
<div class="hero-inner">
<div class="hero-badge">Global rollout · August 2026</div>
<div style="font-size:1rem;font-weight:700;letter-spacing:.08em;color:var(--blue);text-transform:uppercase;margin-bottom:12px;font-family:-apple-system,BlinkMacSystemFont,sans-serif">quiXzoom</div>
<h1 class="hero-h1">Capture the<br>world.<br><em>Earn per mission.</em></h1>
<h1 class="hero-h1">Order raw field<br>photography.<br><em>Delivered in hours.</em></h1>
<p class="hero-sub">Take photo missions near you. Get paid every Monday. No platform fee. Launching August 1, 2026.</p>
<form id="waitlist-form-top" onsubmit="submitWaitlist(event,'top')" novalidate>
<div class="wl-row">
<input type="tel" id="wl-phone" name="phone" class="wl-input" placeholder="Your phone number" required autocomplete="tel">
<button type="submit" id="wl-btn" class="wl-submit">Register as a Zoomer →</button>
</div>
<div id="wl-ok" class="wl-ok" hidden>✓ You're on the list! We'll be in touch before launch.</div>
<p id="wl-err" class="wl-err" hidden>Something went wrong. Please try again.</p>
</form>
<p class="hero-sub">Get raw geotagged photos from verified field contributors. No analysis — just verified visual data delivered in hours. Per mission, per photo, or per project.</p>
<div class="wl-row" style="margin-bottom:20px">
<a href="/order/" class="wl-submit" style="text-decoration:none;display:inline-flex;align-items:center;justify-content:center">Start Order →</a>
<a href="/pricing/" style="display:inline-flex;align-items:center;justify-content:center;background:transparent;color:var(--text-dark);border:2px solid rgba(0,0,0,.15);border-radius:var(--radius-lg);padding:18px 36px;font-size:1rem;font-weight:800;transition:border-color .15s,background .15s;text-decoration:none;white-space:nowrap">Get Quote</a>
</div>
<div class="audience-split">
<a href="#waitlist-form-top" class="audience-card audience-zoomer">
<div class="audience-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="10" r="3"/><path d="M3 21c0-3.3 4-6 9-6s9 2.7 9 6"/></svg></div>
<div><span class="audience-label">I want to earn</span><span class="audience-desc">Take photo missions near me.</span></div>
<a href="/order/" class="audience-card audience-org" style="order:-1">
<div class="audience-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg></div>
<div><span class="audience-label">Order Field Photography</span><span class="audience-desc">Get raw geotagged photos from verified contributors.</span></div>
<span class="audience-arrow"></span>
</a>
<a href="/for-organisations/" class="audience-card audience-org">
<div class="audience-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg></div>
<div><span class="audience-label">We need field data</span><span class="audience-desc">Commission documentation at scale.</span></div>
<a href="/earn/" class="audience-card audience-zoomer">
<div class="audience-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="10" r="3"/><path d="M3 21c0-3.3 4-6 9-6s9 2.7 9 6"/></svg></div>
<div><span class="audience-label">Earn as a Zoomer</span><span class="audience-desc">Free to join. No fees.</span></div>
<span class="audience-arrow"></span>
</a>
</div>
<p class="wl-trust">No signup fee · No platform fee · Paid every Monday · Identity verification required once</p>
<p class="wl-trust">Need analysis, not just photos? → <a href="https://www.landvex.com" style="color:var(--blue);font-weight:600">landvex.com</a></p>
<div class="trust-badges" style="display:flex;flex-wrap:wrap;gap:8px;margin-top:14px">
<span class="badge" style="background:rgba(10,10,10,.07);border:1px solid rgba(10,10,10,.13);border-radius:20px;padding:5px 12px;font-size:.72rem;font-weight:600;color:rgba(10,10,10,.7)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:4px"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> GDPR Compliant</span>
<span class="badge" style="background:rgba(10,10,10,.07);border:1px solid rgba(10,10,10,.13);border-radius:20px;padding:5px 12px;font-size:.72rem;font-weight:600;color:rgba(10,10,10,.7)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:4px"><rect x="1" y="4" width="22" height="16" rx="2"/><line x1="1" y1="10" x2="23" y2="10"/></svg> Stripe Connect Verified</span>
@@ -1232,7 +1211,7 @@ document.addEventListener('qx:market',function(){qxCalcUpdate();});
<p>quiXzoom Community launches alongside the platform. Zoomers will share field observations, local knowledge and discovery reports — building a continuously updated layer of real-world intelligence. <em style="color:var(--text-muted)">Opening 2026.</em></p>
</div>
<div class="community-grid">
<div class="community-card" style="cursor:pointer" onclick="document.getElementById('wl-email').focus()">
<div class="community-card" style="cursor:pointer" onclick="document.getElementById('wl-phone').focus()">
<div class="community-tag">Field Reports</div>
<h3>What's changing out there</h3>
<p>Real observations from the field. Zoomers report what they see — infrastructure, urban change, damage, new construction.</p>
@@ -1243,7 +1222,7 @@ document.addEventListener('qx:market',function(){qxCalcUpdate();});
<li><em style="color:var(--text-muted)">(Sample)</em> Bridge inspection underway in Stockholm</li>
</ul>
</div>
<div class="community-card" style="cursor:pointer" onclick="document.getElementById('wl-email').focus()">
<div class="community-card" style="cursor:pointer" onclick="document.getElementById('wl-phone').focus()">
<div class="community-tag">Zoomer Academy</div>
<h3>Sharpen your fieldwork</h3>
<p>Guides, techniques and standards for producing high-quality, high-approval submissions.</p>
@@ -1254,7 +1233,7 @@ document.addEventListener('qx:market',function(){qxCalcUpdate();});
<li>Field safety and access rights</li>
</ul>
</div>
<div class="community-card" style="cursor:pointer" onclick="document.getElementById('wl-email').focus()">
<div class="community-card" style="cursor:pointer" onclick="document.getElementById('wl-phone').focus()">
<div class="community-tag">Regional Communities</div>
<h3>Local knowledge, global reach</h3>
<p>Every region has its own community. Posts are automatically translated — a Zoomer in Italy and a Zoomer in Sweden read the same thread in their own language.</p>
@@ -1316,10 +1295,10 @@ document.addEventListener('qx:market',function(){qxCalcUpdate();});
<div style="max-width:680px;margin:0 auto">
<form id="waitlist-form-bottom" onsubmit="submitWaitlist(event,'bottom')" novalidate>
<div class="wl-row">
<input type="tel" id="wl-phone-bottom" name="phone" class="wl-input" placeholder="Your phone number" required autocomplete="tel">
<input type="tel" id="wl-phone-bottom" name="phone" class="wl-input" placeholder="+46 70 123 45 67" required autocomplete="tel">
<button type="submit" id="wl-btn-bottom" class="wl-submit">Register as a Zoomer →</button>
</div>
<div id="wl-ok-bottom" class="wl-ok" hidden>✓ You're on the list! We'll be in touch before launch.</div>
<div id="wl-ok-bottom" class="wl-ok" hidden>✓ You're on the list! We'll be in touch before launch. (Notification sent to hello@quixzoom.com)</div>
<p id="wl-err-bottom" class="wl-err" hidden>Something went wrong. Please try again.</p>
</form>
<p class="wl-trust">No signup fee · No platform fee · Credits never expire · Launching Sweden Aug 2026</p>
@@ -1368,6 +1347,7 @@ document.addEventListener('qx:market',function(){qxCalcUpdate();});
<li><a href="/markets/se/">Sverige launch <span class="badge-new">Aug 2026</span></a></li>
<li><a href="/markets/">All markets</a></li>
<li><a href="https://app.quixzoom.com/join">Join as Zoomer</a></li>
<li><a href="https://shop.quixzoom.com">Shop <span class="badge-new">🛒</span></a></li>
</ul>
</div>
@@ -1438,10 +1418,10 @@ document.addEventListener('qx:market',function(){qxCalcUpdate();});
</div>
<div style="border-top:1px solid rgba(10,10,10,.07);padding:20px 32px;max-width:1200px;margin:0 auto">
<p style="font-size:.8rem;color:rgba(10,10,10,.55);line-height:1.6">Data delivered to <a href="https://www.landvex.com" style="color:rgba(10,10,10,.7);text-decoration:underline">Landvex</a> — powering decision intelligence for governments and enterprise. · Credits are redeemable as cash — 1 Credit = €1 · Minimum payout 250 Credits (€250).</p>
<p style="font-size:.8rem;color:rgba(10,10,10,.55);line-height:1.6"><strong style="color:rgba(10,10,10,.75)">quiXzoom delivers raw field data. Landvex delivers decision intelligence.</strong> Need analysis, not just photos? → <a href="https://www.landvex.com" style="color:var(--blue);font-weight:600;text-decoration:none">landvex.com</a></p>
</div>
<div class="footer-bottom">
<p class="footer-copy">© 2026 quiXzoom · Operated by Landvex AB</p>
<p class="footer-copy">© 2026 quiXzoom · Operated by Landvex AB · <em>Raw data by quiXzoom. Intelligence by Landvex.</em></p>
<div class="footer-legal-links">
<a href="/privacy/">Privacy</a>
<a href="/terms/">Terms</a>
@@ -1787,27 +1767,70 @@ document.querySelectorAll('[role="button"]').forEach(el =>
el.addEventListener('keydown', e => { if(e.key==='Enter'||e.key===' '){e.preventDefault();el.click();}})
);
/* Phone number formatting */
function formatPhoneInput(input) {
let v = input.value.replace(/[^\d+\s]/g, '');
if (v && !v.startsWith('+')) {
// Auto-add + if number starts with country code pattern
if (/^(46|31|66|234|254)/.test(v)) v = '+' + v;
}
input.value = v;
}
/* Attach formatters */
document.querySelectorAll('input[type=tel]').forEach(inp => {
inp.addEventListener('input', () => formatPhoneInput(inp));
});
async function submitWaitlist(e, variant) {
e.preventDefault();
const sfx = variant==='bottom' ? '-bottom' : '';
const btn = document.getElementById('wl-btn'+sfx);
const inp = document.getElementById('wl-email'+sfx);
const inp = document.getElementById('wl-phone'+sfx);
const ok = document.getElementById('wl-ok'+sfx);
const err = document.getElementById('wl-err'+sfx);
if (!inp.value.trim() || !inp.value.includes('@')) { inp.focus(); return; }
btn.disabled = true; btn.textContent = '…';
const phone = inp.value.trim().replace(/[^\d+]/g, '');
if (!phone || phone.length < 8) {
inp.focus();
inp.style.borderColor = '#FF4444';
err.textContent = 'Please enter a valid phone number (min 8 digits)';
err.hidden = false;
return;
}
inp.style.borderColor = '';
err.hidden = true;
btn.disabled = true; btn.textContent = '…';
// Send email notification to hello@quixzoom.com
const registrationData = {
phone: phone,
country: (navigator.language||'en').split('-')[1]||'',
lang: (navigator.language||'en').split('-')[0],
source: location.hostname
};
// Send notification to hello@quixzoom.com
try {
fetch('https://shop.quixzoom.com/notify-registration.php', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify(registrationData)
}).catch(() => {}); // Silent fail - don't block registration
} catch(e) {
console.log('Notification failed', e);
}
try {
const r = await fetch('https://api.quixzoom.com/api/qz/waitlist', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ email: inp.value.trim(), country: (navigator.language||'en').split('-')[1]||'', lang: (navigator.language||'en').split('-')[0], source: location.hostname })
body: JSON.stringify(registrationData)
});
if (!r.ok) throw new Error();
inp.closest('.wl-row').style.display = 'none';
ok.hidden = false;
updateCounter();
} catch {
btn.disabled = false; btn.textContent = 'Register interest →';
btn.disabled = false; btn.textContent = 'Register as a Zoomer →';
err.innerHTML = 'Server error. Please <a href="mailto:hello@quixzoom.com?subject=Zoomer Registration: ' + encodeURIComponent(phone) + '&body=Phone: ' + encodeURIComponent(phone) + '" style="color:#0066FF;text-decoration:underline;">email us your phone number</a> or try again.';
err.hidden = false;
}
}
@@ -1969,15 +1992,17 @@ document.addEventListener('qx:market', function(e){
(function(){
if(document.cookie.indexOf('qx_market=')>=0)return;
if(sessionStorage.getItem('qx_geo_dismissed'))return;
var hints={'SE':'SE','NL':'NL','TH':'TH','NG':'NG','KE':'KE'};
var tz=Intl.DateTimeFormat().resolvedOptions().timeZone||'';
var map={'Europe/Stockholm':'SE','Europe/Amsterdam':'NL','Asia/Bangkok':'TH','Africa/Lagos':'NG','Africa/Nairobi':'KE'};
var names={'SE':'Sweden','NL':'Netherlands','TH':'Thailand','NG':'Nigeria','KE':'Kenya'};
var map={
'Europe/Stockholm':'sv-se','Europe/Amsterdam':'nl-nl','Europe/Berlin':'de-de','Europe/Paris':'fr-fr','Europe/Rome':'it-it','Europe/Madrid':'es-es',
'Asia/Bangkok':'th','Asia/Tokyo':'ja','Asia/Seoul':'ko','Asia/Shanghai':'zh-cn','Asia/Jakarta':'id','Asia/Kuala_Lumpur':'ms','Asia/Ho_Chi_Minh':'vi'
};
var names={'sv-se':'Sweden','nl-nl':'Netherlands','de-de':'Germany','fr-fr':'France','it-it':'Italy','es-es':'Spain','th':'Thailand','ja':'Japan','ko':'Korea','zh-cn':'China','id':'Indonesia','ms':'Malaysia','vi':'Vietnam'};
var detected=map[tz];
if(!detected||detected==='SE')return;
if(!detected||detected==='sv-se')return;
var b=document.createElement('div');
b.id='qx-geo-banner';
b.innerHTML='<span>Looks like you&#39;re in '+names[detected]+'</span><button class="geo-switch" onclick="QX_setMarket(''+detected+'');this.closest('#qx-geo-banner').remove()">Switch to '+names[detected]+'</button><button class="geo-dismiss" onclick="sessionStorage.setItem('qx_geo_dismissed',1);this.closest('#qx-geo-banner').remove()" aria-label="Dismiss">×</button>';
b.innerHTML='<span>Looks like you&#39;re in '+names[detected]+'</span><a href="/'+detected+'/" class="geo-switch">Switch to '+names[detected]+'</a><button class="geo-dismiss" onclick="sessionStorage.setItem(\'qx_geo_dismissed\',1);this.closest(\'#qx-geo-banner\').remove()" aria-label="Dismiss">×</button>';
document.addEventListener('DOMContentLoaded',function(){document.body.appendChild(b);});
}());
</script>