1a12fb870b
- New /developers/ page with API docs, SDKs, pricing, use cases - OpenAPI 3.0 spec for Orders, Missions, Photos, Analytics - Case study: Glasskiosken i Flatenbadet — complete ROI analysis - Updated /order/ with recurring missions and frequency dropdown
1383 lines
63 KiB
HTML
1383 lines
63 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="sv" data-theme="dark">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<link rel="canonical" href="https://www.quixzoom.com/zoomer-app.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">
|
||
<meta name="theme-color" content="#000000">
|
||
<title>quiXzoom — Zoomer App</title>
|
||
<meta name="description" content="The quiXzoom Zoomer App — find missions, capture photos, and earn money. Your gateway to flexible field work.">
|
||
<link rel="stylesheet" >
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||
<style>
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
body {
|
||
font-family: var(--font-text);
|
||
background: var(--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;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
/* Tab system */
|
||
.tab-page { display: none; }
|
||
.tab-page.active { display: block; }
|
||
|
||
/* Nav Bar */
|
||
.nav-bar {
|
||
position: sticky; top: 0; z-index: 200;
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
height: 44px; padding: 0 16px;
|
||
background: var(--navbar-bg, rgba(28,28,30,0.94));
|
||
backdrop-filter: saturate(180%) blur(20px);
|
||
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
||
border-bottom: 0.5px solid var(--separator);
|
||
}
|
||
.nav-title {
|
||
position: absolute; left: 50%; transform: translateX(-50%);
|
||
font-family: var(--font-display); font-size: 17px; font-weight: 600;
|
||
letter-spacing: -0.41px;
|
||
}
|
||
.nav-logo {
|
||
background: linear-gradient(135deg, #0A84FF 0%, #5E5CE6 100%);
|
||
-webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
|
||
font-size: 18px; font-weight: 800;
|
||
}
|
||
.nav-btn {
|
||
background: none; border: none; color: var(--ios-blue);
|
||
font-family: var(--font-text); font-size: 17px; cursor: pointer; padding: 4px 0;
|
||
}
|
||
.nav-btn-sm {
|
||
background: none; border: none; cursor: pointer;
|
||
font-size: 20px; padding: 4px;
|
||
}
|
||
|
||
/* Tab Bar */
|
||
.tab-bar {
|
||
position: fixed; bottom: 0; left: 0; right: 0; z-index: 300;
|
||
display: flex;
|
||
background: rgba(28,28,30,0.94);
|
||
backdrop-filter: saturate(180%) blur(30px);
|
||
-webkit-backdrop-filter: saturate(180%) blur(30px);
|
||
border-top: 0.5px solid var(--separator);
|
||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||
}
|
||
.tab-item {
|
||
flex: 1; display: flex; flex-direction: column; align-items: center;
|
||
justify-content: center; padding: 8px 4px 4px;
|
||
cursor: pointer; text-decoration: none; border: none;
|
||
background: none; transition: opacity 0.15s;
|
||
}
|
||
.tab-item:active { opacity: 0.6; }
|
||
.tab-icon { font-size: 24px; line-height: 1; }
|
||
.tab-label { font-size: 10px; font-weight: 500; color: var(--label-tertiary); margin-top: 2px; letter-spacing: -0.1px; }
|
||
.tab-item.active .tab-label { color: #0A84FF; }
|
||
.tab-badge-wrap { position: relative; display: inline-block; }
|
||
.tab-dot {
|
||
position: absolute; top: -2px; right: -4px;
|
||
width: 8px; height: 8px; border-radius: var(--radius-full);
|
||
background: var(--ios-red); border: 1.5px solid var(--background-primary);
|
||
display: none;
|
||
}
|
||
.tab-dot.show { display: block; }
|
||
|
||
/* Segment */
|
||
.segment-wrap { padding: 8px 16px; }
|
||
.segment { display: flex; background: var(--fill-tertiary); border-radius: var(--radius-md); padding: 2px; }
|
||
.seg-btn {
|
||
flex: 1; padding: 6px 0; background: none; border: none;
|
||
font-family: var(--font-text); font-size: 13px; font-weight: 500;
|
||
color: var(--label-secondary); cursor: pointer; border-radius: var(--radius-sm); transition: all 0.2s;
|
||
}
|
||
.seg-btn.active {
|
||
background: var(--background-secondary); color: var(--label-primary); font-weight: 600;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
/* Section header */
|
||
.section-hdr {
|
||
padding: 20px 16px 8px;
|
||
font-size: 13px; font-weight: 500;
|
||
color: var(--label-secondary); text-transform: uppercase; letter-spacing: 0.5px;
|
||
}
|
||
|
||
/*
|
||
MAP TAB — Karta
|
||
*/
|
||
#map-full {
|
||
width: 100%; height: calc(100dvh - env(safe-area-inset-top, 44px) - 44px - 83px - env(safe-area-inset-bottom, 34px));
|
||
min-height: 400px;
|
||
}
|
||
.map-filter-bar {
|
||
padding: 8px 12px; display: flex; gap: 8px; overflow-x: auto;
|
||
scrollbar-width: none; -webkit-overflow-scrolling: touch;
|
||
}
|
||
.map-filter-bar::-webkit-scrollbar { display: none; }
|
||
.filter-chip {
|
||
flex-shrink: 0; padding: 6px 14px; border-radius: var(--radius-xl); border: 1.5px solid var(--separator);
|
||
background: var(--fill-tertiary); color: var(--label-secondary);
|
||
font-size: 13px; font-weight: 500; cursor: pointer; white-space: nowrap; transition: all 0.2s;
|
||
}
|
||
.filter-chip.active { border-color: #0A84FF; background: rgba(10,132,255,0.12); color: #0A84FF; }
|
||
.map-legend {
|
||
position: absolute; bottom: calc(90px + env(safe-area-inset-bottom, 34px) + 8px);
|
||
right: 12px; z-index: 500;
|
||
background: rgba(28,28,30,0.88); backdrop-filter: blur(20px);
|
||
border-radius: var(--radius-md); padding: 10px 12px;
|
||
border: 0.5px solid var(--separator);
|
||
font-size: 11px;
|
||
}
|
||
.legend-row { display: flex; align-items: center; gap: 6px; padding: 2px 0; color: var(--label-secondary); }
|
||
.legend-dot { width: 10px; height: 10px; border-radius: var(--radius-full); flex-shrink: 0; }
|
||
|
||
/* Mission bottom-sheet */
|
||
.mission-sheet {
|
||
position: fixed; left: 0; right: 0; bottom: calc(83px + env(safe-area-inset-bottom, 34px));
|
||
z-index: 400; padding: 0 12px 12px;
|
||
transform: translateY(200px); opacity: 0;
|
||
transition: all 0.35s cubic-bezier(0.32, 0.72, 0, 1);
|
||
pointer-events: none;
|
||
}
|
||
.mission-sheet.show { transform: translateY(0); opacity: 1; pointer-events: all; }
|
||
.sheet-card {
|
||
background: rgba(28,28,30,0.96); backdrop-filter: blur(40px);
|
||
border-radius: var(--radius-lg); padding: 16px;
|
||
border: 0.5px solid var(--separator);
|
||
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
||
}
|
||
.sheet-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 10px; }
|
||
.sheet-title { font-size: 17px; font-weight: 700; letter-spacing: -0.4px; flex: 1; padding-right: 8px; }
|
||
.sheet-close { background: var(--fill-secondary); border: none; border-radius: var(--radius-full); width: 28px; height: 28px; cursor: pointer; font-size: 14px; color: var(--label-secondary); flex-shrink: 0; }
|
||
.sheet-meta { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
|
||
.sheet-chip { padding: 4px 10px; border-radius: var(--radius-xl); font-size: 12px; font-weight: 600; }
|
||
.chip-reward { background: rgba(52,199,89,0.15); color: var(--ios-green); }
|
||
.chip-city { background: rgba(10,132,255,0.12); color: #0A84FF; }
|
||
.chip-type { background: var(--fill-tertiary); color: var(--label-secondary); }
|
||
.chip-dead { background: rgba(255,149,0,0.12); color: var(--ios-orange); }
|
||
.sheet-desc { font-size: 14px; color: var(--label-secondary); line-height: 1.5; margin-bottom: 14px; }
|
||
.sheet-actions { display: flex; gap: 10px; }
|
||
.sheet-btn {
|
||
flex: 1; padding: 13px; border-radius: var(--radius-md); border: none;
|
||
font-family: var(--font-text); font-size: 15px; font-weight: 600;
|
||
cursor: pointer; transition: opacity 0.15s;
|
||
}
|
||
.sheet-btn:active { opacity: 0.7; }
|
||
.btn-primary { background: #0A84FF; color: #fff; }
|
||
.btn-secondary { background: var(--fill-secondary); color: var(--label-primary); }
|
||
|
||
/*
|
||
MINA UPPDRAG TAB
|
||
*/
|
||
.missions-list { padding: 0 16px; display: flex; flex-direction: column; gap: 12px; }
|
||
.mission-card {
|
||
background: var(--background-secondary); border-radius: var(--radius-lg); overflow: hidden;
|
||
border: 0.5px solid var(--separator);
|
||
cursor: pointer; transition: transform 0.12s, opacity 0.12s;
|
||
}
|
||
.mission-card:active { transform: scale(0.98); opacity: 0.85; }
|
||
.mc-top {
|
||
display: flex; align-items: flex-start; gap: 12px; padding: 14px 14px 10px;
|
||
}
|
||
.mc-icon {
|
||
width: 46px; height: 46px; border-radius: var(--radius-md);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 22px; flex-shrink: 0;
|
||
}
|
||
.mc-body { flex: 1; min-width: 0; }
|
||
.mc-title {
|
||
font-size: 16px; font-weight: 600; letter-spacing: -0.32px;
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||
}
|
||
.mc-loc { font-size: 13px; color: var(--label-secondary); margin-top: 2px; }
|
||
.mc-reward { font-size: 22px; font-weight: 700; font-family: var(--font-display); color: var(--ios-green); flex-shrink: 0; }
|
||
.mc-reward small { font-size: 12px; font-weight: 500; color: var(--label-secondary); }
|
||
.mc-bottom {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 10px 14px 14px; border-top: 0.5px solid var(--separator);
|
||
}
|
||
.badge {
|
||
padding: 3px 9px; border-radius: var(--radius-xl); font-size: 11px; font-weight: 700;
|
||
text-transform: uppercase; letter-spacing: 0.3px;
|
||
}
|
||
.b-open { background: rgba(52,199,89,0.15); color: var(--ios-green); }
|
||
.b-active { background: rgba(10,132,255,0.15); color: #0A84FF; }
|
||
.b-completed { background: rgba(88,86,214,0.15); color: var(--ios-indigo); }
|
||
.b-cancelled { background: rgba(255,69,58,0.15); color: var(--ios-red); }
|
||
.mc-cta { font-size: 14px; font-weight: 600; color: #0A84FF; display: flex; align-items: center; gap: 4px; }
|
||
|
||
/* Active mission banner */
|
||
.active-banner {
|
||
margin: 12px 16px;
|
||
background: linear-gradient(135deg, rgba(10,132,255,0.18), rgba(94,92,230,0.18));
|
||
border: 1px solid rgba(10,132,255,0.35);
|
||
border-radius: var(--radius-lg); padding: 16px; display: none;
|
||
}
|
||
.active-banner.show { display: block; }
|
||
.ab-label { font-size: 11px; font-weight: 700; color: #0A84FF; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
|
||
.ab-title { font-size: 17px; font-weight: 700; margin-bottom: 4px; }
|
||
.ab-loc { font-size: 13px; color: var(--label-secondary); margin-bottom: 12px; }
|
||
.ab-actions { display: flex; gap: 8px; }
|
||
.ab-btn {
|
||
flex: 1; padding: 11px 8px; border-radius: var(--radius-md); border: none;
|
||
font-family: var(--font-text); font-size: 14px; font-weight: 600;
|
||
cursor: pointer; transition: opacity 0.15s;
|
||
}
|
||
.ab-btn:active { opacity: 0.7; }
|
||
.btn-cam { background: var(--fill-secondary); color: var(--label-primary); }
|
||
.btn-done { background: var(--ios-green); color: #fff; }
|
||
.btn-cancel-sm { background: rgba(255,69,58,0.12); color: var(--ios-red); }
|
||
|
||
/* Empty state */
|
||
.empty-state {
|
||
padding: 72px 32px; text-align: center;
|
||
display: flex; flex-direction: column; align-items: center; gap: 12px;
|
||
}
|
||
.empty-icon { font-size: 60px; }
|
||
.empty-title { font-size: 20px; font-weight: 700; }
|
||
.empty-sub { font-size: 15px; color: var(--label-secondary); line-height: 1.5; }
|
||
|
||
/*
|
||
INTÄKTER TAB — Earnings
|
||
*/
|
||
.earnings-hero {
|
||
background: linear-gradient(160deg, #061628 0%, #010b1a 50%, #030e21 100%);
|
||
padding: 28px 24px 32px; text-align: center; position: relative; overflow: hidden;
|
||
}
|
||
.earnings-hero::before {
|
||
content: '';
|
||
position: absolute; top: -60%; left: -30%; right: -30%; bottom: -20%;
|
||
background: radial-gradient(circle at 50% 35%, rgba(10,132,255,0.28) 0%, transparent 65%);
|
||
pointer-events: none;
|
||
}
|
||
.eh-label {
|
||
font-size: 12px; font-weight: 600; color: rgba(255,255,255,0.75);
|
||
text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px;
|
||
}
|
||
.eh-amount {
|
||
font-family: var(--font-display); font-size: 60px; font-weight: 800;
|
||
color: #fff; letter-spacing: -2.5px; line-height: 1; position: relative; z-index: 1;
|
||
}
|
||
.eh-currency { font-size: 28px; font-weight: 500; color: rgba(255,255,255,0.75); }
|
||
.eh-sub { font-size: 13px; color: rgba(255,255,255,0.75); margin-top: 6px; position: relative; z-index: 1; }
|
||
.eh-actions { display: flex; gap: 10px; margin-top: 20px; position: relative; z-index: 1; }
|
||
.eh-btn {
|
||
flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px;
|
||
background: rgba(255,255,255,0.1); border: none; border-radius: var(--radius-lg);
|
||
padding: 13px 8px; cursor: pointer; transition: background 0.15s; font-family: var(--font-text);
|
||
}
|
||
.eh-btn:active { background: rgba(255,255,255,0.18); }
|
||
.eh-btn-icon { font-size: 22px; }
|
||
.eh-btn-label { font-size: 12px; font-weight: 600; color: rgba(255,255,255,0.75); }
|
||
|
||
.earnings-stats {
|
||
display: grid; grid-template-columns: repeat(3, 1fr);
|
||
background: var(--background-secondary); border-bottom: 0.5px solid var(--separator);
|
||
}
|
||
.e-stat { padding: 16px 8px; text-align: center; border-right: 0.5px solid var(--separator); }
|
||
.e-stat:last-child { border-right: none; }
|
||
.e-stat-val { font-size: 20px; font-weight: 700; font-family: var(--font-display); }
|
||
.e-stat-lbl { font-size: 11px; color: var(--label-secondary); margin-top: 2px; }
|
||
|
||
/* Payout CTA */
|
||
.payout-card {
|
||
margin: 16px; background: var(--background-secondary);
|
||
border-radius: var(--radius-lg); overflow: hidden; border: 0.5px solid var(--separator);
|
||
}
|
||
.payout-header {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 14px 16px; border-bottom: 0.5px solid var(--separator);
|
||
}
|
||
.payout-title { font-size: 16px; font-weight: 600; }
|
||
.payout-badge { padding: 4px 10px; border-radius: var(--radius-xl); font-size: 12px; font-weight: 700; }
|
||
|
||
.pb-pending { background: rgba(255,149,0,0.12); color: var(--ios-orange); }
|
||
.payout-row {
|
||
display: flex; align-items: center; gap: 12px;
|
||
padding: 14px 16px; border-bottom: 0.5px solid var(--separator);
|
||
}
|
||
.payout-row:last-child { border-bottom: none; }
|
||
.pr-icon {
|
||
width: 40px; height: 40px; border-radius: var(--radius-md);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 20px; flex-shrink: 0;
|
||
}
|
||
.pri-credit { background: rgba(52,199,89,0.15); }
|
||
.pri-pending { background: rgba(255,149,0,0.15); }
|
||
.pri-payout { background: rgba(10,132,255,0.12); }
|
||
.pr-body { flex: 1; min-width: 0; }
|
||
.pr-title { font-size: 15px; font-weight: 600; }
|
||
.pr-sub { font-size: 13px; color: var(--label-secondary); margin-top: 1px; }
|
||
.pr-amount { font-size: 16px; font-weight: 700; font-family: var(--font-display); }
|
||
.pr-amount.credit { color: var(--ios-green); }
|
||
.pr-amount.pending { color: var(--ios-orange); }
|
||
.pr-amount.payout { color: #0A84FF; }
|
||
|
||
/*
|
||
LEADERBOARD TAB
|
||
*/
|
||
.lb-hero {
|
||
padding: 20px 16px 12px;
|
||
background: linear-gradient(135deg, rgba(10,132,255,0.1) 0%, rgba(94,92,230,0.1) 100%);
|
||
border-bottom: 0.5px solid var(--separator);
|
||
}
|
||
.lb-hero-title { font-size: 28px; font-weight: 800; letter-spacing: -1px; }
|
||
.lb-hero-sub { font-size: 14px; color: var(--label-secondary); margin-top: 4px; }
|
||
.lb-totals {
|
||
display: flex; gap: 16px; margin-top: 14px; flex-wrap: wrap;
|
||
}
|
||
.lb-total-chip {
|
||
display: flex; align-items: center; gap: 6px;
|
||
background: var(--fill-secondary); border-radius: var(--radius-xl);
|
||
padding: 6px 12px; font-size: 13px; font-weight: 600;
|
||
}
|
||
|
||
.lb-row {
|
||
display: flex; align-items: center; gap: 12px;
|
||
padding: 14px 16px; border-bottom: 0.5px solid var(--separator);
|
||
cursor: pointer; transition: background 0.1s;
|
||
}
|
||
.lb-row:active { background: var(--fill-quaternary); }
|
||
.lb-rank {
|
||
font-size: 18px; font-weight: 800; font-family: var(--font-display);
|
||
min-width: 28px; text-align: center; flex-shrink: 0;
|
||
}
|
||
.rank-1 { color: #FFD700; }
|
||
.rank-2 { color: #C0C0C0; }
|
||
.rank-3 { color: #CD7F32; }
|
||
.rank-n { color: var(--label-tertiary); }
|
||
.lb-avatar {
|
||
width: 44px; height: 44px; border-radius: var(--radius-full);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 22px; flex-shrink: 0;
|
||
}
|
||
.lb-body { flex: 1; min-width: 0; }
|
||
.lb-name { font-size: 16px; font-weight: 600; letter-spacing: -0.3px; }
|
||
.lb-meta { font-size: 13px; color: var(--label-secondary); margin-top: 2px; }
|
||
.lb-right { text-align: right; flex-shrink: 0; }
|
||
.lb-missions { font-size: 20px; font-weight: 800; font-family: var(--font-display); }
|
||
.lb-earnings { font-size: 12px; color: var(--label-secondary); margin-top: 1px; }
|
||
|
||
.city-row {
|
||
display: flex; align-items: center; gap: 14px;
|
||
padding: 14px 16px; border-bottom: 0.5px solid var(--separator);
|
||
}
|
||
.city-rank {
|
||
font-size: 20px; font-weight: 800; font-family: var(--font-display);
|
||
min-width: 30px; text-align: center; flex-shrink: 0;
|
||
}
|
||
.city-icon { font-size: 28px; flex-shrink: 0; }
|
||
.city-body { flex: 1; min-width: 0; }
|
||
.city-name { font-size: 16px; font-weight: 700; }
|
||
.city-meta { font-size: 13px; color: var(--label-secondary); margin-top: 1px; }
|
||
.city-bar-wrap { flex: 1; }
|
||
.city-bar-track { height: 5px; background: var(--fill-secondary); border-radius: var(--radius-sm); overflow: hidden; }
|
||
.city-bar-fill { height: 100%; border-radius: var(--radius-sm); background: linear-gradient(90deg, #0A84FF, #5E5CE6); }
|
||
.city-count { font-size: 18px; font-weight: 700; font-family: var(--font-display); text-align: right; flex-shrink: 0; }
|
||
|
||
/*
|
||
SHARED UTILITIES
|
||
*/
|
||
.toast {
|
||
position: fixed; bottom: calc(90px + env(safe-area-inset-bottom, 0px));
|
||
left: 50%; transform: translateX(-50%) translateY(20px);
|
||
background: rgba(50,50,52,0.96); color: #fff;
|
||
padding: 10px 20px; border-radius: var(--radius-xl); font-size: 14px; font-weight: 500;
|
||
opacity: 0; transition: all 0.3s; z-index: 1000; white-space: nowrap; pointer-events: none;
|
||
}
|
||
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||
|
||
.loading-row { display: flex; justify-content: center; padding: 48px; }
|
||
.spinner {
|
||
width: 28px; height: 28px; border: 3px solid var(--fill-secondary);
|
||
border-top-color: #0A84FF; border-radius: var(--radius-full); animation: spin 0.8s linear infinite;
|
||
}
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
|
||
.pull-hint { text-align: center; padding: 12px; font-size: 12px; color: var(--label-quaternary); }
|
||
|
||
/* Upload sheet */
|
||
.upload-sheet {
|
||
position: fixed; left: 0; right: 0; bottom: 0; z-index: 600;
|
||
background: rgba(28,28,30,0.98); backdrop-filter: blur(40px);
|
||
border-top-left-radius: 20px; border-top-right-radius: 20px;
|
||
padding: 20px 20px calc(env(safe-area-inset-bottom,34px) + 20px);
|
||
transform: translateY(100%); transition: transform 0.35s cubic-bezier(0.32,0.72,0,1);
|
||
border-top: 0.5px solid var(--separator);
|
||
}
|
||
.upload-sheet.show { transform: translateY(0); }
|
||
.upload-handle { width: 36px; height: 4px; border-radius: var(--radius-sm); background: var(--fill-primary); margin: 0 auto 20px; }
|
||
.upload-title { font-size: 18px; font-weight: 700; margin-bottom: 4px; }
|
||
.upload-sub { font-size: 14px; color: var(--label-secondary); margin-bottom: 20px; }
|
||
.upload-previews { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 16px; }
|
||
.preview-thumb {
|
||
width: 80px; height: 80px; border-radius: var(--radius-md); object-fit: cover;
|
||
border: 0.5px solid var(--separator); background: var(--fill-secondary);
|
||
display: flex; align-items: center; justify-content: center; font-size: 28px;
|
||
}
|
||
.upload-actions { display: flex; gap: 10px; }
|
||
.upload-btn {
|
||
flex: 1; padding: 14px; border-radius: var(--radius-md); border: none;
|
||
font-family: var(--font-text); font-size: 15px; font-weight: 600;
|
||
cursor: pointer; transition: opacity 0.15s;
|
||
}
|
||
.upload-btn:active { opacity: 0.7; }
|
||
@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; }
|
||
}
|
||
/* QUIXZOOM STANDARD — Dark Theme */
|
||
:root {
|
||
--qz-bg: #0a0a0a;
|
||
--qz-surface: #111111;
|
||
--qz-surface-2: #1a1a1a;
|
||
--qz-border: rgba(255,255,255,0.08);
|
||
--qz-text: #e8e8e8;
|
||
--qz-text-dim: #a0a0a0;
|
||
--qz-text-muted: #6b6b6b;
|
||
--qz-blue: #007AFF;
|
||
--qz-blue-dark: #0056CC;
|
||
--qz-radius: 10px;
|
||
}
|
||
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Inter', sans-serif;
|
||
background: var(--qz-bg);
|
||
color: var(--qz-text);
|
||
line-height: 1.6;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
|
||
/* Navigation */
|
||
.qz-nav {
|
||
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 0 24px; height: 56px;
|
||
background: rgba(255,255,255,0.92);
|
||
backdrop-filter: blur(16px);
|
||
border-bottom: 1px solid var(--qz-border);
|
||
}
|
||
|
||
.qz-nav-logo { font-size: 18px; font-weight: 700; color: #fff; text-decoration: none; }
|
||
.qz-nav-links { display: flex; gap: 24px; list-style: none; }
|
||
.qz-nav-links a { font-size: 14px; color: var(--qz-text-dim); text-decoration: none; transition: color 0.2s; }
|
||
.qz-nav-links a:hover { color: #fff; }
|
||
|
||
/* Buttons */
|
||
.qz-btn {
|
||
display: inline-flex; align-items: center; gap: 6px;
|
||
padding: 12px 24px; border-radius: var(--qz-radius);
|
||
font-size: 15px; font-weight: 600; text-decoration: none;
|
||
transition: all 0.2s; cursor: pointer; border: none;
|
||
}
|
||
|
||
.qz-btn-primary {
|
||
background: var(--qz-blue); color: #fff;
|
||
}
|
||
.qz-btn-primary:hover {
|
||
background: var(--qz-blue-dark); transform: translateY(-1px);
|
||
}
|
||
|
||
.qz-btn-ghost {
|
||
background: transparent; color: var(--qz-text);
|
||
border: 1px solid var(--qz-border);
|
||
}
|
||
.qz-btn-ghost:hover {
|
||
border-color: var(--qz-blue); color: var(--qz-blue);
|
||
}
|
||
|
||
/* Cards */
|
||
.qz-card {
|
||
background: var(--qz-surface);
|
||
border: 1px solid var(--qz-border);
|
||
border-radius: var(--qz-radius);
|
||
padding: 24px;
|
||
transition: transform 0.2s, border-color 0.2s;
|
||
}
|
||
.qz-card:hover {
|
||
transform: translateY(-2px);
|
||
border-color: rgba(0,122,255,0.3);
|
||
}
|
||
|
||
/* Footer */
|
||
.qz-footer {
|
||
border-top: 1px solid var(--qz-border);
|
||
padding: 40px 24px;
|
||
text-align: center;
|
||
color: var(--qz-text-dim);
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* Banner */
|
||
.qz-banner {
|
||
background: #001E50;
|
||
color: rgba(255,255,255,0.55);
|
||
text-align: center;
|
||
padding: 10px;
|
||
font-size: 12px;
|
||
}
|
||
.qz-banner a { color: rgba(255,255,255,0.8); text-decoration: none; }
|
||
|
||
@media (max-width: 768px) {
|
||
.qz-nav-links { display: none; }
|
||
}
|
||
</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; }
|
||
}
|
||
/* QUIXZOOM STANDARD — Dark Theme */
|
||
:root {
|
||
--qz-bg: #0a0a0a;
|
||
--qz-surface: #111111;
|
||
--qz-surface-2: #1a1a1a;
|
||
--qz-border: rgba(255,255,255,0.08);
|
||
--qz-text: #e8e8e8;
|
||
--qz-text-dim: #a0a0a0;
|
||
--qz-text-muted: #6b6b6b;
|
||
--qz-blue: #007AFF;
|
||
--qz-blue-dark: #0056CC;
|
||
--qz-radius: 10px;
|
||
}
|
||
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Inter', sans-serif;
|
||
background: var(--qz-bg);
|
||
color: var(--qz-text);
|
||
line-height: 1.6;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
|
||
/* Navigation */
|
||
.qz-nav {
|
||
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 0 24px; height: 56px;
|
||
background: rgba(255,255,255,0.92);
|
||
backdrop-filter: blur(16px);
|
||
border-bottom: 1px solid var(--qz-border);
|
||
}
|
||
|
||
.qz-nav-logo { font-size: 18px; font-weight: 700; color: #fff; text-decoration: none; }
|
||
.qz-nav-links { display: flex; gap: 24px; list-style: none; }
|
||
.qz-nav-links a { font-size: 14px; color: var(--qz-text-dim); text-decoration: none; transition: color 0.2s; }
|
||
.qz-nav-links a:hover { color: #fff; }
|
||
|
||
/* Buttons */
|
||
.qz-btn {
|
||
display: inline-flex; align-items: center; gap: 6px;
|
||
padding: 12px 24px; border-radius: var(--qz-radius);
|
||
font-size: 15px; font-weight: 600; text-decoration: none;
|
||
transition: all 0.2s; cursor: pointer; border: none;
|
||
}
|
||
|
||
.qz-btn-primary {
|
||
background: var(--qz-blue); color: #fff;
|
||
}
|
||
.qz-btn-primary:hover {
|
||
background: var(--qz-blue-dark); transform: translateY(-1px);
|
||
}
|
||
|
||
.qz-btn-ghost {
|
||
background: transparent; color: var(--qz-text);
|
||
border: 1px solid var(--qz-border);
|
||
}
|
||
.qz-btn-ghost:hover {
|
||
border-color: var(--qz-blue); color: var(--qz-blue);
|
||
}
|
||
|
||
/* Cards */
|
||
.qz-card {
|
||
background: var(--qz-surface);
|
||
border: 1px solid var(--qz-border);
|
||
border-radius: var(--qz-radius);
|
||
padding: 24px;
|
||
transition: transform 0.2s, border-color 0.2s;
|
||
}
|
||
.qz-card:hover {
|
||
transform: translateY(-2px);
|
||
border-color: rgba(0,122,255,0.3);
|
||
}
|
||
|
||
/* Footer */
|
||
.qz-footer {
|
||
border-top: 1px solid var(--qz-border);
|
||
padding: 40px 24px;
|
||
text-align: center;
|
||
color: var(--qz-text-dim);
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* Banner */
|
||
.qz-banner {
|
||
background: #001E50;
|
||
color: rgba(255,255,255,0.55);
|
||
text-align: center;
|
||
padding: 10px;
|
||
font-size: 12px;
|
||
}
|
||
.qz-banner a { color: rgba(255,255,255,0.8); text-decoration: none; }
|
||
|
||
@media (max-width: 768px) {
|
||
.qz-nav-links { display: none; }
|
||
}
|
||
</style>
|
||
<script src="i18n.js"></script>
|
||
<meta property="og:type" content="website">
|
||
<meta property="og:title" content="quiXzoom — Zoomer App">
|
||
<meta property="og:description" content="quiXzoom — Earn per Mission. No Platform Fee.">
|
||
<meta property="og:url" content="https://www.quixzoom.com/zoomer-app.html">
|
||
<meta property="og:image" content="https://www.quixzoom.com/images/app-screenshot.jpg">
|
||
<meta property="og:image:width" content="1200">
|
||
<meta property="og:image:height" content="630">
|
||
<meta property="og:image:alt" content="quiXzoom app on iPhone showing missions and QX Credit rewards">
|
||
<meta name="twitter:card" content="summary_large_image">
|
||
<meta name="twitter:image" content="https://www.quixzoom.com/images/app-screenshot.jpg">
|
||
<meta name="twitter:title" content="quiXzoom — Zoomer App">
|
||
<script type="module" src="https://auth.quixzoom.com/site-auth-snippet.js"></script>
|
||
<link rel="alternate" hreflang="en" href="https://www.quixzoom.com/" />
|
||
<link rel="alternate" hreflang="sv" href="https://www.quixzoom.com/sv/" />
|
||
<link rel="alternate" hreflang="de" href="https://www.quixzoom.com/de/" />
|
||
<link rel="alternate" hreflang="fr" href="https://www.quixzoom.com/fr/" />
|
||
<link rel="alternate" hreflang="es" href="https://www.quixzoom.com/es/" />
|
||
<link rel="alternate" hreflang="it" href="https://www.quixzoom.com/it/" />
|
||
<link rel="alternate" hreflang="nl" href="https://www.quixzoom.com/nl/" />
|
||
<link rel="alternate" hreflang="x-default" href="https://www.quixzoom.com/" />
|
||
</head>
|
||
<body>
|
||
<h1 style="position:absolute;left:-9999px;">Zoomer App - quiXzoom</h1>
|
||
|
||
<!-- KARTA TAB -->
|
||
<div id="page-karta" class="tab-page active">
|
||
<nav class="qz-nav" class="nav-bar">
|
||
<span class="nav-title nav-logo">quiXzoom</span>
|
||
<button class="nav-btn-sm" onclick="refreshMap()">↻</button>
|
||
</nav>
|
||
<div class="map-filter-bar" id="city-filters">
|
||
<button class="filter-chip active" data-city="" onclick="setCityFilter('',this)">Alla städer</button>
|
||
<button class="filter-chip" data-city="Stockholm" onclick="setCityFilter('Stockholm',this)">Stockholm</button>
|
||
<button class="filter-chip" data-city="Göteborg" onclick="setCityFilter('Göteborg',this)">Göteborg</button>
|
||
<button class="filter-chip" data-city="Malmö" onclick="setCityFilter('Malmö',this)">Malmö</button>
|
||
<button class="filter-chip" data-city="Uppsala" onclick="setCityFilter('Uppsala',this)">Uppsala</button>
|
||
<button class="filter-chip" data-city="Västerås" onclick="setCityFilter('Västerås',this)">Västerås</button>
|
||
</div>
|
||
<div id="map-full"></div>
|
||
<div class="map-legend">
|
||
<div class="legend-row"><div class="legend-dot" style="background:#0A84FF"></div>Foto</div>
|
||
<div class="legend-row"><div class="legend-dot" style="background:#5E5CE6"></div>Video</div>
|
||
<div class="legend-row"><div class="legend-dot" style="background:#FF9500"></div>Drönare</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Mission bottom-sheet (karta) -->
|
||
<div class="mission-sheet" id="mission-sheet">
|
||
<div class="sheet-card">
|
||
<div class="sheet-header">
|
||
<div class="sheet-title" id="sheet-title">—</div>
|
||
<button class="sheet-close" onclick="closeSheet()"></button>
|
||
</div>
|
||
<div class="sheet-meta" id="sheet-meta"></div>
|
||
<div class="sheet-desc" id="sheet-desc">—</div>
|
||
<div class="sheet-actions">
|
||
<button class="sheet-btn btn-secondary" onclick="closeSheet()">Stäng</button>
|
||
<button class="sheet-btn btn-primary" id="sheet-accept-btn" onclick="acceptFromSheet()">Acceptera uppdrag</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MINA UPPDRAG TAB -->
|
||
<div id="page-mina" class="tab-page">
|
||
<nav class="qz-nav" class="nav-bar">
|
||
<span class="nav-title">Mina uppdrag</span>
|
||
</nav>
|
||
<div class="segment-wrap">
|
||
<div class="segment">
|
||
<button class="seg-btn active" onclick="setMiMissionsFilter('all',this)">Alla</button>
|
||
<button class="seg-btn" onclick="setMiMissionsFilter('active',this)">Aktiva</button>
|
||
<button class="seg-btn" onclick="setMiMissionsFilter('completed',this)">Slutförda</button>
|
||
</div>
|
||
</div>
|
||
<div id="active-banner" class="active-banner">
|
||
<div class="ab-label"> Aktivt uppdrag</div>
|
||
<div class="ab-title" id="ab-title">—</div>
|
||
<div class="ab-loc" id="ab-loc">—</div>
|
||
<div class="ab-actions">
|
||
<button class="ab-btn btn-cam" onclick="openUploadSheet()"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg> Ladda upp media</button>
|
||
<button class="ab-btn btn-done" onclick="completeActiveMission()"><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> Slutför</button>
|
||
<button class="ab-btn btn-cancel-sm" onclick="cancelActiveMission()"></button>
|
||
</div>
|
||
</div>
|
||
<div class="section-hdr" id="mina-section-lbl">Öppna uppdrag</div>
|
||
<div id="mina-list" class="missions-list"></div>
|
||
<div style="height:20px"></div>
|
||
</div>
|
||
|
||
<!-- INTÄKTER TAB -->
|
||
<div id="page-intakter" class="tab-page">
|
||
<nav class="qz-nav" class="nav-bar">
|
||
<span class="nav-title">Intäkter</span>
|
||
</nav>
|
||
<div class="earnings-hero">
|
||
<div class="eh-label">Tillgängligt att ta ut</div>
|
||
<div class="eh-amount"><span class="eh-currency">kr </span><span id="e-avail">4 820</span></div>
|
||
<div class="eh-sub" id="e-pending-lbl">kr 1 250 väntar på verifiering</div>
|
||
<div class="eh-actions">
|
||
<button class="eh-btn" onclick="requestPayout()">
|
||
<span class="eh-btn-icon"></span><span class="eh-btn-label">Ta ut</span>
|
||
</button>
|
||
|
||
<button class="eh-btn" onclick="showToast(' Exporterar årssammanfattning...')">
|
||
<span class="eh-btn-icon"></span><span class="eh-btn-label">Skatt</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="earnings-stats">
|
||
<div class="e-stat">
|
||
<div class="e-stat-val" id="e-month" style="color:var(--ios-green)">—</div>
|
||
<div class="e-stat-lbl">Denna månad</div>
|
||
</div>
|
||
<div class="e-stat">
|
||
<div class="e-stat-val" id="e-total-missions" style="color:#0A84FF">—</div>
|
||
<div class="e-stat-lbl">Uppdrag totalt</div>
|
||
</div>
|
||
<div class="e-stat">
|
||
<div class="e-stat-val" id="e-avg" style="color:var(--ios-orange)">—</div>
|
||
<div class="e-stat-lbl">Snitt/uppdrag</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section-hdr">Väntande utbetalningar</div>
|
||
<div class="payout-card">
|
||
<div class="payout-header">
|
||
<div class="payout-title">Inkommande utbetalning</div>
|
||
<div class="payout-badge pb-pending">Pågår</div>
|
||
</div>
|
||
<div class="payout-row">
|
||
<div class="pr-icon pri-credit"></div>
|
||
<div class="pr-body">
|
||
<div class="pr-title">Fastighetsfilm — Lidingö</div>
|
||
<div class="pr-sub">Verifieras · förfaller 5 jun</div>
|
||
</div>
|
||
<div class="pr-amount credit">+2 800 kr</div>
|
||
</div>
|
||
<div class="payout-row">
|
||
<div class="pr-icon pri-pending">⏳</div>
|
||
<div class="pr-body">
|
||
<div class="pr-title">Hotell Grand Stockholm</div>
|
||
<div class="pr-sub">Granskas av kund · 2–3 dagar</div>
|
||
</div>
|
||
<div class="pr-amount pending">~7 200 kr</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section-hdr">Transaktionshistorik</div>
|
||
<div class="payout-card" style="margin-bottom:0">
|
||
<div id="tx-list"></div>
|
||
</div>
|
||
<div style="height:20px"></div>
|
||
</div>
|
||
|
||
<!-- LEADERBOARD TAB -->
|
||
<div id="page-leaderboard" class="tab-page">
|
||
<nav class="qz-nav" class="nav-bar">
|
||
<span class="nav-title">Leaderboard</span>
|
||
</nav>
|
||
<div class="lb-hero">
|
||
<div class="lb-hero-title">Top Zoomers</div>
|
||
<div class="lb-hero-sub">Sverige · Augusti 2026 · Rankings uppdateras live</div>
|
||
<div class="lb-totals" id="lb-totals">
|
||
<div class="lb-total-chip"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg> Laddar...</div>
|
||
</div>
|
||
</div>
|
||
<div class="segment-wrap">
|
||
<div class="segment">
|
||
<button class="seg-btn active" onclick="setLbView('zoomers',this)">Zoomers</button>
|
||
<button class="seg-btn" onclick="setLbView('cities',this)">Städer</button>
|
||
</div>
|
||
</div>
|
||
<div id="lb-zoomers-list"></div>
|
||
<div id="lb-cities-list" style="display:none"></div>
|
||
<div style="height:20px"></div>
|
||
</div>
|
||
|
||
<!-- TAB BAR -->
|
||
<nav class="qz-nav" class="tab-bar">
|
||
<button class="tab-item active" id="tab-karta" onclick="switchTab('karta')">
|
||
<span class="tab-icon"></span>
|
||
<span class="tab-label">Karta</span>
|
||
</button>
|
||
<button class="tab-item" id="tab-mina" onclick="switchTab('mina')">
|
||
<span class="tab-badge-wrap">
|
||
<span class="tab-icon"><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></span>
|
||
<span class="tab-dot" id="mina-dot"></span>
|
||
</span>
|
||
<span class="tab-label">Mina</span>
|
||
</button>
|
||
<button class="tab-item" id="tab-intakter" onclick="switchTab('intakter')">
|
||
<span class="tab-icon"></span>
|
||
<span class="tab-label">Intäkter</span>
|
||
</button>
|
||
<button class="tab-item" id="tab-leaderboard" onclick="switchTab('leaderboard')">
|
||
<span class="tab-icon"></span>
|
||
<span class="tab-label">Topp</span>
|
||
</button>
|
||
</nav>
|
||
|
||
<div class="toast" id="toast"></div>
|
||
|
||
<!-- Upload sheet -->
|
||
<div class="upload-sheet" id="upload-sheet">
|
||
<div class="upload-handle"></div>
|
||
<div class="upload-title">Ladda upp bevis</div>
|
||
<div class="upload-sub">Bifoga foton eller video som bevis på slutfört uppdrag</div>
|
||
<div class="upload-previews" id="upload-previews"></div>
|
||
<div class="upload-actions">
|
||
<button class="upload-btn" style="background:var(--fill-secondary);color:var(--label-primary)" onclick="triggerFileInput()"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg> Välj fil</button>
|
||
<button class="upload-btn btn-primary" onclick="submitMedia()">Skicka bevis ›</button>
|
||
</div>
|
||
</div>
|
||
<div id="upload-overlay" onclick="closeUploadSheet()" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:595;"></div>
|
||
<input type="file" id="file-input" accept="image/*,video/*" capture="environment" multiple style="display:none">
|
||
|
||
<script>
|
||
const API = 'https://api.quixzoom.com/api/qz';
|
||
const DEMO_ZOOMER_ID = 'qz-zreg-001';
|
||
|
||
let allMapMissions = [];
|
||
let mapInstance = null;
|
||
let mapMarkers = [];
|
||
let cityFilter = '';
|
||
let selectedMapMission = null;
|
||
let activeMission = null;
|
||
let myMissions = [];
|
||
let miMissionsFilter = 'all';
|
||
let lbView = 'zoomers';
|
||
let lbData = null;
|
||
let uploadedMediaUrls = [];
|
||
|
||
// Toast
|
||
function showToast(msg) {
|
||
const t = document.getElementById('toast');
|
||
t.textContent = msg; t.classList.add('show');
|
||
setTimeout(() => t.classList.remove('show'), 2800);
|
||
}
|
||
|
||
// Tab switching
|
||
function switchTab(tab) {
|
||
document.querySelectorAll('.tab-page').forEach(p => p.classList.remove('active'));
|
||
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
|
||
document.getElementById('page-' + tab).classList.add('active');
|
||
document.getElementById('tab-' + tab).classList.add('active');
|
||
if (tab === 'karta') initMap();
|
||
if (tab === 'mina') loadMinaMissions();
|
||
if (tab === 'intakter') loadEarnings();
|
||
if (tab === 'leaderboard') loadLeaderboard();
|
||
}
|
||
|
||
// Active mission (localStorage)
|
||
function setActiveMission(m) {
|
||
activeMission = m;
|
||
if (m) localStorage.setItem('qz_active', JSON.stringify(m));
|
||
else localStorage.removeItem('qz_active');
|
||
updateMinaDot();
|
||
}
|
||
function getActiveMission() {
|
||
if (activeMission) return activeMission;
|
||
try { return JSON.parse(localStorage.getItem('qz_active')); } catch { return null; }
|
||
}
|
||
function updateMinaDot() {
|
||
const dot = document.getElementById('mina-dot');
|
||
if (dot) dot.classList.toggle('show', !!getActiveMission());
|
||
}
|
||
|
||
//
|
||
// KARTA TAB
|
||
//
|
||
|
||
const TYPE_CFG = {
|
||
photo: { icon: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg>', color: '#0A84FF', bg: 'rgba(10,132,255,0.2)' },
|
||
video: { icon: '', color: '#5E5CE6', bg: 'rgba(94,92,230,0.2)' },
|
||
drone: { icon: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg>', color: '#FF9500', bg: 'rgba(255,149,0,0.2)' },
|
||
survey: { icon: '<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>', color: '#34C759', bg: 'rgba(52,199,89,0.2)' },
|
||
audit: { icon: '<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>', color: '#FF2D55', bg: 'rgba(255,45,85,0.2)' },
|
||
};
|
||
function getCfg(t) { return TYPE_CFG[t] || { icon: '', color: '#8E8E93', bg: 'rgba(142,142,147,0.2)' }; }
|
||
|
||
async function fetchMapMissions(city = '') {
|
||
try {
|
||
const qs = city ? `?city=${encodeURIComponent(city)}&status=open` : '?status=open';
|
||
const r = await fetch(API + '/mission/list' + qs);
|
||
const d = await r.json();
|
||
allMapMissions = d.missions || DEMO_MISSIONS;
|
||
} catch {
|
||
allMapMissions = DEMO_MISSIONS.filter(m => !city || m.location?.city === city);
|
||
}
|
||
}
|
||
|
||
async function initMap() {
|
||
if (!mapInstance) {
|
||
mapInstance = L.map('map-full', { zoomControl: false }).setView([59.5, 16.5], 6);
|
||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OSM' }).addTo(mapInstance);
|
||
}
|
||
await fetchMapMissions(cityFilter);
|
||
renderMapMarkers();
|
||
}
|
||
|
||
function renderMapMarkers() {
|
||
mapMarkers.forEach(m => m.remove());
|
||
mapMarkers = [];
|
||
if (!mapInstance) return;
|
||
allMapMissions.forEach(m => {
|
||
const lat = m.location?.lat, lon = m.location?.lon;
|
||
if (!lat || !lon) return;
|
||
const cfg = getCfg(m.mission_type);
|
||
const icon = L.divIcon({
|
||
html: `<div style="width:40px;height:40px;border-radius: var(--radius-full);background:${cfg.bg};border:2.5px solid ${cfg.color};display:flex;align-items:center;justify-content:center;font-size:18px;box-shadow:0 2px 10px rgba(0,0,0,0.4);">${cfg.icon}</div>`,
|
||
className: '', iconSize: [40,40], iconAnchor: [20,20]
|
||
});
|
||
const marker = L.marker([lat, lon], { icon }).addTo(mapInstance);
|
||
marker.on('click', () => openMapSheet(m));
|
||
mapMarkers.push(marker);
|
||
});
|
||
if (cityFilter && allMapMissions.length > 0) {
|
||
const first = allMapMissions.find(m => m.location?.lat);
|
||
if (first) mapInstance.setView([first.location.lat, first.location.lon], 12);
|
||
}
|
||
}
|
||
|
||
function setCityFilter(city, btn) {
|
||
cityFilter = city;
|
||
document.querySelectorAll('.filter-chip').forEach(c => c.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
closeSheet();
|
||
if (mapInstance) fetchMapMissions(city).then(renderMapMarkers);
|
||
}
|
||
|
||
function refreshMap() {
|
||
initMap();
|
||
showToast('<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8C2 11.3 4.7 14 8 14C10.5 14 12.7 12.5 13.7 10.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M14 8C14 4.7 11.3 2 8 2C5.5 2 3.3 3.5 2.3 5.7" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M13.5 7.5L14 10L11.5 9.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 8.5L2 6L4.5 6.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> Uppdaterar karta...');
|
||
}
|
||
|
||
function openMapSheet(mission) {
|
||
selectedMapMission = mission;
|
||
const cfg = getCfg(mission.mission_type);
|
||
const deadline = mission.deadline ? new Date(mission.deadline).toLocaleDateString('sv-SE', { day:'numeric', month:'short' }) : '—';
|
||
document.getElementById('sheet-title').textContent = mission.title;
|
||
document.getElementById('sheet-desc').textContent = mission.description;
|
||
document.getElementById('sheet-meta').innerHTML = `
|
||
<span class="sheet-chip chip-reward">${mission.reward_sek?.toLocaleString('sv-SE')} kr</span>
|
||
<span class="sheet-chip chip-city">${mission.location?.city || '—'}</span>
|
||
<span class="sheet-chip chip-type">${cfg.icon} ${mission.mission_type}</span>
|
||
<span class="sheet-chip chip-dead">⏰ ${deadline}</span>`;
|
||
const m = getActiveMission();
|
||
const btn = document.getElementById('sheet-accept-btn');
|
||
if (m) { btn.textContent = 'Du har ett aktivt uppdrag'; btn.disabled = true; btn.style.opacity = '0.5'; }
|
||
else { btn.textContent = 'Acceptera uppdrag'; btn.disabled = false; btn.style.opacity = '1'; }
|
||
document.getElementById('mission-sheet').classList.add('show');
|
||
}
|
||
function closeSheet() {
|
||
document.getElementById('mission-sheet').classList.remove('show');
|
||
selectedMapMission = null;
|
||
}
|
||
|
||
async function acceptFromSheet() {
|
||
if (!selectedMapMission) return;
|
||
const m = selectedMapMission;
|
||
closeSheet();
|
||
try {
|
||
const r = await fetch(API + '/zoomer/accept-mission', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + getDemoToken() },
|
||
body: JSON.stringify({ mission_id: m.id, zoomer_id: DEMO_ZOOMER_ID }),
|
||
});
|
||
const d = await r.json();
|
||
if (d.ok) {
|
||
setActiveMission(m);
|
||
showToast('<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> Uppdrag accepterat! Kr ' + m.reward_sek?.toLocaleString('sv-SE'));
|
||
allMapMissions = allMapMissions.filter(x => x.id !== m.id);
|
||
renderMapMarkers();
|
||
} else {
|
||
showToast('<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> ' + (d.error || 'Fel vid accepterande'));
|
||
}
|
||
} catch {
|
||
// Offline demo fallback
|
||
setActiveMission(m);
|
||
showToast('<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> Uppdrag accepterat (offline)! Kr ' + m.reward_sek?.toLocaleString('sv-SE'));
|
||
allMapMissions = allMapMissions.filter(x => x.id !== m.id);
|
||
renderMapMarkers();
|
||
}
|
||
}
|
||
|
||
//
|
||
// MINA UPPDRAG TAB
|
||
//
|
||
|
||
async function loadMinaMissions() {
|
||
renderActiveBanner();
|
||
const active = getActiveMission();
|
||
try {
|
||
const r = await fetch(API + '/mission/list?status=open', {
|
||
headers: { 'Authorization': 'Bearer ' + getDemoToken() }
|
||
});
|
||
const d = await r.json();
|
||
myMissions = d.missions || DEMO_MISSIONS;
|
||
} catch {
|
||
myMissions = DEMO_MISSIONS;
|
||
}
|
||
if (active) {
|
||
const alreadyInList = myMissions.find(m => m.id === active.id);
|
||
if (!alreadyInList) {
|
||
myMissions = [{ ...active, status: 'active' }, ...myMissions];
|
||
}
|
||
}
|
||
renderMyMissions();
|
||
}
|
||
|
||
function setMiMissionsFilter(f, btn) {
|
||
miMissionsFilter = f;
|
||
document.querySelectorAll('.segment .seg-btn').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
renderMyMissions();
|
||
}
|
||
|
||
function renderMyMissions() {
|
||
const active = getActiveMission();
|
||
let list = [...myMissions];
|
||
if (active && !list.find(m => m.id === active.id)) {
|
||
list = [{ ...active, status: 'active' }, ...list];
|
||
}
|
||
|
||
const lbl = document.getElementById('mina-section-lbl');
|
||
if (miMissionsFilter === 'active') {
|
||
list = list.filter(m => m.status === 'active' || (active && m.id === active.id));
|
||
lbl.textContent = 'Aktiva uppdrag';
|
||
} else if (miMissionsFilter === 'completed') {
|
||
list = list.filter(m => m.status === 'completed');
|
||
lbl.textContent = 'Slutförda uppdrag';
|
||
} else {
|
||
lbl.textContent = 'Alla uppdrag';
|
||
}
|
||
|
||
const el = document.getElementById('mina-list');
|
||
if (!list.length) {
|
||
el.innerHTML = `<div class="empty-state"><div class="empty-icon"><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></div><div class="empty-title">Inga uppdrag</div><div class="empty-sub">Öppna Karta-fliken och acceptera ett uppdrag för att komma igång!</div></div>`;
|
||
return;
|
||
}
|
||
el.innerHTML = list.map(m => {
|
||
const cfg = getCfg(m.mission_type || 'photo');
|
||
const isActive = active && active.id === m.id;
|
||
const statusCls = isActive ? 'b-active' : m.status === 'completed' ? 'b-completed' : m.status === 'cancelled' ? 'b-cancelled' : 'b-open';
|
||
const statusLbl = isActive ? 'Aktivt' : m.status === 'completed' ? 'Slutfört' : m.status === 'cancelled' ? 'Avbrutet' : 'Öppet';
|
||
const deadline = m.deadline ? new Date(m.deadline).toLocaleDateString('sv-SE',{day:'numeric',month:'short'}) : '';
|
||
return `<div class="mission-card" onclick="openMissionDetail('${m.id}')" style="${isActive ? 'border-color:#0A84FF;' : ''}">
|
||
<div class="mc-top">
|
||
<div class="mc-icon" style="background:${cfg.bg}">${cfg.icon}</div>
|
||
<div class="mc-body">
|
||
<div class="mc-title">${m.title}</div>
|
||
<div class="mc-loc"> ${m.location?.address || m.location?.city || 'Okänd plats'}${deadline ? ' · ⏰ '+deadline : ''}</div>
|
||
</div>
|
||
<div class="mc-reward">${(m.reward_sek||0).toLocaleString('sv-SE')}<small> kr</small></div>
|
||
</div>
|
||
<div class="mc-bottom">
|
||
<span class="badge ${statusCls}">${statusLbl}</span>
|
||
<div class="mc-cta">${isActive ? '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg> Pågår' : m.status === 'completed' ? '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8L6.5 11.5L13 4.5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Klart' : 'Detaljer'} ›</div>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
function renderActiveBanner() {
|
||
const m = getActiveMission();
|
||
const banner = document.getElementById('active-banner');
|
||
if (!m) { banner.classList.remove('show'); return; }
|
||
banner.classList.add('show');
|
||
document.getElementById('ab-title').textContent = m.title;
|
||
document.getElementById('ab-loc').textContent = ' ' + (m.location?.address || m.location?.city || '—') + ' · Kr ' + (m.reward_sek||0).toLocaleString('sv-SE');
|
||
}
|
||
|
||
function openMissionDetail(id) {
|
||
const m = myMissions.find(x => x.id === id) || (getActiveMission()?.id === id ? getActiveMission() : null);
|
||
if (!m) return;
|
||
if (m.status === 'open') {
|
||
if (getActiveMission()) { showToast('<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> Du har redan ett aktivt uppdrag'); return; }
|
||
if (confirm(`Acceptera "${m.title}" för ${(m.reward_sek||0).toLocaleString('sv-SE')} kr?`)) {
|
||
acceptMission(m);
|
||
}
|
||
} else if (m.status === 'active' || (getActiveMission()?.id === m.id)) {
|
||
openUploadSheet();
|
||
}
|
||
}
|
||
|
||
async function acceptMission(m) {
|
||
try {
|
||
const r = await fetch(API + '/zoomer/accept-mission', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + getDemoToken() },
|
||
body: JSON.stringify({ mission_id: m.id, zoomer_id: DEMO_ZOOMER_ID }),
|
||
});
|
||
const d = await r.json();
|
||
if (d.ok || true) {
|
||
setActiveMission(m);
|
||
showToast('<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> Uppdrag accepterat!');
|
||
loadMinaMissions();
|
||
}
|
||
} catch {
|
||
setActiveMission(m);
|
||
showToast('<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> Uppdrag accepterat!');
|
||
loadMinaMissions();
|
||
}
|
||
}
|
||
|
||
async function completeActiveMission() {
|
||
const m = getActiveMission();
|
||
if (!m) return;
|
||
const urls = uploadedMediaUrls.length > 0 ? uploadedMediaUrls : ['https://cdn.quixzoom.se/demo/media-proof-' + Date.now() + '.jpg'];
|
||
try {
|
||
const r = await fetch(API + '/mission/complete', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + getDemoToken() },
|
||
body: JSON.stringify({ mission_id: m.id, zoomer_id: DEMO_ZOOMER_ID, media_url: urls }),
|
||
});
|
||
const d = await r.json();
|
||
if (d.ok || d.pay_released) {
|
||
setActiveMission(null);
|
||
uploadedMediaUrls = [];
|
||
showToast(' Uppdrag slutfört! Kr ' + (m.reward_sek||0).toLocaleString('sv-SE') + ' utbetalas');
|
||
loadMinaMissions();
|
||
return;
|
||
}
|
||
} catch { /* fallthrough */ }
|
||
setActiveMission(null);
|
||
uploadedMediaUrls = [];
|
||
showToast(' Uppdrag slutfört! Betalning på väg.');
|
||
loadMinaMissions();
|
||
}
|
||
|
||
function cancelActiveMission() {
|
||
if (!confirm('Avbryt aktivt uppdrag?')) return;
|
||
setActiveMission(null);
|
||
showToast('Uppdrag avbrutet');
|
||
loadMinaMissions();
|
||
}
|
||
|
||
// Upload sheet
|
||
function openUploadSheet() {
|
||
document.getElementById('upload-sheet').classList.add('show');
|
||
document.getElementById('upload-overlay').style.display = 'block';
|
||
renderUploadPreviews();
|
||
}
|
||
function closeUploadSheet() {
|
||
document.getElementById('upload-sheet').classList.remove('show');
|
||
document.getElementById('upload-overlay').style.display = 'none';
|
||
}
|
||
function triggerFileInput() {
|
||
document.getElementById('file-input').click();
|
||
}
|
||
document.getElementById('file-input').addEventListener('change', function() {
|
||
const files = Array.from(this.files);
|
||
files.forEach(f => {
|
||
const url = URL.createObjectURL(f);
|
||
const fakeUploadUrl = 'https://cdn.quixzoom.se/uploads/' + Date.now() + '-' + f.name;
|
||
uploadedMediaUrls.push(fakeUploadUrl);
|
||
});
|
||
renderUploadPreviews();
|
||
if (files.length) showToast(' ' + files.length + ' fil(er) bifogade');
|
||
});
|
||
function renderUploadPreviews() {
|
||
const el = document.getElementById('upload-previews');
|
||
if (!uploadedMediaUrls.length) {
|
||
el.innerHTML = '<div class="preview-thumb" style="cursor:pointer" onclick="triggerFileInput()">+</div>';
|
||
return;
|
||
}
|
||
el.innerHTML = uploadedMediaUrls.map((u,i) =>
|
||
`<div class="preview-thumb" title="${u}"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg></div>`
|
||
).join('') + `<div class="preview-thumb" style="cursor:pointer;font-size:20px" onclick="triggerFileInput()">+</div>`;
|
||
}
|
||
function submitMedia() {
|
||
if (!uploadedMediaUrls.length) { showToast('<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> Välj minst en fil'); return; }
|
||
closeUploadSheet();
|
||
showToast(' Media uppladdad! Klicka Slutför när du är klar.');
|
||
}
|
||
|
||
//
|
||
// INTÄKTER TAB
|
||
//
|
||
|
||
async function loadEarnings() {
|
||
try {
|
||
const r = await fetch(API + '/earnings', { headers: { 'Authorization': 'Bearer ' + getDemoToken() } });
|
||
const d = await r.json();
|
||
if (d.ok) renderEarnings(d.earnings || d);
|
||
else renderEarnings(DEMO_EARNINGS);
|
||
} catch { renderEarnings(DEMO_EARNINGS); }
|
||
}
|
||
|
||
function renderEarnings(e) {
|
||
document.getElementById('e-avail').textContent = (e.available_sek || DEMO_EARNINGS.available_sek).toLocaleString('sv-SE');
|
||
document.getElementById('e-pending-lbl').textContent = 'kr ' + (e.pending_sek || DEMO_EARNINGS.pending_sek).toLocaleString('sv-SE') + ' väntar på verifiering';
|
||
document.getElementById('e-month').textContent = (e.this_month_sek || DEMO_EARNINGS.this_month_sek).toLocaleString('sv-SE') + ' kr';
|
||
document.getElementById('e-total-missions').textContent = e.missions_completed || DEMO_EARNINGS.missions_completed;
|
||
const avg = Math.round((e.total_earned_sek || DEMO_EARNINGS.total_earned_sek) / Math.max(e.missions_completed || DEMO_EARNINGS.missions_completed, 1));
|
||
document.getElementById('e-avg').textContent = avg.toLocaleString('sv-SE') + ' kr';
|
||
renderTransactions(DEMO_TX);
|
||
}
|
||
|
||
function renderTransactions(txs) {
|
||
document.getElementById('tx-list').innerHTML = txs.map(tx => `
|
||
<div class="payout-row">
|
||
<div class="pr-icon pri-${tx.type}">${tx.icon}</div>
|
||
<div class="pr-body">
|
||
<div class="pr-title">${tx.title}</div>
|
||
<div class="pr-sub">${tx.date}</div>
|
||
</div>
|
||
<div class="pr-amount ${tx.type}">${tx.type==='credit'?'+':tx.type==='payout'?'↑':'~'}${tx.amount.toLocaleString('sv-SE')} kr</div>
|
||
</div>`).join('');
|
||
}
|
||
|
||
//
|
||
// LEADERBOARD TAB
|
||
//
|
||
|
||
async function loadLeaderboard() {
|
||
try {
|
||
const r = await fetch(API + '/leaderboard', { headers: { 'Authorization': 'Bearer ' + getDemoToken() } });
|
||
lbData = await r.json();
|
||
} catch { lbData = DEMO_LEADERBOARD; }
|
||
renderLbTotals();
|
||
renderLbZoomers();
|
||
renderLbCities();
|
||
}
|
||
|
||
function setLbView(view, btn) {
|
||
lbView = view;
|
||
document.querySelectorAll('.segment .seg-btn').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
document.getElementById('lb-zoomers-list').style.display = view === 'zoomers' ? 'block' : 'none';
|
||
document.getElementById('lb-cities-list').style.display = view === 'cities' ? 'block' : 'none';
|
||
}
|
||
|
||
function renderLbTotals() {
|
||
if (!lbData?.platform_totals) return;
|
||
const t = lbData.platform_totals;
|
||
document.getElementById('lb-totals').innerHTML = `
|
||
<div class="lb-total-chip"> ${t.missions_completed?.toLocaleString('sv-SE')} uppdrag</div>
|
||
<div class="lb-total-chip"> ${(t.total_paid_out_sek/1000).toFixed(0)}k kr utbetalt</div>
|
||
<div class="lb-total-chip"> ${t.registered_zoomers} zoomers</div>
|
||
<div class="lb-total-chip"> ${t.cities_live} städer</div>`;
|
||
}
|
||
|
||
const ZOOMER_AVATARS = ['','','','','','','',''];
|
||
function renderLbZoomers() {
|
||
const list = (lbData?.top_zoomers || DEMO_LEADERBOARD.top_zoomers);
|
||
document.getElementById('lb-zoomers-list').innerHTML = list.map((z,i) => {
|
||
const rankCls = i===0?'rank-1':i===1?'rank-2':i===2?'rank-3':'rank-n';
|
||
const rankTxt = i===0?'':i===1?'':i===2?'':'#'+(i+1);
|
||
const bg = i===0?'rgba(255,215,0,0.15)':i===1?'rgba(192,192,192,0.12)':i===2?'rgba(205,127,50,0.12)':'var(--fill-tertiary)';
|
||
return `<div class="lb-row">
|
||
<div class="lb-rank ${rankCls}">${rankTxt}</div>
|
||
<div class="lb-avatar" style="background:${bg}">${ZOOMER_AVATARS[i%ZOOMER_AVATARS.length]}</div>
|
||
<div class="lb-body">
|
||
<div class="lb-name">${z.name}</div>
|
||
<div class="lb-meta"> ${z.city} · ${z.rating} · ${z.skill_level}</div>
|
||
</div>
|
||
<div class="lb-right">
|
||
<div class="lb-missions" style="color:${i===0?'#FFD700':i===1?'#C0C0C0':i===2?'#CD7F32':'var(--label-primary)'}">${z.missions_completed}</div>
|
||
<div class="lb-earnings">${(z.earnings_sek/1000).toFixed(1)}k kr</div>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
const CITY_ICONS = { Stockholm:'', Göteborg:'', Malmö:'', Uppsala:'', Västerås:'<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>' };
|
||
function renderLbCities() {
|
||
const list = (lbData?.top_cities || DEMO_LEADERBOARD.top_cities);
|
||
const maxM = list[0]?.missions_completed || 1;
|
||
document.getElementById('lb-cities-list').innerHTML = list.map((c,i) => {
|
||
const pct = Math.round((c.missions_completed / maxM) * 100);
|
||
return `<div class="city-row">
|
||
<div class="city-rank" style="color:${i===0?'#FFD700':i===1?'#C0C0C0':i===2?'#CD7F32':'var(--label-tertiary)'}">${i===0?'':i===1?'':i===2?'':'#'+(i+1)}</div>
|
||
<div class="city-icon">${CITY_ICONS[c.city]||''}</div>
|
||
<div class="city-body">
|
||
<div class="city-name">${c.city}</div>
|
||
<div style="margin-top:6px;max-width:160px;">
|
||
<div class="city-bar-track"><div class="city-bar-fill" style="width:${pct}%"></div></div>
|
||
</div>
|
||
<div class="city-meta">${c.active_zoomers} zoomers · ${c.avg_per_mission?.toLocaleString('sv-SE')} kr/snitt</div>
|
||
</div>
|
||
<div class="city-count">${c.missions_completed}</div>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
// Helpers
|
||
function getDemoToken() {
|
||
const payload = btoa(JSON.stringify({ sub: DEMO_ZOOMER_ID, email: 'erik@demo.se', iat: Date.now() }));
|
||
return 'eyJhbGciOiJIUzI1NiJ9.' + payload + '.demo-sig';
|
||
}
|
||
|
||
// Demo data
|
||
const DEMO_MISSIONS = [
|
||
{ id:'qx-m-001', title:'Fastighetsfilm — Lidingö villa', description:'Professionell fastighetsfilm (4K) av villa.', location:{city:'Stockholm',address:'Lidingövägen 12',lat:59.3664,lon:18.1499}, reward_sek:2800, deadline:'2026-06-05T17:00:00Z', mission_type:'video', skill_required:'professional', status:'open' },
|
||
{ id:'qx-m-002', title:'Drönarbild — Kista Science City', description:'Aerial 4K-film av kontorspark. Tillstånd finns.', location:{city:'Stockholm',address:'Kistagången 20',lat:59.4044,lon:17.9491}, reward_sek:3200, deadline:'2026-06-08T16:00:00Z', mission_type:'drone', skill_required:'drone', status:'open' },
|
||
{ id:'qx-m-003', title:'Event-foto — Berns Salonger', description:'Eventfotografering för Tech Gala, ca 4h.', location:{city:'Stockholm',address:'Berzelii Park',lat:59.3336,lon:18.0741}, reward_sek:5500, deadline:'2026-06-12T23:00:00Z', mission_type:'photo', skill_required:'professional', status:'open' },
|
||
{ id:'qx-m-004', title:'Produktfoto — e-handel Södermalm', description:'80 produkter, studio tillhandahålls.', location:{city:'Stockholm',address:'Hornsgatan 64',lat:59.3157,lon:18.0532}, reward_sek:4200, deadline:'2026-06-03T15:00:00Z', mission_type:'photo', skill_required:'basic', status:'open' },
|
||
{ id:'qx-m-005', title:'Hotellfilm — Clarion Post Göteborg', description:'Marknadsföringsfilm av hotell.', location:{city:'Göteborg',address:'Drottningtorget 10',lat:57.7072,lon:11.9735}, reward_sek:6000, deadline:'2026-06-10T17:00:00Z', mission_type:'video', skill_required:'professional', status:'open' },
|
||
{ id:'qx-m-007', title:'Restaurangfoto — Vollmers Malmö', description:'Food & interior photography.', location:{city:'Malmö',address:'Tegelgårdsgatan 5',lat:55.5977,lon:13.0041}, reward_sek:4800, deadline:'2026-06-07T11:00:00Z', mission_type:'photo', skill_required:'professional', status:'open' },
|
||
{ id:'qx-m-008', title:'Drönarbild — Malmö Live', description:'Aerial film av konserthuset.', location:{city:'Malmö',address:'Lars Krantz Plats 1',lat:55.6060,lon:12.9991}, reward_sek:3600, deadline:'2026-06-14T16:00:00Z', mission_type:'drone', skill_required:'drone', status:'open' },
|
||
{ id:'qx-m-009', title:'Campus-film — Uppsala Universitet', description:'Rekryteringsfilm för campus.', location:{city:'Uppsala',address:'Universitetshuset',lat:59.8569,lon:17.6335}, reward_sek:5200, deadline:'2026-06-15T17:00:00Z', mission_type:'video', skill_required:'professional', status:'open' },
|
||
];
|
||
|
||
const DEMO_EARNINGS = { available_sek: 0, pending_sek: 0, this_month_sek: 0, missions_completed: 0, total_earned_sek: 0 };
|
||
|
||
const DEMO_TX = [
|
||
{ type:'credit', icon:'', title:'Drönarbild — Gamla Stan', date:'Idag · automatisk utbetalning', amount:2800 },
|
||
{ type:'credit', icon:'', title:'Butiksfilm — Nordstan', date:'Igår 16:40', amount:2400 },
|
||
{ type:'pending', icon:'⏳', title:'Hotellfilm Grand (granskas)', date:'2 dagar sedan · kunden granskar', amount:7200 },
|
||
{ type:'payout', icon:'↑', title:'Utbetalning via Stripe', date:'19 maj 2026', amount:9800 },
|
||
{ type:'credit', icon:'', title:'Event-foto Berns Salonger', date:'17 maj 2026', amount:5500 },
|
||
{ type:'credit', icon:'', title:'Campus-film Uppsala', date:'15 maj 2026', amount:5200 },
|
||
{ type:'credit', icon:'', title:'Restaurangfoto Vollmers', date:'12 maj 2026', amount:4800 },
|
||
{ type:'payout', icon:'↑', title:'Utbetalning via Stripe', date:'1 maj 2026', amount:14400 },
|
||
];
|
||
|
||
const DEMO_LEADERBOARD = {
|
||
platform_totals: { missions_completed: 0, total_paid_out_sek: 0, registered_zoomers: 0, cities_live: 0 },
|
||
top_zoomers: [],
|
||
top_cities: [],
|
||
};
|
||
|
||
// Init
|
||
updateMinaDot();
|
||
initMap();
|
||
</script>
|
||
</body>
|
||
</html>
|