Files
boc/quixzoom-landing-fixed/zoomer-earnings.html
T

496 lines
22 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<link rel="canonical" href="https://www.quixzoom.com/zoomer-earnings.html">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>Earnings — quiXzoom</title>
<meta name="description" content="Learn how much you can earn as a quiXzoom Zoomer. Transparent payout structure, instant payments, and bonus opportunities.">
<link rel="stylesheet" >
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--font-text);
background: var(--grouped-background-primary);
color: var(--label-primary);
min-height: 100dvh;
padding-top: env(safe-area-inset-top, 44px);
padding-bottom: calc(env(safe-area-inset-bottom, 34px) + 83px);
-webkit-font-smoothing: antialiased;
}
/* Nav */
.nav-bar {
position: sticky; top: 0; z-index: 200;
display: flex; align-items: center; justify-content: center;
height: 44px; padding: 0 16px;
background: var(--navbar-bg);
backdrop-filter: saturate(180%) blur(20px);
-webkit-backdrop-filter: saturate(180%) blur(20px);
border-bottom: 0.5px solid var(--separator);
}
.nav-title { font-size: 17px; font-weight: 600; letter-spacing: -0.41px; }
/* Hero earnings card */
.earnings-hero {
background: linear-gradient(160deg, #1a3a5c 0%, #0d2137 50%, #0a1929 100%);
padding: 28px 24px 32px;
text-align: center;
position: relative; overflow: hidden;
}
.earnings-hero::before {
content: '';
position: absolute; top: -40%; left: -20%; right: -20%; bottom: -20%;
background: radial-gradient(circle at 50% 40%, rgba(0,122,255,0.25) 0%, transparent 70%);
pointer-events: none;
}
.earnings-label {
font-size: 13px; font-weight: 500; color: rgba(255,255,255,0.75);
text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px;
}
.earnings-amount {
font-family: var(--font-display);
font-size: 52px; font-weight: 800;
color: #fff; letter-spacing: -2px;
line-height: 1;
}
.earnings-currency { font-size: 24px; font-weight: 500; letter-spacing: -0.5px; color: rgba(60,60,67,0.7); }
.earnings-sub {
font-size: 14px; color: rgba(60,60,67,0.5);
margin-top: 8px;
}
/* Stats row */
.stats-row {
display: grid; grid-template-columns: 1fr 1fr 1fr;
background: var(--background-secondary);
border-bottom: 0.5px solid var(--separator);
}
.stat-cell {
padding: 16px 8px; text-align: center;
border-right: 0.5px solid var(--separator);
}
.stat-cell:last-child { border-right: none; }
.stat-val { font-size: 22px; font-weight: 700; font-family: var(--font-display); }
.stat-label { font-size: 11px; color: var(--label-secondary); margin-top: 2px; }
.green { color: var(--ios-green); }
.blue { color: var(--ios-blue); }
.orange { color: var(--ios-orange); }
/* Section */
.section { margin: 20px 16px 0; }
.section-header {
font-size: 20px; font-weight: 700; letter-spacing: -0.5px;
margin-bottom: 10px;
}
.section-card {
background: var(--background-secondary);
border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
}
/* Payout row */
.payout-row {
display: flex; align-items: center; gap: 12px;
padding: 13px 16px;
border-bottom: 0.5px solid var(--separator);
}
.payout-row:last-child { border-bottom: none; }
.payout-icon {
width: 38px; height: 38px; border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 18px; flex-shrink: 0;
}
.payout-body { flex: 1; min-width: 0; }
.payout-title { font-size: 15px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.payout-date { font-size: 12px; color: var(--label-secondary); margin-top: 2px; }
.payout-amount { font-size: 17px; font-weight: 700; font-family: var(--font-display); }
/* Connect card */
.connect-card {
background: var(--background-secondary);
border-radius: var(--radius-lg);
border: 0.5px solid var(--separator);
overflow: hidden;
}
.connect-top {
padding: 16px;
display: flex; gap: 14px; align-items: center;
}
.connect-icon {
width: 48px; height: 48px; border-radius: var(--radius-md);
background: rgba(0,122,255,0.15);
display: flex; align-items: center; justify-content: center;
font-size: 24px; flex-shrink: 0;
}
.connect-info { flex: 1; }
.connect-title { font-size: 17px; font-weight: 600; margin-bottom: 3px; }
.connect-desc { font-size: 13px; color: var(--label-secondary); }
.connect-btn {
display: block; width: 100%;
background: var(--ios-blue); color: #fff;
border: none; border-radius: 0; padding: 15px;
font-family: var(--font-text); font-size: 16px; font-weight: 700;
cursor: pointer; transition: opacity 0.15s;
letter-spacing: -0.2px;
border-top: 0.5px solid var(--separator);
}
.connect-btn:active { opacity: 0.8; }
.connect-status {
padding: 10px 16px;
font-size: 13px; color: var(--label-secondary);
border-top: 0.5px solid var(--separator);
display: flex; align-items: center; gap: 6px;
}
.status-dot {
width: 8px; height: 8px; border-radius: var(--radius-full);
}
.status-dot.active { background: var(--ios-green); }
.status-dot.pending { background: var(--ios-orange); }
.status-dot.inactive { background: var(--ios-gray3); }
/* Payout schedule */
.schedule-row {
display: flex; align-items: center; justify-content: space-between;
padding: 13px 16px;
border-bottom: 0.5px solid var(--separator);
}
.schedule-row:last-child { border-bottom: none; }
.schedule-label { font-size: 15px; }
.schedule-value { font-size: 15px; font-weight: 600; color: var(--label-secondary); }
/* Loader */
.loader { display:flex; justify-content:center; padding:40px; }
@keyframes spin { to { transform: rotate(360deg); } }
.spinner { width:28px; height:28px; border-radius: var(--radius-full); border:3px solid var(--fill-secondary); border-top-color:var(--ios-blue); animation:spin 0.8s linear infinite; }
/* Tab Bar */
.tab-bar {
position: fixed; bottom: 0; left: 0; right: 0; z-index: 300;
height: calc(49px + env(safe-area-inset-bottom, 34px));
background: var(--tabbar-bg);
backdrop-filter: saturate(180%) blur(20px);
-webkit-backdrop-filter: saturate(180%) blur(20px);
border-top: 0.5px solid var(--separator);
display: flex; align-items: flex-start; padding-top: 8px;
}
.tab-item {
flex: 1; display: flex; flex-direction: column; align-items: center;
gap: 3px; cursor: pointer; text-decoration: none; padding: 0 4px;
}
.tab-icon { font-size: 24px; line-height: 1; }
.tab-label { font-size: 10px; font-weight: 500; color: var(--label-tertiary); }
.tab-item.active .tab-label { color: var(--ios-blue); }
/* Toast */
.toast { position:fixed; bottom:calc(env(safe-area-inset-bottom,34px)+100px); left:50%; transform:translateX(-50%) translateY(20px); background:var(--background-secondary); border:0.5px solid var(--separator); border-radius: var(--radius-xl); padding:10px 18px; font-size:14px; font-weight:500; box-shadow:0 8px 32px rgba(0,0,0,0.3); opacity:0; transition:all 0.3s; z-index:9999; white-space:nowrap; }
.toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
@media (max-width: 640px) {
.hero h1 { font-size: 2rem !important; }
.hero p { font-size: 1rem !important; }
.card { padding: 1.5rem !important; }
.grid { grid-template-columns: 1fr !important; }
.btn { padding: 0.75rem 1.5rem !important; font-size: 1rem !important; }
}
</style>
<style id="wcag-contrast-enforcement">
/* AAMOS GLOBAL CONTRAST ENFORCEMENT — WCAG AA (4.5:1 minimum) */
/* Regel: Ingen text under 70% opacity. Aldrig. */
body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
/* Fix common low-contrast patterns */
[style*="rgba(60,60,67,0.4)"] { color: rgba(60,60,67,0.65) !important; }
[style*="rgba(60,60,67,0.3)"] { color: rgba(60,60,67,0.65) !important; }
[style*="rgba(255,255,255,0.4)"] { color: rgba(255,255,255,0.72) !important; }
[style*="rgba(255,255,255,0.3)"] { color: rgba(255,255,255,0.72) !important; }
[style*="opacity: 0.4"] { opacity: 0.72 !important; }
[style*="opacity: 0.3"] { opacity: 0.72 !important; }
@media (max-width: 640px) {
.hero h1 { font-size: 2rem !important; }
.hero p { font-size: 1rem !important; }
.card { padding: 1.5rem !important; }
.grid { grid-template-columns: 1fr !important; }
.btn { padding: 0.75rem 1.5rem !important; font-size: 1rem !important; }
}
</style>
<script src="i18n.js"></script>
<meta property="og:type" content="website">
<meta property="og:title" content="Earnings — quiXzoom">
<meta property="og:description" content="quiXzoom — Earn per Mission. No Platform Fee.">
<meta property="og:url" content="https://www.quixzoom.com/zoomer-earnings.html">
<meta property="og:image" content="https://www.quixzoom.com/images/zoomer-earnings.jpg">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="quiXzoom Zoomer earnings and QX Credits payout dashboard">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="https://www.quixzoom.com/images/zoomer-earnings.jpg">
<meta name="twitter:title" content="Earnings — quiXzoom">
</head>
<body>
<h1 style="position:absolute;left:-9999px;">Zoomer Earnings - quiXzoom</h1>
<!-- Nav -->
<div class="nav-bar">
<span class="nav-title">Earnings</span>
</div>
<!-- Hero -->
<div class="earnings-hero">
<div class="earnings-label">Total earned</div>
<div class="earnings-amount" id="total-amount"><span class="earnings-currency">SEK </span></div>
<div class="earnings-sub" id="earnings-sub">Loading data...</div>
</div>
<!-- Stats -->
<div class="stats-row">
<div class="stat-cell">
<div class="stat-val green" id="stat-uppdrag"></div>
<div class="stat-label">Missions</div>
</div>
<div class="stat-cell">
<div class="stat-val blue" id="stat-snittersattning"></div>
<div class="stat-label">Avg/mission</div>
</div>
<div class="stat-cell">
<div class="stat-val orange" id="stat-pending"></div>
<div class="stat-label">Pending</div>
</div>
</div>
<!-- Payouts -->
<div class="section">
<div class="section-header">Recent payouts</div>
<div class="section-card" id="payouts-card">
<div class="loader"><div class="spinner"></div></div>
</div>
</div>
<!-- Stripe Connect -->
<div class="section" style="margin-top:20px">
<div class="section-header">Payout account</div>
<div class="connect-card">
<div class="connect-top">
<div class="connect-icon"></div>
<div class="connect-info">
<div class="connect-title">Stripe Connect</div>
<div class="connect-desc">Verify your account to receive payouts directly to your bank</div>
</div>
</div>
<div class="connect-status" id="connect-status">
<div class="status-dot pending"></div>
Loading account status...
</div>
<button class="connect-btn" id="connect-btn" onclick="openConnectOnboarding()">
Configure payout account →
</button>
</div>
</div>
<!-- Payout schedule -->
<div class="section" style="margin-top:20px;margin-bottom:20px">
<div class="section-header">Payout schedule</div>
<div class="section-card">
<div class="schedule-row">
<span class="schedule-label"> Frekvens</span>
<span class="schedule-value">Every Monday</span>
</div>
<div class="schedule-row">
<span class="schedule-label">⏱ Processing time</span>
<span class="schedule-value">13 bankdagar</span>
</div>
<div class="schedule-row">
<span class="schedule-label"> Method</span>
<span class="schedule-value">Stripe Connect</span>
</div>
<div class="schedule-row">
<span class="schedule-label"> Currency</span>
<span class="schedule-value">SEK</span>
</div>
<div class="schedule-row">
<span class="schedule-label"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H10M6 8H10M6 11H8" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> Min. payout</span>
<span class="schedule-value">500 kr</span>
</div>
</div>
</div>
<!-- Toast -->
<div class="toast" id="toast"></div>
<!-- Tab Bar -->
<nav class="tab-bar">
<a class="tab-item" href="zoomer-app.html">
<span class="tab-icon"></span>
<span class="tab-label">Missions</span>
</a>
<a class="tab-item active" href="zoomer-earnings.html">
<span class="tab-icon"></span>
<span class="tab-label">Earnings</span>
</a>
<a class="tab-item" href="zoomer-profile.html">
<span class="tab-icon"></span>
<span class="tab-label">Profile</span>
</a>
</nav>
<script>
const API = 'https://api.quixzoom.com/api/qz/missions';
function getZoomerId() {
let id = localStorage.getItem('qz_zoomer_id');
if (!id) { id = 'zoomer-' + Math.random().toString(36).slice(2,10); localStorage.setItem('qz_zoomer_id', id); }
return id;
}
function toast(msg, dur=3000) {
const el = document.getElementById('toast');
el.textContent = msg;
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), dur);
}
function formatSEK(n) { return new Intl.NumberFormat('sv-SE').format(n); }
function formatDate(s) {
return new Date(s).toLocaleDateString('sv-SE', { day:'numeric', month:'short', year:'numeric' });
}
function missionTypeEmoji(type) {
const m = { facade_scan:'<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H7M6 8H7M6 11H7M9 5H10M9 8H10M9 11H10" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg>', roof_scan:'', road_inspection:'', interior_scan:'', vegetation_survey:'', inspection:'<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="7" cy="7" r="5" stroke="#666" stroke-width="2"/><path d="M11 11L14 14" stroke="#666" stroke-width="2" stroke-linecap="round"/></svg>' };
return m[type] || '';
}
function missionTypeBg(type) {
const m = { facade_scan:'rgba(0,122,255,0.15)', roof_scan:'rgba(255,149,0,0.15)', road_inspection:'rgba(142,142,147,0.15)', interior_scan:'rgba(52,199,89,0.15)', vegetation_survey:'rgba(48,209,88,0.15)' };
return m[type] || 'rgba(175,82,222,0.15)';
}
async function loadEarnings() {
const zoomerId = getZoomerId();
try {
const r = await fetch(`${API}/missions`);
const data = await r.json();
const all = data.missions || [];
// My missions with payment
const mine = all.filter(m => m.zoomer_id === zoomerId);
const approved = mine.filter(m => m.status === 'approved');
const pending = mine.filter(m => ['assigned','submitted'].includes(m.status));
const totalEarned = approved.reduce((s,m) => s + m.reward_sek, 0);
const totalPending = pending.reduce((s,m) => s + m.reward_sek, 0);
const avg = approved.length > 0 ? Math.round(totalEarned / approved.length) : 0;
// Update hero
document.getElementById('total-amount').innerHTML = `<span class="earnings-currency">SEK </span>${formatSEK(totalEarned)}`;
document.getElementById('earnings-sub').textContent = `${approved.length} approved missions · ${formatSEK(totalPending)} SEK pending`;
// Stats
document.getElementById('stat-uppdrag').textContent = approved.length;
document.getElementById('stat-snittersattning').textContent = avg > 0 ? formatSEK(avg) + ' kr' : '—';
document.getElementById('stat-pending').textContent = totalPending > 0 ? formatSEK(totalPending) + ' kr' : '—';
// Payouts list
const payoutsCard = document.getElementById('payouts-card');
const recent = [...approved].sort((a,b) => new Date(b.approved_at) - new Date(a.approved_at)).slice(0, 5);
if (recent.length === 0) {
payoutsCard.innerHTML = `
<div style="padding:32px;text-align:center">
<div style="font-size:40px;margin-bottom:8px"></div>
<div style="font-size:16px;font-weight:600;margin-bottom:4px">Inga utbetalningar än</div>
<div style="font-size:14px;color:var(--label-secondary)">Slutför ditt första uppdrag för att börja tjäna</div>
</div>`;
} else {
payoutsCard.innerHTML = recent.map(m => `
<div class="payout-row">
<div class="payout-icon" style="background:${missionTypeBg(m.mission_type)}">${missionTypeEmoji(m.mission_type)}</div>
<div class="payout-body">
<div class="payout-title">${m.title}</div>
<div class="payout-date">${formatDate(m.approved_at)} · ${m.payment_ref || '—'}</div>
</div>
<div class="payout-amount" style="color:var(--ios-green)">+${formatSEK(m.reward_sek)} kr</div>
</div>`).join('');
}
// Add pending items with different styling
if (pending.length > 0) {
const pendingHTML = pending.slice(0,3).map(m => `
<div class="payout-row" style="opacity:0.6">
<div class="payout-icon" style="background:rgba(255,149,0,0.15)">⏳</div>
<div class="payout-body">
<div class="payout-title">${m.title}</div>
<div class="payout-date">Väntande · ${m.status === 'submitted' ? 'Under granskning' : 'Pågående'}</div>
</div>
<div class="payout-amount" style="color:var(--ios-orange)">~${formatSEK(m.reward_sek)} kr</div>
</div>`).join('');
payoutsCard.innerHTML += pendingHTML;
}
} catch(e) {
console.error(e);
document.getElementById('payouts-card').innerHTML = `
<div style="padding:24px;text-align:center;color:var(--label-secondary)">
<div><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Could not load earnings data</div>
</div>`;
}
}
async function loadConnectStatus() {
const statusEl = document.getElementById('connect-status');
const btnEl = document.getElementById('connect-btn');
// Try to get onboarding link to check if connect is available
try {
const r = await fetch(`${API}/payments/connect/onboarding-link`);
if (r.ok) {
const data = await r.json();
statusEl.innerHTML = `<div class="status-dot active"></div> Account activated — ready for payouts`;
btnEl.textContent = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="3" stroke="#666" stroke-width="1.5"/><path d="M8 2V4M8 12V14M2 8H4M12 8H14M4.3 4.3L5.7 5.7M10.3 10.3L11.7 11.7M4.3 11.7L5.7 10.3M10.3 5.7L11.7 4.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> Hantera konto →';
if (data.url) { btnEl.dataset.url = data.url; }
} else if (r.status === 401) {
statusEl.innerHTML = `<div class="status-dot pending"></div> Inloggning krävs för att verifiera konto`;
} else {
statusEl.innerHTML = `<div class="status-dot inactive"></div> Konto ej konfigurerat`;
}
} catch {
statusEl.innerHTML = `<div class="status-dot inactive"></div> Konto ej konfigurerat — klicka för att börja`;
}
}
async function openConnectOnboarding() {
const btn = document.getElementById('connect-btn');
btn.textContent = '⏳ Öppnar...';
btn.disabled = true;
try {
const r = await fetch(`${API}/payments/connect/onboarding-link`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ zoomer_id: getZoomerId() })
});
if (r.ok) {
const data = await r.json();
if (data.url) { window.open(data.url, '_blank'); }
else { toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Konto verifierat!'); }
} else if (r.status === 401) {
toast('<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="7" width="10" height="7" rx="1" stroke="#666" stroke-width="1.5"/><path d="M5 7V5C5 3.3 6.3 2 8 2C9.7 2 11 3.3 11 5V7" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="10.5" r="1" fill="#666"/></svg> Behörighet saknas — kontakta support');
} else {
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Stripe Connect onboarding — kontakta support@quixzoom.com');
}
} catch {
toast(' Contact support to configure your payout account');
} finally {
btn.textContent = ' Configure payout account →';
btn.disabled = false;
}
}
// Init
loadEarnings();
loadConnectStatus();
</script>
</body>
</html>