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
1168 lines
47 KiB
HTML
1168 lines
47 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="sv" data-theme="dark">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||
<title>Uppdragsdetaljer — quiXzoom</title>
|
||
<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) + 90px);
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
|
||
/* Nav */
|
||
.nav-bar {
|
||
position: sticky; top: 0; z-index: 200;
|
||
display: flex; align-items: center; height: 44px; padding: 0 16px;
|
||
background: var(--navbar-bg);
|
||
backdrop-filter: saturate(180%) blur(20px);
|
||
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
||
border-bottom: 0.5px solid var(--separator);
|
||
gap: 12px;
|
||
}
|
||
.back-btn {
|
||
background: none; border: none;
|
||
color: var(--ios-blue); font-size: 17px;
|
||
cursor: pointer; display: flex; align-items: center; gap: 4px;
|
||
font-family: var(--font-text);
|
||
}
|
||
.nav-center {
|
||
flex: 1; text-align: center;
|
||
font-size: 17px; font-weight: 600; letter-spacing: -0.41px;
|
||
}
|
||
.nav-spacer { width: 60px; }
|
||
|
||
/* Hero map */
|
||
#detail-map { width: 100%; height: 200px; }
|
||
|
||
/* Content */
|
||
.content { padding: 0 16px; }
|
||
|
||
/* Hero Card */
|
||
.hero-card {
|
||
background: var(--background-secondary);
|
||
border-radius: var(--radius-lg);
|
||
margin: 16px 0;
|
||
overflow: hidden;
|
||
border: 0.5px solid var(--separator);
|
||
}
|
||
.hero-top {
|
||
display: flex; align-items: flex-start; gap: 14px;
|
||
padding: 16px;
|
||
}
|
||
.hero-icon {
|
||
width: 52px; height: 52px; border-radius: var(--radius-md);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 26px; flex-shrink: 0;
|
||
}
|
||
.hero-info { flex: 1; }
|
||
.hero-title {
|
||
font-family: var(--font-display);
|
||
font-size: 20px; font-weight: 700; letter-spacing: -0.5px;
|
||
margin-bottom: 4px;
|
||
}
|
||
.hero-address {
|
||
font-size: 14px; color: var(--label-secondary);
|
||
}
|
||
.hero-reward {
|
||
font-family: var(--font-display);
|
||
font-size: 28px; font-weight: 800; color: var(--ios-green);
|
||
letter-spacing: -0.5px;
|
||
}
|
||
.hero-reward-label { font-size: 13px; color: var(--label-secondary); font-weight: 500; }
|
||
|
||
.hero-stats {
|
||
display: grid; grid-template-columns: 1fr 1fr 1fr;
|
||
border-top: 0.5px solid var(--separator);
|
||
}
|
||
.stat-cell {
|
||
padding: 12px;
|
||
display: flex; flex-direction: column; align-items: center; gap: 3px;
|
||
border-right: 0.5px solid var(--separator);
|
||
}
|
||
.stat-cell:last-child { border-right: none; }
|
||
.stat-value { font-size: 15px; font-weight: 600; }
|
||
.stat-label { font-size: 11px; color: var(--label-secondary); text-align: center; }
|
||
|
||
/* Sections */
|
||
.section { margin-bottom: 20px; }
|
||
.section-title {
|
||
font-size: 20px; font-weight: 700; letter-spacing: -0.5px;
|
||
margin-bottom: 10px;
|
||
}
|
||
.section-card {
|
||
background: var(--background-secondary);
|
||
border-radius: var(--radius-lg); overflow: hidden;
|
||
border: 0.5px solid var(--separator);
|
||
}
|
||
.section-row {
|
||
display: flex; align-items: flex-start; gap: 12px;
|
||
padding: 13px 16px;
|
||
border-bottom: 0.5px solid var(--separator);
|
||
}
|
||
.section-row:last-child { border-bottom: none; }
|
||
.row-icon { font-size: 18px; flex-shrink: 0; width: 24px; text-align: center; }
|
||
.row-content { flex: 1; }
|
||
.row-title { font-size: 15px; font-weight: 500; }
|
||
.row-sub { font-size: 13px; color: var(--label-secondary); margin-top: 2px; }
|
||
|
||
/* Description */
|
||
.description-text {
|
||
font-size: 15px; line-height: 1.55;
|
||
color: var(--label-primary); padding: 14px 16px;
|
||
background: var(--background-secondary);
|
||
border-radius: var(--radius-lg);
|
||
border: 0.5px solid var(--separator);
|
||
}
|
||
|
||
/* Steps */
|
||
.steps { display: flex; flex-direction: column; gap: 0; }
|
||
.step {
|
||
display: flex; gap: 14px; padding: 12px 16px;
|
||
position: relative;
|
||
}
|
||
.step-num {
|
||
width: 28px; height: 28px; border-radius: var(--radius-full);
|
||
background: var(--ios-blue); color: #fff;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 14px; font-weight: 700; flex-shrink: 0;
|
||
}
|
||
.step-num.done { background: var(--ios-green); }
|
||
.step-body { flex: 1; padding-top: 4px; }
|
||
.step-title { font-size: 15px; font-weight: 600; }
|
||
.step-desc { font-size: 13px; color: var(--label-secondary); margin-top: 3px; }
|
||
|
||
/* Photo Section */
|
||
.photo-wrap { padding: 14px 16px; }
|
||
.photo-grid { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
|
||
.photo-thumb { width: 80px; height: 80px; border-radius: var(--radius-md); object-fit: cover; border: 0.5px solid var(--separator); }
|
||
.photo-add-btn {
|
||
width: 80px; height: 80px; border-radius: var(--radius-md);
|
||
background: var(--fill-secondary);
|
||
border: 1.5px dashed var(--ios-blue);
|
||
display: flex; flex-direction: column;
|
||
align-items: center; justify-content: center;
|
||
font-size: 24px; cursor: pointer;
|
||
color: var(--ios-blue); gap: 2px;
|
||
}
|
||
.photo-add-btn span { font-size: 10px; font-weight: 600; }
|
||
|
||
/* Bottom Actions */
|
||
.bottom-actions {
|
||
position: fixed; bottom: 0; left: 0; right: 0;
|
||
background: var(--tabbar-bg);
|
||
backdrop-filter: saturate(180%) blur(20px);
|
||
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
||
border-top: 0.5px solid var(--separator);
|
||
padding: 12px 16px calc(env(safe-area-inset-bottom,34px) + 12px);
|
||
display: flex; gap: 10px;
|
||
}
|
||
.action-btn {
|
||
flex: 1; padding: 14px;
|
||
border: none; border-radius: var(--radius-lg);
|
||
font-family: var(--font-text); font-size: 16px; font-weight: 700;
|
||
cursor: pointer; transition: transform 0.1s, opacity 0.1s;
|
||
letter-spacing: -0.2px;
|
||
}
|
||
.action-btn:active { transform: scale(0.97); opacity: 0.85; }
|
||
.btn-accept { background: var(--ios-blue); color: #fff; }
|
||
.btn-decline { background: var(--fill-secondary); color: var(--label-primary); }
|
||
.btn-camera { background: var(--fill-secondary); color: var(--label-primary); flex: 0 0 52px; font-size: 22px; }
|
||
.btn-complete { background: var(--ios-green); color: #fff; }
|
||
.btn-disabled { background: var(--fill-secondary); color: var(--label-tertiary); pointer-events: none; }
|
||
|
||
/* Badge */
|
||
.status-badge {
|
||
display: inline-block; padding: 4px 10px; border-radius: var(--radius-xl);
|
||
font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.3px;
|
||
}
|
||
.badge-open { background: rgba(52,199,89,0.15); color: var(--ios-green); }
|
||
.badge-assigned { background: rgba(255,149,0,0.15); color: var(--ios-orange); }
|
||
.badge-submitted { background: rgba(0,122,255,0.15); color: var(--ios-blue); }
|
||
.badge-approved { background: rgba(88,86,214,0.15); color: var(--ios-indigo); }
|
||
|
||
/* Loader */
|
||
.loader { display:flex; justify-content:center; padding:80px; }
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
.spinner { width:32px; height:32px; border-radius: var(--radius-full); border:3px solid var(--fill-secondary); border-top-color:var(--ios-blue); animation:spin 0.8s linear infinite; }
|
||
|
||
/* Toast */
|
||
.toast { position:fixed; bottom:calc(env(safe-area-inset-bottom,34px)+100px); left:50%; transform:translateX(-50%) translateY(20px); background:var(--background-secondary); border:0.5px solid var(--separator); border-radius: var(--radius-xl); padding:10px 18px; font-size:14px; font-weight:500; box-shadow:0 8px 32px rgba(0,0,0,0.3); opacity:0; transition:all 0.3s; z-index:9999; white-space:nowrap; }
|
||
.toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
|
||
|
||
/* Icons bg */
|
||
.icon-facade { background: rgba(0,122,255,0.15); }
|
||
.icon-roof { background: rgba(255,149,0,0.15); }
|
||
.icon-road { background: rgba(142,142,147,0.15); }
|
||
.icon-interior { background: rgba(52,199,89,0.15); }
|
||
.icon-vegetation { background: rgba(48,209,88,0.15); }
|
||
.icon-inspection { background: rgba(175,82,222,0.15); }
|
||
|
||
.gps-chip {
|
||
display: inline-flex; align-items: center; gap: 4px;
|
||
padding: 4px 10px; border-radius: var(--radius-xl);
|
||
background: rgba(52,199,89,0.15); color: var(--ios-green);
|
||
font-size: 12px; font-weight: 600;
|
||
}
|
||
.gps-chip.searching { background: rgba(255,149,0,0.15); color: var(--ios-orange); }
|
||
@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="Uppdragsdetaljer — quiXzoom">
|
||
<meta property="og:description" content="quiXzoom — Earn per Mission. No Platform Fee.">
|
||
<meta property="og:url" content="https://www.quixzoom.com/mission-detail.html">
|
||
<meta property="og:image" content="https://www.quixzoom.com/images/infrastructure-scan.jpg">
|
||
<meta property="og:image:width" content="1200">
|
||
<meta property="og:image:height" content="630">
|
||
<meta property="og:image:alt" content="quiXzoom mission detail — how photo missions work">
|
||
<meta name="twitter:card" content="summary_large_image">
|
||
<meta name="twitter:image" content="https://www.quixzoom.com/images/infrastructure-scan.jpg">
|
||
<meta name="twitter:title" content="Uppdragsdetaljer — quiXzoom">
|
||
<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>
|
||
|
||
<!-- Nav -->
|
||
<div class="nav-bar">
|
||
<button class="back-btn" onclick="history.back()">
|
||
‹ Tillbaka
|
||
</button>
|
||
<div class="nav-center">Uppdragsdetaljer</div>
|
||
<div class="nav-spacer"></div>
|
||
</div>
|
||
|
||
<!-- Map -->
|
||
<div id="detail-map"></div>
|
||
|
||
<!-- Main content -->
|
||
<div class="content" id="main-content">
|
||
<div class="loader"><div class="spinner"></div></div>
|
||
</div>
|
||
|
||
<!-- Hidden file input -->
|
||
<input type="file" id="photo-input" accept="image/*" capture="environment" multiple style="display:none">
|
||
|
||
<!-- Bottom actions -->
|
||
<div class="bottom-actions" id="bottom-actions" style="display:none"></div>
|
||
|
||
<!-- Toast -->
|
||
<div class="toast" id="toast"></div>
|
||
|
||
<script>
|
||
const API = 'https://api.quixzoom.com/api/qz/missions';
|
||
let mission = null;
|
||
let map = null;
|
||
let uploadedFiles = [];
|
||
let userLat = null, userLon = null;
|
||
|
||
const urlParams = new URLSearchParams(location.search);
|
||
const missionId = urlParams.get('id');
|
||
|
||
function getZoomerId() {
|
||
let id = localStorage.getItem('qz_zoomer_id');
|
||
if (!id) { id = 'zoomer-' + Math.random().toString(36).slice(2,10); localStorage.setItem('qz_zoomer_id', id); }
|
||
return id;
|
||
}
|
||
|
||
function getActiveMission() {
|
||
try { return JSON.parse(localStorage.getItem('qz_active_mission') || 'null'); } catch { return null; }
|
||
}
|
||
function setActiveMission(m) {
|
||
if (m) localStorage.setItem('qz_active_mission', JSON.stringify(m));
|
||
else localStorage.removeItem('qz_active_mission');
|
||
}
|
||
|
||
function toast(msg, dur=3000) {
|
||
const el = document.getElementById('toast');
|
||
el.textContent = msg;
|
||
el.classList.add('show');
|
||
setTimeout(() => el.classList.remove('show'), dur);
|
||
}
|
||
|
||
function missionTypeConfig(type) {
|
||
const map = {
|
||
facade_scan: { 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 5H7M6 8H7M6 11H7M9 5H10M9 8H10M9 11H10" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg>', cls:'icon-facade', label:'Fasadscanning' },
|
||
roof_scan: { icon:'', cls:'icon-roof', label:'Takscanning' },
|
||
road_inspection: { icon:'', cls:'icon-road', label:'Väginspekt.' },
|
||
interior_scan: { icon:'', cls:'icon-interior', label:'Interi ör' },
|
||
vegetation_survey: { icon:'', cls:'icon-vegetation', label:'Vegetationssurv.' },
|
||
inspection: { 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>', cls:'icon-inspection', label:'Inspektion' },
|
||
};
|
||
return map[type] || { icon:'', cls:'icon-inspection', label: type };
|
||
}
|
||
|
||
function getInstructions(type) {
|
||
const m = {
|
||
facade_scan: [
|
||
{ title: 'Anländ till adressen', desc: 'Bekräfta GPS-position är inom 50m' },
|
||
{ title: 'Fotografera hela fasaden', desc: 'Minst 8 bilder från olika vinklar' },
|
||
{ title: 'Detaljer & skador', desc: 'Närbild på brandskadat/skadat parti' },
|
||
{ title: 'Ladda upp & slutför', desc: 'Skicka in bilder för 3D-processering' },
|
||
],
|
||
roof_scan: [
|
||
{ title: 'Anländ till fastigheten', desc: 'Bekräfta GPS och se upp för hinder' },
|
||
{ title: 'Fotografera takytan', desc: 'Täck hela taket med överlappande bilder' },
|
||
{ title: 'Dokumentera skador', desc: 'Markera stormskadade partier' },
|
||
{ title: 'Ladda upp & slutför', desc: 'Skicka in för AI-analys' },
|
||
],
|
||
road_inspection: [
|
||
{ title: 'Gå längs sträckan', desc: 'Strandvägen 200 m enl. uppdraget' },
|
||
{ title: 'Fotografera varje skada', desc: 'Potthål, sprickor, markskador' },
|
||
{ title: 'GPS-verifiering', desc: 'Systemet loggar din rutt automatiskt' },
|
||
{ title: 'Slutför', desc: 'Skicka in dokumentation' },
|
||
],
|
||
};
|
||
return m[type] || [
|
||
{ title: 'Anländ till platsen', desc: 'Bekräfta GPS-position' },
|
||
{ title: 'Dokumentera objektet', desc: 'Minst 5 bilder från olika vinklar' },
|
||
{ title: 'Ladda upp & slutför', desc: 'Skicka in bilderna' },
|
||
];
|
||
}
|
||
|
||
function formatSEK(n) { return new Intl.NumberFormat('sv-SE').format(n) + ' kr'; }
|
||
function formatDate(s) { return s ? new Date(s).toLocaleDateString('sv-SE', { day:'numeric', month:'short', hour:'2-digit', minute:'2-digit'}) : '—'; }
|
||
|
||
function statusBadge(status) {
|
||
const labels = { open:'Öppen', assigned:'Tilldelad', submitted:'Inskickad', approved:'Godkänd' };
|
||
return `<span class="status-badge badge-${status}">${labels[status]||status}</span>`;
|
||
}
|
||
|
||
function initMap(lat, lon) {
|
||
map = L.map('detail-map', { zoomControl: false }).setView([lat, lon], 16);
|
||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution:'© OpenStreetMap' }).addTo(map);
|
||
|
||
const icon = L.divIcon({
|
||
html: `<div style="width:40px;height:40px;border-radius: var(--radius-full);background:var(--ios-blue);display:flex;align-items:center;justify-content:center;font-size:20px;box-shadow:0 2px 10px rgba(0,0,0,0.4);border:2.5px solid white;">${missionTypeConfig(mission.mission_type).icon}</div>`,
|
||
className:'', iconSize:[40,40], iconAnchor:[20,20]
|
||
});
|
||
L.marker([lat, lon], {icon}).addTo(map).openPopup();
|
||
|
||
// Accuracy circle
|
||
L.circle([lat, lon], { radius: 50, color: 'rgba(0,122,255,0.3)', fillColor: 'rgba(0,122,255,0.1)', fillOpacity: 0.5, weight: 1 }).addTo(map);
|
||
}
|
||
|
||
function renderContent() {
|
||
const cfg = missionTypeConfig(mission.mission_type);
|
||
const instructions = getInstructions(mission.mission_type);
|
||
const zoomerId = getZoomerId();
|
||
const isMyMission = mission.zoomer_id === zoomerId;
|
||
const activeMission = getActiveMission();
|
||
|
||
const distText = (userLat && userLon)
|
||
? `${Math.round(calcDist(userLat, userLon, mission.lat, mission.lon))} m bort`
|
||
: 'Hämtar GPS...';
|
||
|
||
const html = `
|
||
<div class="hero-card">
|
||
<div class="hero-top">
|
||
<div class="hero-icon ${cfg.cls}">${cfg.icon}</div>
|
||
<div class="hero-info">
|
||
<div class="hero-title">${mission.title}</div>
|
||
<div class="hero-address"> ${mission.address || 'Okänd plats'}</div>
|
||
<div style="margin-top:6px">${statusBadge(mission.status)}</div>
|
||
</div>
|
||
</div>
|
||
<div class="hero-stats">
|
||
<div class="stat-cell">
|
||
<div class="stat-value" style="color:var(--ios-green)">${formatSEK(mission.reward_sek)}</div>
|
||
<div class="stat-label">Ersättning</div>
|
||
</div>
|
||
<div class="stat-cell">
|
||
<div class="stat-value">${cfg.label}</div>
|
||
<div class="stat-label">Uppdragstyp</div>
|
||
</div>
|
||
<div class="stat-cell">
|
||
<div class="stat-value gps-chip ${!userLat ? 'searching' : ''}">${!userLat ? ' GPS' : ' ' + distText}</div>
|
||
<div class="stat-label">Avstånd</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-title">Om uppdraget</div>
|
||
<div class="description-text">${mission.description || 'Ingen ytterligare beskrivning.'}</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-title">Information</div>
|
||
<div class="section-card">
|
||
<div class="section-row">
|
||
<div class="row-icon"></div>
|
||
<div class="row-content">
|
||
<div class="row-title">Skapat</div>
|
||
<div class="row-sub">${formatDate(mission.created_at)}</div>
|
||
</div>
|
||
</div>
|
||
${mission.accepted_at ? `
|
||
<div class="section-row">
|
||
<div class="row-icon"><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></div>
|
||
<div class="row-content">
|
||
<div class="row-title">Accepterat</div>
|
||
<div class="row-sub">${formatDate(mission.accepted_at)}</div>
|
||
</div>
|
||
</div>` : ''}
|
||
${mission.submitted_at ? `
|
||
<div class="section-row">
|
||
<div class="row-icon"></div>
|
||
<div class="row-content">
|
||
<div class="row-title">Inskickat</div>
|
||
<div class="row-sub">${formatDate(mission.submitted_at)}</div>
|
||
</div>
|
||
</div>` : ''}
|
||
${mission.approved_at ? `
|
||
<div class="section-row">
|
||
<div class="row-icon"></div>
|
||
<div class="row-content">
|
||
<div class="row-title">Godkänt & betalt</div>
|
||
<div class="row-sub">${formatDate(mission.approved_at)} · Ref: ${mission.payment_ref || '—'}</div>
|
||
</div>
|
||
</div>` : ''}
|
||
<div class="section-row">
|
||
<div class="row-icon"></div>
|
||
<div class="row-content">
|
||
<div class="row-title">Uppdragsnr</div>
|
||
<div class="row-sub" style="font-family:var(--font-mono);font-size:12px">${mission.id}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-title">Instruktioner</div>
|
||
<div class="section-card steps">
|
||
${instructions.map((step, i) => `
|
||
<div class="step">
|
||
<div class="step-num ${mission.status !== 'open' && i === 0 ? 'done' : ''}">${i+1}</div>
|
||
<div class="step-body">
|
||
<div class="step-title">${step.title}</div>
|
||
<div class="step-desc">${step.desc}</div>
|
||
</div>
|
||
</div>`).join('')}
|
||
</div>
|
||
</div>
|
||
|
||
${(isMyMission && mission.status === 'assigned') ? `
|
||
<div class="section">
|
||
<div class="section-title"><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> Dokumentation</div>
|
||
<div class="section-card">
|
||
<div class="photo-wrap">
|
||
<div style="font-size:13px;color:var(--label-secondary);margin-bottom:6px">
|
||
Ta minst 3 bilder för att kunna slutföra uppdraget
|
||
</div>
|
||
<div class="photo-grid" id="photo-grid">
|
||
<div class="photo-add-btn" onclick="document.getElementById('photo-input').click()">
|
||
<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><span>Lägg till</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>` : ''}
|
||
`;
|
||
|
||
document.getElementById('main-content').innerHTML = html;
|
||
renderBottomActions();
|
||
}
|
||
|
||
function renderBottomActions() {
|
||
const bar = document.getElementById('bottom-actions');
|
||
bar.style.display = 'flex';
|
||
const zoomerId = getZoomerId();
|
||
const isMyMission = mission.zoomer_id === zoomerId;
|
||
|
||
if (mission.status === 'open') {
|
||
bar.innerHTML = `
|
||
<button class="action-btn btn-decline" onclick="history.back()"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 4L12 12M12 4L4 12" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Avböj</button>
|
||
<button class="action-btn btn-accept" onclick="acceptMission()"><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> Acceptera uppdrag</button>`;
|
||
} else if (mission.status === 'assigned' && isMyMission) {
|
||
bar.innerHTML = `
|
||
<button class="action-btn btn-camera" onclick="document.getElementById('photo-input').click()"><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></button>
|
||
<button class="action-btn btn-complete" onclick="submitWithPhotos()"><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 & skicka in</button>`;
|
||
} else if (mission.status === 'submitted') {
|
||
bar.innerHTML = `<button class="action-btn btn-disabled">⏳ Väntar på granskning</button>`;
|
||
} else if (mission.status === 'approved') {
|
||
bar.innerHTML = `<button class="action-btn btn-disabled" style="background:rgba(52,199,89,0.15);color:var(--ios-green)"> Godkänt & betalt</button>`;
|
||
} else {
|
||
bar.innerHTML = `<button class="action-btn btn-decline" onclick="history.back()">← Tillbaka</button>`;
|
||
}
|
||
}
|
||
|
||
async function acceptMission() {
|
||
const zoomerId = getZoomerId();
|
||
const btn = document.querySelector('.btn-accept');
|
||
if (btn) { btn.textContent = '⏳ Accepterar...'; btn.disabled = true; }
|
||
|
||
try {
|
||
const r = await fetch(`${API}/missions/${missionId}/accept`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ zoomer_id: zoomerId })
|
||
});
|
||
const data = await r.json();
|
||
if (r.ok) {
|
||
mission = data;
|
||
setActiveMission(data);
|
||
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Uppdrag accepterat! Dra till platsen och starta.');
|
||
renderContent();
|
||
} else {
|
||
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> ' + (data.error || 'Kunde inte acceptera'));
|
||
if (btn) { btn.textContent = '<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> Acceptera uppdrag'; btn.disabled = false; }
|
||
}
|
||
} catch(e) {
|
||
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Nätverksfel');
|
||
if (btn) { btn.textContent = '<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> Acceptera uppdrag'; btn.disabled = false; }
|
||
}
|
||
}
|
||
|
||
document.getElementById('photo-input').addEventListener('change', e => {
|
||
uploadedFiles = Array.from(e.target.files);
|
||
const grid = document.getElementById('photo-grid');
|
||
if (!grid) return;
|
||
|
||
// Remove old thumbs, keep add btn
|
||
grid.querySelectorAll('.photo-thumb').forEach(el => el.remove());
|
||
|
||
uploadedFiles.forEach(f => {
|
||
const img = document.createElement('img');
|
||
img.className = 'photo-thumb';
|
||
img.src = URL.createObjectURL(f);
|
||
grid.insertBefore(img, grid.firstChild);
|
||
});
|
||
|
||
toast(`<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> ${uploadedFiles.length} foto valda`);
|
||
const bar = document.getElementById('bottom-actions');
|
||
bar.innerHTML = `
|
||
<button class="action-btn btn-camera" onclick="document.getElementById('photo-input').click()"><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> (${uploadedFiles.length})</button>
|
||
<button class="action-btn btn-complete" onclick="submitWithPhotos()"><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> Skicka in ${uploadedFiles.length} bilder</button>`;
|
||
});
|
||
|
||
async function submitWithPhotos() {
|
||
if (uploadedFiles.length === 0) {
|
||
toast('<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> Ta minst ett foto innan du slutför');
|
||
return;
|
||
}
|
||
const btn = document.querySelector('.btn-complete');
|
||
if (btn) { btn.textContent = '⏳ Skickar...'; btn.disabled = true; }
|
||
|
||
const fd = new FormData();
|
||
uploadedFiles.forEach(f => fd.append('images', f));
|
||
|
||
try {
|
||
const r = await fetch(`${API}/missions/${missionId}/submit`, { method:'POST', body: fd });
|
||
const data = await r.json();
|
||
if (r.ok) {
|
||
mission = data.mission;
|
||
setActiveMission(null);
|
||
toast(' Inskickat! Väntar på granskning och utbetalning.');
|
||
uploadedFiles = [];
|
||
setTimeout(() => window.location.href = 'zoomer-app.html', 2000);
|
||
} else {
|
||
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> ' + (data.error || 'Fel vid inskick'));
|
||
if (btn) { btn.textContent = '<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> Skicka in'; btn.disabled = false; }
|
||
}
|
||
} catch(e) {
|
||
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Nätverksfel — försök igen');
|
||
if (btn) { btn.textContent = '<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> Skicka in'; btn.disabled = false; }
|
||
}
|
||
}
|
||
|
||
function calcDist(lat1, lon1, lat2, lon2) {
|
||
const R = 6371000;
|
||
const φ1 = lat1 * Math.PI/180, φ2 = lat2 * Math.PI/180;
|
||
const Δφ = (lat2-lat1) * Math.PI/180, Δλ = (lon2-lon1) * Math.PI/180;
|
||
const a = Math.sin(Δφ/2)*Math.sin(Δφ/2) + Math.cos(φ1)*Math.cos(φ2)*Math.sin(Δλ/2)*Math.sin(Δλ/2);
|
||
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
||
}
|
||
|
||
async function load() {
|
||
if (!missionId) {
|
||
document.getElementById('main-content').innerHTML = '<div style="padding:40px;text-align:center;color:var(--label-secondary)">Inget uppdrag valt</div>';
|
||
return;
|
||
}
|
||
|
||
// Get GPS in parallel
|
||
navigator.geolocation?.getCurrentPosition(pos => {
|
||
userLat = pos.coords.latitude;
|
||
userLon = pos.coords.longitude;
|
||
});
|
||
|
||
try {
|
||
const r = await fetch(`${API}/missions/${missionId}`);
|
||
mission = await r.json();
|
||
initMap(mission.lat, mission.lon);
|
||
renderContent();
|
||
} catch(e) {
|
||
document.getElementById('main-content').innerHTML = `<div style="padding:40px;text-align:center"><div style="font-size:48px"></div><div style="margin-top:12px;color:var(--label-secondary)">Kunde inte ladda uppdrag</div></div>`;
|
||
}
|
||
}
|
||
|
||
load();
|
||
</script>
|
||
<!-- quiXzoom Mina Sidor - Universal Header Component -->
|
||
<!-- Injicera detta i <head> på alla quiXzoom-sajter -->
|
||
|
||
<style>
|
||
.qz-auth-bar {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 48px;
|
||
background: #0A0A0A;
|
||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 20px;
|
||
z-index: 9999;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif;
|
||
}
|
||
.qz-auth-bar .qz-logo {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
color: #fff;
|
||
text-decoration: none;
|
||
letter-spacing: -0.5px;
|
||
}
|
||
.qz-auth-bar .qz-logo span {
|
||
color: #007AFF;
|
||
}
|
||
.qz-auth-bar .qz-nav {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
.qz-auth-bar .qz-btn {
|
||
padding: 8px 16px;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
border: none;
|
||
text-decoration: none;
|
||
}
|
||
.qz-auth-bar .qz-btn-primary {
|
||
background: #007AFF;
|
||
color: white;
|
||
}
|
||
.qz-auth-bar .qz-btn-primary:hover {
|
||
background: #0056CC;
|
||
}
|
||
.qz-auth-bar .qz-btn-ghost {
|
||
background: transparent;
|
||
color: rgba(255,255,255,0.8);
|
||
}
|
||
.qz-auth-bar .qz-btn-ghost:hover {
|
||
background: rgba(255,255,255,0.1);
|
||
color: white;
|
||
}
|
||
.qz-auth-bar .qz-user {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
color: white;
|
||
font-size: 14px;
|
||
}
|
||
.qz-auth-bar .qz-avatar {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 50%;
|
||
background: #007AFF;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
}
|
||
.qz-auth-bar .qz-dropdown {
|
||
position: relative;
|
||
}
|
||
.qz-auth-bar .qz-dropdown-menu {
|
||
position: absolute;
|
||
top: 40px;
|
||
right: 0;
|
||
background: #1C1C1E;
|
||
border: 1px solid rgba(255,255,255,0.1);
|
||
border-radius: 12px;
|
||
padding: 8px;
|
||
min-width: 200px;
|
||
display: none;
|
||
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
||
}
|
||
.qz-auth-bar .qz-dropdown-menu.active {
|
||
display: block;
|
||
}
|
||
.qz-auth-bar .qz-dropdown-item {
|
||
padding: 10px 12px;
|
||
border-radius: 8px;
|
||
color: white;
|
||
text-decoration: none;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
font-size: 14px;
|
||
transition: background 0.2s;
|
||
}
|
||
.qz-auth-bar .qz-dropdown-item:hover {
|
||
background: rgba(255,255,255,0.1);
|
||
}
|
||
.qz-auth-bar .qz-dropdown-divider {
|
||
height: 1px;
|
||
background: rgba(255,255,255,0.1);
|
||
margin: 8px 0;
|
||
}
|
||
.qz-auth-bar .qz-balance {
|
||
background: rgba(0,122,255,0.15);
|
||
color: #007AFF;
|
||
padding: 4px 10px;
|
||
border-radius: 6px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
}
|
||
@media (max-width: 640px) {
|
||
.qz-auth-bar .qz-nav .qz-btn-ghost:not(.qz-user) {
|
||
display: none;
|
||
}
|
||
}
|
||
/* 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>
|
||
|
||
<div id="qz-auth-bar" class="qz-auth-bar">
|
||
<a href="https://quixzoom.com" class="qz-logo">qui<span>X</span>zoom</a>
|
||
<div class="qz-nav">
|
||
<div id="qz-auth-guest">
|
||
<a href="https://app.quixzoom.com/login" class="qz-btn qz-btn-ghost">Logga in</a>
|
||
<a href="https://app.quixzoom.com/register" class="qz-btn qz-btn-primary">Bli Zoomer</a>
|
||
</div>
|
||
<div id="qz-auth-user" style="display:none;">
|
||
<span class="qz-balance" id="qz-balance">$0.00</span>
|
||
<div class="qz-dropdown">
|
||
<button class="qz-btn qz-btn-ghost qz-user" onclick="qzToggleDropdown()">
|
||
<div class="qz-avatar" id="qz-avatar">E</div>
|
||
<span id="qz-user-name">Erik</span>
|
||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2.5 4.5L6 8L9.5 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||
</button>
|
||
<div class="qz-dropdown-menu" id="qz-dropdown-menu">
|
||
<a href="https://app.quixzoom.com/dashboard" class="qz-dropdown-item">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/></svg>
|
||
Dashboard
|
||
</a>
|
||
<a href="https://app.quixzoom.com/wallet" class="qz-dropdown-item">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/><path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/><path d="M18 12a2 2 0 0 0 0 4h4v-4h-4z"/></svg>
|
||
Plånbok
|
||
</a>
|
||
<a href="https://app.quixzoom.com/missions" class="qz-dropdown-item">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
|
||
Mina uppdrag
|
||
</a>
|
||
<div class="qz-dropdown-divider"></div>
|
||
<a href="https://app.quixzoom.com/settings" class="qz-dropdown-item">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.67 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.67 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.67a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
||
Inställningar
|
||
</a>
|
||
<a href="#" onclick="qzLogout(); return false;" class="qz-dropdown-item">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
||
Logga ut
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
(function() {
|
||
const SSO_BASE = 'https://auth.quixzoom.com';
|
||
const APP_BASE = 'https://app.quixzoom.com';
|
||
|
||
// Check auth status
|
||
async function qzCheckAuth() {
|
||
try {
|
||
const res = await fetch(SSO_BASE + '/auth/check', {
|
||
credentials: 'include',
|
||
});
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
if (data.authenticated) {
|
||
document.getElementById('qz-auth-guest').style.display = 'none';
|
||
document.getElementById('qz-auth-user').style.display = 'flex';
|
||
document.getElementById('qz-user-name').textContent = data.user.name || data.user.email;
|
||
document.getElementById('qz-avatar').textContent = (data.user.name || data.user.email)[0].toUpperCase();
|
||
if (data.user.balance !== undefined) {
|
||
document.getElementById('qz-balance').textContent = '$' + data.user.balance.toFixed(2);
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// Silent fail - show guest state
|
||
}
|
||
}
|
||
|
||
// Toggle dropdown
|
||
window.qzToggleDropdown = function() {
|
||
document.getElementById('qz-dropdown-menu').classList.toggle('active');
|
||
};
|
||
|
||
// Close dropdown on outside click
|
||
document.addEventListener('click', function(e) {
|
||
if (!e.target.closest('.qz-dropdown')) {
|
||
document.getElementById('qz-dropdown-menu').classList.remove('active');
|
||
}
|
||
});
|
||
|
||
// Logout
|
||
window.qzLogout = async function() {
|
||
try {
|
||
await fetch(SSO_BASE + '/auth/logout', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
});
|
||
} catch (e) {}
|
||
document.cookie = 'qz_access_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=.quixzoom.com; path=/;';
|
||
document.cookie = 'qz_refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=.quixzoom.com; path=/;';
|
||
window.location.reload();
|
||
};
|
||
|
||
// Init
|
||
qzCheckAuth();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|