2026-06-29 16:24:48 +00:00
<!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 > Zoomer App — quiXzoom< / title >
< meta name = "description" content = "Find missions near you, capture photos, and earn money with the quiXzoom Zoomer App. Flexible field work on your terms." >
< link rel = "canonical" href = "https://www.quixzoom.com/zoomer-app.html" >
< link rel = "stylesheet" href = "/_design-system/apple-native/tokens.css" >
< link rel = "stylesheet" href = "/vendor/leaflet.css" >
< script src = "/vendor/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 : 100 dvh ;
padding-top : env ( safe - area - inset - top , 44 px ) ;
padding-bottom : calc ( env ( safe - area - inset - bottom , 34 px ) + 83 px ) ;
-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 : 44 px ; padding : 0 16 px ;
background : var ( - - navbar - bg , rgba ( 28 , 28 , 30 , 0.94 ) ) ;
backdrop-filter : saturate ( 180 % ) blur ( 20 px ) ;
-webkit- backdrop-filter : saturate ( 180 % ) blur ( 20 px ) ;
border-bottom : 0.5 px solid var ( - - separator ) ;
}
. nav-title {
position : absolute ; left : 50 % ; transform : translateX ( -50 % ) ;
font-family : var ( - - font - display ) ; font-size : 17 px ; font-weight : 600 ;
letter-spacing : -0.41 px ;
}
2026-07-07 07:11:50 +00:00
. nav-bar . nav-btn-sm : last-child {
margin-left : auto ;
}
2026-06-29 16:24:48 +00:00
. nav-logo {
background : linear-gradient ( 135 deg , #0A84FF 0 % , #5E5CE6 100 % ) ;
-webkit- background-clip : text ; -webkit- text-fill-color : transparent ; background-clip : text ;
font-size : 18 px ; font-weight : 800 ;
}
. nav-btn {
background : none ; border : none ; color : var ( - - ios - blue ) ;
font-family : var ( - - font - text ) ; font-size : 17 px ; cursor : pointer ; padding : 4 px 0 ;
}
. nav-btn-sm {
background : none ; border : none ; cursor : pointer ;
font-size : 20 px ; padding : 4 px ;
}
/* ── 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 ( 30 px ) ;
-webkit- backdrop-filter : saturate ( 180 % ) blur ( 30 px ) ;
border-top : 0.5 px solid var ( - - separator ) ;
padding-bottom : env ( safe - area - inset - bottom , 0 px ) ;
}
. tab-item {
flex : 1 ; display : flex ; flex-direction : column ; align-items : center ;
justify-content : center ; padding : 8 px 4 px 4 px ;
cursor : pointer ; text-decoration : none ; border : none ;
background : none ; transition : opacity 0.15 s ;
}
. tab-item : active { opacity : 0.6 ; }
. tab-icon { font-size : 24 px ; line-height : 1 ; }
. tab-label { font-size : 10 px ; font-weight : 500 ; color : var ( - - label - tertiary ) ; margin-top : 2 px ; letter-spacing : -0.1 px ; }
. tab-item . active . tab-label { color : #0A84FF ; }
. tab-badge-wrap { position : relative ; display : inline-block ; }
. tab-dot {
position : absolute ; top : -2 px ; right : -4 px ;
width : 8 px ; height : 8 px ; border-radius : 50 % ;
background : var ( - - ios - red ) ; border : 1.5 px solid var ( - - background - primary ) ;
display : none ;
}
. tab-dot . show { display : block ; }
/* ── Segment ── */
. segment-wrap { padding : 8 px 16 px ; }
. segment { display : flex ; background : var ( - - fill - tertiary ) ; border-radius : 9 px ; padding : 2 px ; }
. seg-btn {
flex : 1 ; padding : 6 px 0 ; background : none ; border : none ;
font-family : var ( - - font - text ) ; font-size : 13 px ; font-weight : 500 ;
color : var ( - - label - secondary ) ; cursor : pointer ; border-radius : 7 px ; transition : all 0.2 s ;
}
. seg-btn . active {
background : var ( - - background - secondary ) ; color : var ( - - label - primary ) ; font-weight : 600 ;
box-shadow : 0 1 px 3 px rgba ( 0 , 0 , 0 , 0.3 ) ;
}
/* ── Section header ── */
. section-hdr {
padding : 20 px 16 px 8 px ;
font-size : 13 px ; font-weight : 500 ;
color : var ( - - label - secondary ) ; text-transform : uppercase ; letter-spacing : 0.5 px ;
}
/* ══════════════════════════════════
MAP TAB — Karta
══════════════════════════════════ */
# map-full {
width : 100 % ; height : calc ( 100 dvh - env ( safe - area - inset - top , 44 px ) - 44 px - 83 px - env ( safe - area - inset - bottom , 34 px ) ) ;
min-height : 400 px ;
}
. map-filter-bar {
padding : 8 px 12 px ; display : flex ; gap : 8 px ; overflow-x : auto ;
scrollbar-width : none ; -webkit- overflow-scrolling : touch ;
}
. map-filter-bar :: -webkit-scrollbar { display : none ; }
. filter-chip {
flex-shrink : 0 ; padding : 6 px 14 px ; border-radius : 20 px ; border : 1.5 px solid var ( - - separator ) ;
background : var ( - - fill - tertiary ) ; color : var ( - - label - secondary ) ;
font-size : 13 px ; font-weight : 500 ; cursor : pointer ; white-space : nowrap ; transition : all 0.2 s ;
}
. filter-chip . active { border-color : #0A84FF ; background : rgba ( 10 , 132 , 255 , 0.12 ) ; color : #0A84FF ; }
. map-legend {
position : absolute ; bottom : calc ( 90 px + env ( safe - area - inset - bottom , 34 px ) + 8 px ) ;
right : 12 px ; z-index : 500 ;
background : rgba ( 28 , 28 , 30 , 0.88 ) ; backdrop-filter : blur ( 20 px ) ;
border-radius : 12 px ; padding : 10 px 12 px ;
border : 0.5 px solid var ( - - separator ) ;
font-size : 11 px ;
}
. legend-row { display : flex ; align-items : center ; gap : 6 px ; padding : 2 px 0 ; color : var ( - - label - secondary ) ; }
. legend-dot { width : 10 px ; height : 10 px ; border-radius : 50 % ; flex-shrink : 0 ; }
/* Mission bottom-sheet */
. mission-sheet {
position : fixed ; left : 0 ; right : 0 ; bottom : calc ( 83 px + env ( safe - area - inset - bottom , 34 px ) ) ;
z-index : 400 ; padding : 0 12 px 12 px ;
transform : translateY ( 200 px ) ; opacity : 0 ;
transition : all 0.35 s 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 ( 40 px ) ;
border-radius : 18 px ; padding : 16 px ;
border : 0.5 px solid var ( - - separator ) ;
box-shadow : 0 8 px 32 px rgba ( 0 , 0 , 0 , 0.5 ) ;
}
. sheet-header { display : flex ; align-items : flex-start ; justify-content : space-between ; margin-bottom : 10 px ; }
. sheet-title { font-size : 17 px ; font-weight : 700 ; letter-spacing : -0.4 px ; flex : 1 ; padding-right : 8 px ; }
. sheet-close { background : var ( - - fill - secondary ) ; border : none ; border-radius : 50 % ; width : 28 px ; height : 28 px ; cursor : pointer ; font-size : 14 px ; color : var ( - - label - secondary ) ; flex-shrink : 0 ; }
. sheet-meta { display : flex ; gap : 8 px ; flex-wrap : wrap ; margin-bottom : 12 px ; }
. sheet-chip { padding : 4 px 10 px ; border-radius : 20 px ; font-size : 12 px ; 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 : 14 px ; color : var ( - - label - secondary ) ; line-height : 1.5 ; margin-bottom : 14 px ; }
. sheet-actions { display : flex ; gap : 10 px ; }
. sheet-btn {
flex : 1 ; padding : 13 px ; border-radius : 12 px ; border : none ;
font-family : var ( - - font - text ) ; font-size : 15 px ; font-weight : 600 ;
cursor : pointer ; transition : opacity 0.15 s ;
}
. 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 16 px ; display : flex ; flex-direction : column ; gap : 12 px ; }
. mission-card {
background : var ( - - background - secondary ) ; border-radius : 16 px ; overflow : hidden ;
border : 0.5 px solid var ( - - separator ) ;
cursor : pointer ; transition : transform 0.12 s , opacity 0.12 s ;
}
. mission-card : active { transform : scale ( 0.98 ) ; opacity : 0.85 ; }
. mc-top {
display : flex ; align-items : flex-start ; gap : 12 px ; padding : 14 px 14 px 10 px ;
}
. mc-icon {
width : 46 px ; height : 46 px ; border-radius : 12 px ;
display : flex ; align-items : center ; justify-content : center ;
font-size : 22 px ; flex-shrink : 0 ;
}
. mc-body { flex : 1 ; min-width : 0 ; }
. mc-title {
font-size : 16 px ; font-weight : 600 ; letter-spacing : -0.32 px ;
white-space : nowrap ; overflow : hidden ; text-overflow : ellipsis ;
}
. mc-loc { font-size : 13 px ; color : var ( - - label - secondary ) ; margin-top : 2 px ; }
. mc-reward { font-size : 22 px ; font-weight : 700 ; font-family : var ( - - font - display ) ; color : var ( - - ios - green ) ; flex-shrink : 0 ; }
. mc-reward small { font-size : 12 px ; font-weight : 500 ; color : var ( - - label - secondary ) ; }
. mc-bottom {
display : flex ; align-items : center ; justify-content : space-between ;
padding : 10 px 14 px 14 px ; border-top : 0.5 px solid var ( - - separator ) ;
}
. badge {
padding : 3 px 9 px ; border-radius : 20 px ; font-size : 11 px ; font-weight : 700 ;
text-transform : uppercase ; letter-spacing : 0.3 px ;
}
. 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 : 14 px ; font-weight : 600 ; color : #0A84FF ; display : flex ; align-items : center ; gap : 4 px ; }
/* Active mission banner */
. active-banner {
margin : 12 px 16 px ;
background : linear-gradient ( 135 deg , rgba ( 10 , 132 , 255 , 0.18 ) , rgba ( 94 , 92 , 230 , 0.18 ) ) ;
border : 1 px solid rgba ( 10 , 132 , 255 , 0.35 ) ;
border-radius : 16 px ; padding : 16 px ; display : none ;
}
. active-banner . show { display : block ; }
. ab-label { font-size : 11 px ; font-weight : 700 ; color : #0A84FF ; text-transform : uppercase ; letter-spacing : 0.5 px ; margin-bottom : 4 px ; }
. ab-title { font-size : 17 px ; font-weight : 700 ; margin-bottom : 4 px ; }
. ab-loc { font-size : 13 px ; color : var ( - - label - secondary ) ; margin-bottom : 12 px ; }
. ab-actions { display : flex ; gap : 8 px ; }
. ab-btn {
flex : 1 ; padding : 11 px 8 px ; border-radius : 11 px ; border : none ;
font-family : var ( - - font - text ) ; font-size : 14 px ; font-weight : 600 ;
cursor : pointer ; transition : opacity 0.15 s ;
}
. 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 : 72 px 32 px ; text-align : center ;
display : flex ; flex-direction : column ; align-items : center ; gap : 12 px ;
}
. empty-icon { font-size : 60 px ; }
. empty-title { font-size : 20 px ; font-weight : 700 ; }
. empty-sub { font-size : 15 px ; color : var ( - - label - secondary ) ; line-height : 1.5 ; }
/* ══════════════════════════════════
INTÄKTER TAB — Earnings
══════════════════════════════════ */
. earnings-hero {
background : linear-gradient ( 160 deg , #061628 0 % , #010b1a 50 % , #030e21 100 % ) ;
padding : 28 px 24 px 32 px ; 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 : 12 px ; font-weight : 600 ; color : rgba ( 255 , 255 , 255 , 0.75 ) ;
text-transform : uppercase ; letter-spacing : 1 px ; margin-bottom : 6 px ;
}
. eh-amount {
font-family : var ( - - font - display ) ; font-size : 60 px ; font-weight : 800 ;
color : #fff ; letter-spacing : -2.5 px ; line-height : 1 ; position : relative ; z-index : 1 ;
}
. eh-currency { font-size : 28 px ; font-weight : 500 ; color : rgba ( 255 , 255 , 255 , 0.75 ) ; }
. eh-sub { font-size : 13 px ; color : rgba ( 255 , 255 , 255 , 0.75 ) ; margin-top : 6 px ; position : relative ; z-index : 1 ; }
. eh-actions { display : flex ; gap : 10 px ; margin-top : 20 px ; position : relative ; z-index : 1 ; }
. eh-btn {
flex : 1 ; display : flex ; flex-direction : column ; align-items : center ; gap : 4 px ;
background : rgba ( 255 , 255 , 255 , 0.1 ) ; border : none ; border-radius : 14 px ;
padding : 13 px 8 px ; cursor : pointer ; transition : background 0.15 s ; font-family : var ( - - font - text ) ;
}
. eh-btn : active { background : rgba ( 255 , 255 , 255 , 0.18 ) ; }
. eh-btn-icon { font-size : 22 px ; }
. eh-btn-label { font-size : 12 px ; font-weight : 600 ; color : rgba ( 255 , 255 , 255 , 0.75 ) ; }
. earnings-stats {
display : grid ; grid-template-columns : repeat ( 3 , 1 fr ) ;
background : var ( - - background - secondary ) ; border-bottom : 0.5 px solid var ( - - separator ) ;
}
. e-stat { padding : 16 px 8 px ; text-align : center ; border-right : 0.5 px solid var ( - - separator ) ; }
. e-stat : last-child { border-right : none ; }
. e-stat-val { font-size : 20 px ; font-weight : 700 ; font-family : var ( - - font - display ) ; }
. e-stat-lbl { font-size : 11 px ; color : var ( - - label - secondary ) ; margin-top : 2 px ; }
/* Payout CTA */
. payout-card {
margin : 16 px ; background : var ( - - background - secondary ) ;
border-radius : 16 px ; overflow : hidden ; border : 0.5 px solid var ( - - separator ) ;
}
. payout-header {
display : flex ; align-items : center ; justify-content : space-between ;
padding : 14 px 16 px ; border-bottom : 0.5 px solid var ( - - separator ) ;
}
. payout-title { font-size : 16 px ; font-weight : 600 ; }
. payout-badge { padding : 4 px 10 px ; border-radius : 20 px ; font-size : 12 px ; font-weight : 700 ; }
. pb-pending { background : rgba ( 255 , 149 , 0 , 0.12 ) ; color : var ( - - ios - orange ) ; }
. payout-row {
display : flex ; align-items : center ; gap : 12 px ;
padding : 14 px 16 px ; border-bottom : 0.5 px solid var ( - - separator ) ;
}
. payout-row : last-child { border-bottom : none ; }
. pr-icon {
width : 40 px ; height : 40 px ; border-radius : 10 px ;
display : flex ; align-items : center ; justify-content : center ;
font-size : 20 px ; 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 : 15 px ; font-weight : 600 ; }
. pr-sub { font-size : 13 px ; color : var ( - - label - secondary ) ; margin-top : 1 px ; }
. pr-amount { font-size : 16 px ; 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 : 20 px 16 px 12 px ;
background : linear-gradient ( 135 deg , rgba ( 10 , 132 , 255 , 0.1 ) 0 % , rgba ( 94 , 92 , 230 , 0.1 ) 100 % ) ;
border-bottom : 0.5 px solid var ( - - separator ) ;
}
. lb-hero-title { font-size : 28 px ; font-weight : 800 ; letter-spacing : -1 px ; }
. lb-hero-sub { font-size : 14 px ; color : var ( - - label - secondary ) ; margin-top : 4 px ; }
. lb-totals {
display : flex ; gap : 16 px ; margin-top : 14 px ; flex-wrap : wrap ;
}
. lb-total-chip {
display : flex ; align-items : center ; gap : 6 px ;
background : var ( - - fill - secondary ) ; border-radius : 20 px ;
padding : 6 px 12 px ; font-size : 13 px ; font-weight : 600 ;
}
. lb-row {
display : flex ; align-items : center ; gap : 12 px ;
padding : 14 px 16 px ; border-bottom : 0.5 px solid var ( - - separator ) ;
cursor : pointer ; transition : background 0.1 s ;
}
. lb-row : active { background : var ( - - fill - quaternary ) ; }
. lb-rank {
font-size : 18 px ; font-weight : 800 ; font-family : var ( - - font - display ) ;
min-width : 28 px ; 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 : 44 px ; height : 44 px ; border-radius : 50 % ;
display : flex ; align-items : center ; justify-content : center ;
font-size : 22 px ; flex-shrink : 0 ;
}
. lb-body { flex : 1 ; min-width : 0 ; }
. lb-name { font-size : 16 px ; font-weight : 600 ; letter-spacing : -0.3 px ; }
. lb-meta { font-size : 13 px ; color : var ( - - label - secondary ) ; margin-top : 2 px ; }
. lb-right { text-align : right ; flex-shrink : 0 ; }
. lb-missions { font-size : 20 px ; font-weight : 800 ; font-family : var ( - - font - display ) ; }
. lb-earnings { font-size : 12 px ; color : var ( - - label - secondary ) ; margin-top : 1 px ; }
. city-row {
display : flex ; align-items : center ; gap : 14 px ;
padding : 14 px 16 px ; border-bottom : 0.5 px solid var ( - - separator ) ;
}
. city-rank {
font-size : 20 px ; font-weight : 800 ; font-family : var ( - - font - display ) ;
min-width : 30 px ; text-align : center ; flex-shrink : 0 ;
}
. city-icon { font-size : 28 px ; flex-shrink : 0 ; }
. city-body { flex : 1 ; min-width : 0 ; }
. city-name { font-size : 16 px ; font-weight : 700 ; }
. city-meta { font-size : 13 px ; color : var ( - - label - secondary ) ; margin-top : 1 px ; }
. city-bar-wrap { flex : 1 ; }
. city-bar-track { height : 5 px ; background : var ( - - fill - secondary ) ; border-radius : 3 px ; overflow : hidden ; }
. city-bar-fill { height : 100 % ; border-radius : 3 px ; background : linear-gradient ( 90 deg , #0A84FF , #5E5CE6 ) ; }
. city-count { font-size : 18 px ; font-weight : 700 ; font-family : var ( - - font - display ) ; text-align : right ; flex-shrink : 0 ; }
/* ══════════════════════════════════
SHARED UTILITIES
══════════════════════════════════ */
. toast {
position : fixed ; bottom : calc ( 90 px + env ( safe - area - inset - bottom , 0 px ) ) ;
left : 50 % ; transform : translateX ( -50 % ) translateY ( 20 px ) ;
background : rgba ( 50 , 50 , 52 , 0.96 ) ; color : #fff ;
padding : 10 px 20 px ; border-radius : 20 px ; font-size : 14 px ; font-weight : 500 ;
opacity : 0 ; transition : all 0.3 s ; 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 : 48 px ; }
. spinner {
width : 28 px ; height : 28 px ; border : 3 px solid var ( - - fill - secondary ) ;
border-top-color : #0A84FF ; border-radius : 50 % ; animation : spin 0.8 s linear infinite ;
}
@ keyframes spin { to { transform : rotate ( 360 deg ) ; } }
. pull-hint { text-align : center ; padding : 12 px ; font-size : 12 px ; 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 ( 40 px ) ;
border-top-left-radius : 20 px ; border-top-right-radius : 20 px ;
padding : 20 px 20 px calc ( env ( safe - area - inset - bottom , 34 px ) + 20 px ) ;
transform : translateY ( 100 % ) ; transition : transform 0.35 s cubic-bezier ( 0.32 , 0.72 , 0 , 1 ) ;
border-top : 0.5 px solid var ( - - separator ) ;
}
. upload-sheet . show { transform : translateY ( 0 ) ; }
. upload-handle { width : 36 px ; height : 4 px ; border-radius : 2 px ; background : var ( - - fill - primary ) ; margin : 0 auto 20 px ; }
. upload-title { font-size : 18 px ; font-weight : 700 ; margin-bottom : 4 px ; }
. upload-sub { font-size : 14 px ; color : var ( - - label - secondary ) ; margin-bottom : 20 px ; }
. upload-previews { display : flex ; gap : 10 px ; flex-wrap : wrap ; margin-bottom : 16 px ; }
. preview-thumb {
width : 80 px ; height : 80 px ; border-radius : 10 px ; object-fit : cover ;
border : 0.5 px solid var ( - - separator ) ; background : var ( - - fill - secondary ) ;
display : flex ; align-items : center ; justify-content : center ; font-size : 28 px ;
}
. upload-actions { display : flex ; gap : 10 px ; }
. upload-btn {
flex : 1 ; padding : 14 px ; border-radius : 12 px ; border : none ;
font-family : var ( - - font - text ) ; font-size : 15 px ; font-weight : 600 ;
cursor : pointer ; transition : opacity 0.15 s ;
}
. upload-btn : active { opacity : 0.7 ; }
< / 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 ; }
< / style >
< script src = "/quixzoom/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" >
< / head >
< body >
<!-- H1 for SEO -->
< h1 style = "position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden;" > quiXzoom Zoomer App — Find Missions & Earn Money< / h1 >
<!-- ══════════════════════════════ KARTA TAB ══════════════════════════════ -->
< div id = "page-karta" class = "tab-page active" >
< 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 >
2026-07-07 07:11:50 +00:00
< button class = "sheet-close" onclick = "closeSheet()" > × < / button >
2026-06-29 16:24:48 +00:00
< / 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 = "nav-bar" >
< span class = "nav-title" > Mina uppdrag< / span >
2026-07-07 07:11:50 +00:00
< button class = "nav-btn-sm" onclick = "refreshMissions()" title = "Uppdatera" style = "margin-left:auto;z-index:10;position:relative;" >
< svg width = "20" height = "20" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" stroke-linejoin = "round" style = "color:var(--ios-blue)" >
< polyline points = "23 4 23 10 17 10" > < / polyline >
< polyline points = "1 20 1 14 7 14" > < / polyline >
< path d = "M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" > < / path >
< / svg >
< / button >
2026-06-29 16:24:48 +00:00
< / 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" >
2026-07-07 07:11:50 +00:00
< div class = "ab-label" > AKTIVT UPPDRAG< / div >
2026-06-29 16:24:48 +00:00
< div class = "ab-title" id = "ab-title" > —< / div >
< div class = "ab-loc" id = "ab-loc" > —< / div >
< div class = "ab-actions" >
2026-07-07 07:11:50 +00:00
< button class = "ab-btn btn-cam" onclick = "openUploadSheet()" > Ladda upp media< / button >
< button class = "ab-btn btn-done" onclick = "completeActiveMission()" > Slutför< / button >
< button class = "ab-btn btn-cancel-sm" onclick = "cancelActiveMission()" > × < / button >
2026-06-29 16:24:48 +00:00
< / 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 = "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 >
2026-07-07 07:11:50 +00:00
< button class = "eh-btn" onclick = "showToast('Exporterar årssammanfattning...')" >
< span class = "eh-btn-icon" > Skatt< / span > < span class = "eh-btn-label" > Skatt< / span >
2026-06-29 16:24:48 +00:00
< / 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" >
2026-07-07 07:11:50 +00:00
< div class = "pr-icon pri-credit" > +< / div >
2026-06-29 16:24:48 +00:00
< 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 = "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" >
2026-07-07 07:11:50 +00:00
< div class = "lb-total-chip" > Laddar...< / div >
2026-06-29 16:24:48 +00:00
< / 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 = "tab-bar" >
< button class = "tab-item active" id = "tab-karta" onclick = "switchTab('karta')" >
2026-07-07 07:11:50 +00:00
< span class = "tab-icon" > < svg width = "24" height = "24" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M1 6l7-4 6 4 7-4v17l-7 4-6-4-7 4z" / > < / svg > < / span >
2026-06-29 16:24:48 +00:00
< span class = "tab-label" > Karta< / span >
< / button >
< button class = "tab-item" id = "tab-mina" onclick = "switchTab('mina')" >
< span class = "tab-badge-wrap" >
2026-07-07 07:11:50 +00:00
< span class = "tab-icon" > < svg width = "24" height = "24" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M9 11l3 3L22 4" / > < path d = "M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11" / > < / svg > < / span >
2026-06-29 16:24:48 +00:00
< 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')" >
2026-07-07 07:11:50 +00:00
< span class = "tab-icon" > < svg width = "24" height = "24" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < circle cx = "12" cy = "12" r = "10" / > < path d = "M12 6v12M8 10h8M8 14h8" / > < / svg > < / span >
2026-06-29 16:24:48 +00:00
< span class = "tab-label" > Intäkter< / span >
< / button >
< button class = "tab-item" id = "tab-leaderboard" onclick = "switchTab('leaderboard')" >
2026-07-07 07:11:50 +00:00
< span class = "tab-icon" > < svg width = "24" height = "24" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M6 9H4.5a2.5 2.5 0 010-5H6" / > < path d = "M18 9h1.5a2.5 2.5 0 000-5H18" / > < path d = "M4 22h16" / > < path d = "M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22" / > < path d = "M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22" / > < path d = "M18 2H6v7a6 6 0 006 6 6 6 0 006-6V2z" / > < / svg > < / span >
2026-06-29 16:24:48 +00:00
< 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" >
2026-07-07 07:11:50 +00:00
< button class = "upload-btn" style = "background:var(--fill-secondary);color:var(--label-primary)" onclick = "triggerFileInput()" > Välj fil< / button >
2026-06-29 16:24:48 +00:00
< 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 = {
2026-07-07 07:11:50 +00:00
photo : { icon : '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v12M8 10h8M8 14h8"/></svg>' , color : '#0A84FF' , bg : 'rgba(10,132,255,0.2)' } ,
video : { icon : 'V' , color : '#5E5CE6' , bg : 'rgba(94,92,230,0.2)' } ,
drone : { icon : 'D' , color : '#FF9500' , bg : 'rgba(255,149,0,0.2)' } ,
survey : { icon : 'S' , color : '#34C759' , bg : 'rgba(52,199,89,0.2)' } ,
audit : { icon : 'A' , color : '#FF2D55' , bg : 'rgba(255,45,85,0.2)' } ,
2026-06-29 16:24:48 +00:00
} ;
2026-07-07 07:11:50 +00:00
function getCfg ( t ) { return TYPE _CFG [ t ] || { icon : '+' , color : '#8E8E93' , bg : 'rgba(142,142,147,0.2)' } ; }
2026-06-29 16:24:48 +00:00
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:50%;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 ( ) ;
2026-07-07 07:11:50 +00:00
showToast ( 'Uppdaterar karta...' ) ;
}
function refreshMissions ( ) {
loadMinaMissions ( ) ;
showToast ( 'Uppdaterar uppdrag...' ) ;
2026-06-29 16:24:48 +00:00
}
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>
2026-07-07 07:11:50 +00:00
<span class="sheet-chip chip-dead">+ ${ deadline } </span> ` ;
2026-06-29 16:24:48 +00:00
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 ) ;
2026-07-07 07:11:50 +00:00
showToast ( 'Uppdrag accepterat! Kr ' + m . reward _sek ? . toLocaleString ( 'sv-SE' ) ) ;
2026-06-29 16:24:48 +00:00
allMapMissions = allMapMissions . filter ( x => x . id !== m . id ) ;
renderMapMarkers ( ) ;
} else {
2026-07-07 07:11:50 +00:00
showToast ( '! ' + ( d . error || 'Fel vid accepterande' ) ) ;
2026-06-29 16:24:48 +00:00
}
} catch {
// Offline demo fallback
setActiveMission ( m ) ;
2026-07-07 07:11:50 +00:00
showToast ( 'Uppdrag accepterat (offline)! Kr ' + m . reward _sek ? . toLocaleString ( 'sv-SE' ) ) ;
2026-06-29 16:24:48 +00:00
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">📋</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>
2026-07-07 07:11:50 +00:00
<div class="mc-loc">+ ${ m . location ? . address || m . location ? . city || 'Okänd plats' } ${ deadline ? ' · + ' + deadline : '' } </div>
2026-06-29 16:24:48 +00:00
</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>
2026-07-07 07:11:50 +00:00
<div class="mc-cta"> ${ isActive ? 'Pågår' : m . status === 'completed' ? 'Klart' : 'Detaljer' } › </div>
2026-06-29 16:24:48 +00:00
</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 ;
2026-07-07 07:11:50 +00:00
document . getElementById ( 'ab-loc' ) . textContent = '+ ' + ( m . location ? . address || m . location ? . city || '—' ) + ' · Kr ' + ( m . reward _sek || 0 ) . toLocaleString ( 'sv-SE' ) ;
2026-06-29 16:24:48 +00:00
}
function openMissionDetail ( id ) {
const m = myMissions . find ( x => x . id === id ) || ( getActiveMission ( ) ? . id === id ? getActiveMission ( ) : null ) ;
if ( ! m ) return ;
if ( m . status === 'open' ) {
2026-07-07 07:11:50 +00:00
if ( getActiveMission ( ) ) { showToast ( '! Du har redan ett aktivt uppdrag' ) ; return ; }
2026-06-29 16:24:48 +00:00
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 ) ;
2026-07-07 07:11:50 +00:00
showToast ( 'Uppdrag accepterat!' ) ;
2026-06-29 16:24:48 +00:00
loadMinaMissions ( ) ;
}
} catch {
setActiveMission ( m ) ;
2026-07-07 07:11:50 +00:00
showToast ( 'Uppdrag accepterat!' ) ;
2026-06-29 16:24:48 +00:00
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 = [ ] ;
2026-07-07 07:11:50 +00:00
showToast ( '+ Uppdrag slutfört! Kr ' + ( m . reward _sek || 0 ) . toLocaleString ( 'sv-SE' ) + ' utbetalas' ) ;
2026-06-29 16:24:48 +00:00
loadMinaMissions ( ) ;
return ;
}
} catch { /* fallthrough */ }
setActiveMission ( null ) ;
uploadedMediaUrls = [ ] ;
2026-07-07 07:11:50 +00:00
showToast ( '+ Uppdrag slutfört! Betalning på väg.' ) ;
2026-06-29 16:24:48 +00:00
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 ( ) ;
2026-07-07 07:11:50 +00:00
if ( files . length ) showToast ( '+ ' + files . length + ' fil(er) bifogade' ) ;
2026-06-29 16:24:48 +00:00
} ) ;
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 ) =>
2026-07-07 07:11:50 +00:00
` <div class="preview-thumb" title=" ${ u } ">+</div> `
2026-06-29 16:24:48 +00:00
) . join ( '' ) + ` <div class="preview-thumb" style="cursor:pointer;font-size:20px" onclick="triggerFileInput()">+</div> ` ;
}
function submitMedia ( ) {
2026-07-07 07:11:50 +00:00
if ( ! uploadedMediaUrls . length ) { showToast ( '! Välj minst en fil' ) ; return ; }
2026-06-29 16:24:48 +00:00
closeUploadSheet ( ) ;
2026-07-07 07:11:50 +00:00
showToast ( '+ Media uppladdad! Klicka Slutför när du är klar.' ) ;
2026-06-29 16:24:48 +00:00
}
// ══════════════════════════════════════════════════════════════════════════
// 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 = `
2026-07-07 07:11:50 +00:00
<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> ` ;
2026-06-29 16:24:48 +00:00
}
2026-07-07 07:11:50 +00:00
const ZOOMER _AVATARS = [ 'Z' , 'Z' , 'Z' , 'Z' , 'Z' , 'Z' , 'Z' , 'Z' ] ;
2026-06-29 16:24:48 +00:00
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' ;
2026-07-07 07:11:50 +00:00
const rankTxt = i === 0 ? '1' : i === 1 ? '2' : i === 2 ? '3' : '#' + ( i + 1 ) ;
2026-06-29 16:24:48 +00:00
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>
2026-07-07 07:11:50 +00:00
<div class="lb-meta">+ ${ z . city } · ⭐ ${ z . rating } · ${ z . skill _level } </div>
2026-06-29 16:24:48 +00:00
</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 ( '' ) ;
}
2026-07-07 07:11:50 +00:00
const CITY _ICONS = { Stockholm : '+' , Göteborg : '+' , Malmö : '+' , Uppsala : '+' , Västerås : '+' } ;
2026-06-29 16:24:48 +00:00
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">
2026-07-07 07:11:50 +00:00
<div class="city-rank" style="color: ${ i === 0 ? '#FFD700' : i === 1 ? '#C0C0C0' : i === 2 ? '#CD7F32' : 'var(--label-tertiary)' } "> ${ i === 0 ? '1' : i === 1 ? '2' : i === 2 ? '3' : '#' + ( i + 1 ) } </div>
<div class="city-icon"> ${ CITY _ICONS [ c . city ] || '+' } </div>
2026-06-29 16:24:48 +00:00
<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 = [
2026-07-07 07:11:50 +00:00
{ 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 } ,
2026-06-29 16:24:48 +00:00
{ 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 } ,
2026-07-07 07:11:50 +00:00
{ 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 } ,
2026-06-29 16:24:48 +00:00
{ 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 >