Files
boc/quixzoom-landing-fixed/launch-status.html
T
Bernt 1a12fb870b dev-api: add developer portal, OpenAPI spec, and Flatenbadet case study
- 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
2026-07-14 15:27:33 +00:00

1183 lines
38 KiB
HTML

<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, 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>quiXzoom — Launch Status</title>
<style>
/* iOS System Font Stack */
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
:root {
--ios-blue: #007AFF;
--ios-green: #34C759;
--ios-orange: #FF9500;
--ios-red: #FF3B30;
--ios-purple: #AF52DE;
--ios-gray: #8E8E93;
--ios-gray2: #AEAEB2;
--ios-gray6: #F2F2F7;
--ios-label: #000000;
--ios-label2: #3C3C43;
--ios-bg: #F2F2F7;
--ios-card: #FFFFFF;
--ios-sep: rgba(60,60,67,0.12);
--qx-accent: #FF6D00;
}
body {
font-family: -apple-system, 'SF Pro Display', 'Helvetica Neue', sans-serif;
background: var(--ios-bg);
color: var(--ios-label);
min-height: 100dvh;
padding-bottom: env(safe-area-inset-bottom, 20px);
}
/* Status Bar */
.status-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 20px 8px;
font-size: 15px;
font-weight: 600;
letter-spacing: -0.3px;
}
.status-bar .time { font-variant-numeric: tabular-nums; }
.status-bar .icons { display: flex; gap: 6px; align-items: center; }
/* Navigation Bar */
.nav-bar {
padding: 6px 16px 12px;
display: flex;
align-items: flex-end;
justify-content: space-between;
}
.nav-bar h1 {
font-size: 34px;
font-weight: 700;
letter-spacing: -0.5px;
line-height: 1;
}
.nav-bar h1 span { color: var(--qx-accent); }
.launch-badge {
background: var(--ios-green);
color: white;
font-size: 12px;
font-weight: 600;
padding: 5px 10px;
border-radius: var(--radius-md);
letter-spacing: 0.3px;
}
.launch-badge.warning { background: var(--ios-orange); }
/* Scroll Container */
.scroll { overflow-y: auto; padding: 0 16px 32px; }
/* Hero Card */
.hero-card {
background: linear-gradient(135deg, #FF6D00 0%, #FF3D00 100%);
border-radius: var(--radius-xl);
padding: 24px;
color: white;
margin-bottom: 20px;
position: relative;
overflow: hidden;
}
.hero-card::before {
content: '';
position: absolute;
right: -30px; top: -30px;
width: 140px; height: 140px;
border-radius: var(--radius-full);
background: rgba(255,255,255,0.12);
}
.hero-card::after {
content: '';
position: absolute;
right: 30px; top: 40px;
width: 80px; height: 80px;
border-radius: var(--radius-full);
background: rgba(255,255,255,0.08);
}
.hero-label {
font-size: 13px;
font-weight: 500;
opacity: 0.85;
text-transform: uppercase;
letter-spacing: 0.8px;
margin-bottom: 8px;
}
.hero-date {
font-size: 36px;
font-weight: 700;
letter-spacing: -1px;
margin-bottom: 4px;
}
.hero-sub {
font-size: 15px;
opacity: 0.9;
margin-bottom: 20px;
}
.hero-countdown {
display: flex;
gap: 12px;
}
.countdown-unit {
background: rgba(255,255,255,0.22);
border-radius: var(--radius-lg);
padding: 12px 16px;
text-align: center;
backdrop-filter: blur(4px);
}
.countdown-unit .val {
font-size: 28px;
font-weight: 700;
letter-spacing: -1px;
display: block;
}
.countdown-unit .lbl {
font-size: 10px;
opacity: 0.75;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Section Header */
.section-header {
font-size: 20px;
font-weight: 700;
letter-spacing: -0.3px;
margin: 24px 0 10px;
}
/* Stats Row */
.stats-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 20px;
}
.stat-card {
background: var(--ios-card);
border-radius: var(--radius-lg);
padding: 18px 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
.stat-card .icon {
font-size: 28px;
margin-bottom: 8px;
display: block;
}
.stat-card .val {
font-size: 30px;
font-weight: 700;
letter-spacing: -1px;
display: block;
line-height: 1;
margin-bottom: 4px;
}
.stat-card .lbl {
font-size: 13px;
color: var(--ios-gray);
font-weight: 500;
}
/* Progress Bar */
.readiness-card {
background: var(--ios-card);
border-radius: var(--radius-lg);
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
.readiness-top {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 12px;
}
.readiness-pct {
font-size: 36px;
font-weight: 700;
letter-spacing: -1px;
}
.readiness-label { font-size: 15px; font-weight: 500; color: var(--ios-gray); }
.progress-track {
background: var(--ios-gray6);
border-radius: var(--radius-md);
height: 10px;
overflow: hidden;
margin-bottom: 10px;
}
.progress-fill {
height: 100%;
border-radius: var(--radius-md);
background: linear-gradient(90deg, var(--ios-green) 0%, #30D158 100%);
transition: width 1.2s cubic-bezier(0.16,1,0.3,1);
}
.readiness-sub {
font-size: 13px;
color: var(--ios-gray);
}
/* Cities */
.cities-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 20px;
}
.city-chip {
background: var(--ios-card);
border-radius: var(--radius-xl);
padding: 8px 14px;
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
gap: 6px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
.city-chip .dot {
width: 8px; height: 8px;
border-radius: var(--radius-full);
background: var(--ios-green);
}
/* Checklist */
.checklist-group {
background: var(--ios-card);
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
margin-bottom: 20px;
}
.check-item {
display: flex;
align-items: center;
padding: 14px 16px;
border-bottom: 1px solid var(--ios-sep);
gap: 12px;
}
.check-item:last-child { border-bottom: none; }
.check-icon {
width: 32px; height: 32px;
border-radius: var(--radius-full);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
}
.check-icon.done { background: rgba(52,199,89,0.15); }
.check-icon.pending { background: rgba(255,149,0,0.15); }
.check-body { flex: 1; min-width: 0; }
.check-title {
font-size: 15px;
font-weight: 500;
margin-bottom: 2px;
}
.check-detail {
font-size: 12px;
color: var(--ios-gray);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.check-badge {
font-size: 11px;
font-weight: 600;
padding: 3px 8px;
border-radius: var(--radius-md);
letter-spacing: 0.2px;
flex-shrink: 0;
}
.check-badge.done { background: rgba(52,199,89,0.15); color: #27a745; }
.check-badge.pending { background: rgba(255,149,0,0.15); color: #c87000; }
/* Footer */
.footer {
text-align: center;
font-size: 13px;
color: var(--ios-gray);
padding: 16px 0 8px;
}
.footer strong { color: var(--ios-label); }
/* Skeleton / loading */
.skeleton {
background: linear-gradient(90deg, #e8e8e8 25%, #f5f5f5 50%, #e8e8e8 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: var(--radius-md);
}
@keyframes shimmer { to { background-position: -200% 0; } }
/* Dark Mode */
@media (prefers-color-scheme: dark) {
:root {
--ios-bg: #000000;
--ios-card: #1C1C1E;
--ios-label: #FFFFFF;
--ios-gray6: #2C2C2E;
--ios-sep: rgba(255,255,255,0.1);
}
.skeleton { background: linear-gradient(90deg, #2a2a2a 25%, #383838 50%, #2a2a2a 75%); background-size: 200% 100%; }
}
/* Pulse dot */
.live-dot {
display: inline-block;
width: 8px; height: 8px;
border-radius: var(--radius-full);
background: var(--ios-green);
box-shadow: 0 0 0 0 rgba(52,199,89,0.5);
animation: pulse 2s infinite;
margin-right: 6px;
vertical-align: middle;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(52,199,89,0.5); }
70% { box-shadow: 0 0 0 8px rgba(52,199,89,0); }
100%{ box-shadow: 0 0 0 0 rgba(52,199,89,0); }
}
/* Refresh button */
.refresh-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
background: var(--ios-blue);
color: white;
border: none;
border-radius: var(--radius-lg);
padding: 16px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
margin-top: 8px;
font-family: inherit;
letter-spacing: -0.3px;
}
.refresh-btn:active { opacity: 0.8; transform: scale(0.98); }
@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 type="module" src="https://auth.quixzoom.com/site-auth-snippet.js"></script>
</head>
<body>
<!-- Status Bar -->
<div class="status-bar">
<span class="time" id="clock">9:41</span>
<span class="icons">
<svg width="17" height="12" viewBox="0 0 17 12" fill="currentColor"><rect x="0" y="3" width="3" height="9" rx="1" opacity="0.35"/><rect x="4" y="2" width="3" height="10" rx="1" opacity="0.55"/><rect x="8" y="1" width="3" height="11" rx="1" opacity="0.75"/><rect x="12" y="0" width="3" height="12" rx="1"/></svg>
<svg width="16" height="12" viewBox="0 0 16 12" fill="currentColor"><path d="M8 2.5C10.2 2.5 12.2 3.5 13.5 5.1L15 3.4C13.3 1.3 10.8 0 8 0S2.7 1.3 1 3.4L2.5 5.1C3.8 3.5 5.8 2.5 8 2.5zM8 5.5C9.5 5.5 10.8 6.1 11.7 7.1L13.2 5.4C11.9 4 10 3 8 3S4.1 4 2.8 5.4L4.3 7.1C5.2 6.1 6.5 5.5 8 5.5zM8 8.5c.9 0 1.7.4 2.3 1L8 12 5.7 9.5c.6-.6 1.4-1 2.3-1z"/></svg>
<svg width="25" height="12" viewBox="0 0 25 12" fill="currentColor"><rect x="0" y="1" width="21" height="10" rx="3.5" stroke="currentColor" stroke-width="1" fill="none" opacity="0.35"/><rect x="1.5" y="2.5" width="17" height="7" rx="2" fill="currentColor"/><rect x="22" y="4" width="3" height="4" rx="1.5" opacity="0.4"/></svg>
</span>
</div>
<!-- Nav Bar -->
<div class="nav-bar">
<h1>quiX<span>zoom</span></h1>
<div class="launch-badge" id="go-badge">Laddar...</div>
</div>
<!-- Scroll Area -->
<div class="scroll">
<!-- Hero Card -->
<div class="hero-card">
<div class="hero-label">Launch Sverige</div>
<div class="hero-date">1 augusti 2026</div>
<div class="hero-sub"><span class="live-dot"></span>Live tracking</div>
<div class="hero-countdown">
<div class="countdown-unit">
<span class="val" id="cd-days"></span>
<span class="lbl">Dagar</span>
</div>
<div class="countdown-unit">
<span class="val" id="cd-hours"></span>
<span class="lbl">Timmar</span>
</div>
<div class="countdown-unit">
<span class="val" id="cd-mins"></span>
<span class="lbl">Minuter</span>
</div>
</div>
</div>
<!-- Stats -->
<div class="section-header">Live-statistik</div>
<div class="stats-row">
<div class="stat-card">
<span class="icon"></span>
<span class="val" id="stat-missions"></span>
<span class="lbl">Uppdrag aktiva</span>
</div>
<div class="stat-card">
<span class="icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg></span>
<span class="val" id="stat-zoomers"></span>
<span class="lbl">Zoomers aktiva</span>
</div>
<div class="stat-card">
<span class="icon"></span>
<span class="val" id="stat-cities"></span>
<span class="lbl">Städer täckta</span>
</div>
<div class="stat-card">
<span class="icon"></span>
<span class="val" id="stat-days"></span>
<span class="lbl">Dagar kvar</span>
</div>
</div>
<!-- Readiness -->
<div class="readiness-card">
<div class="readiness-top">
<div>
<div class="readiness-pct" id="readiness-pct">—%</div>
<div class="readiness-label">Launch Readiness</div>
</div>
<div style="font-size:13px; color:var(--ios-gray); text-align:right;">
<span id="done-count"></span> / <span id="total-count"></span><br>klara
</div>
</div>
<div class="progress-track">
<div class="progress-fill" id="progress-fill" style="width:0%"></div>
</div>
<div class="readiness-sub" id="readiness-sub">Hämtar status...</div>
</div>
<!-- Cities -->
<div class="section-header">Täckningsområde</div>
<div class="cities-row" id="cities-row">
<div class="city-chip skeleton" style="width:100px; height:36px;"></div>
<div class="city-chip skeleton" style="width:90px; height:36px;"></div>
<div class="city-chip skeleton" style="width:80px; height:36px;"></div>
</div>
<!-- Checklist: Done -->
<div class="section-header">Klart <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="checklist-group" id="done-list">
<div class="check-item">
<div class="check-icon done skeleton" style="background:transparent"></div>
<div class="check-body">
<div class="skeleton" style="height:14px; width:70%; margin-bottom:6px;"></div>
<div class="skeleton" style="height:11px; width:50%;"></div>
</div>
</div>
</div>
<!-- Checklist: Pending -->
<div class="section-header">Återstår ⏳</div>
<div class="checklist-group" id="pending-list">
<div class="check-item">
<div class="check-icon pending skeleton" style="background:transparent"></div>
<div class="check-body">
<div class="skeleton" style="height:14px; width:60%; margin-bottom:6px;"></div>
<div class="skeleton" style="height:11px; width:45%;"></div>
</div>
</div>
</div>
<button class="refresh-btn" onclick="loadData()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
Uppdatera
</button>
<div class="footer">
<strong>quiXzoom</strong> — Wavult Group &nbsp;·&nbsp; <span id="last-updated"></span>
</div>
</div><!-- /scroll -->
<script>
// Clock
function updateClock() {
const now = new Date();
document.getElementById('clock').textContent =
now.getHours().toString().padStart(2,'0') + ':' +
now.getMinutes().toString().padStart(2,'0');
}
updateClock();
setInterval(updateClock, 30000);
// Countdown
function updateCountdown() {
const launch = new Date('2026-06-15T07:00:00+02:00');
const now = new Date();
const diff = Math.max(0, launch - now);
const d = Math.floor(diff / 86400000);
const h = Math.floor((diff % 86400000) / 3600000);
const m = Math.floor((diff % 3600000) / 60000);
document.getElementById('cd-days').textContent = d;
document.getElementById('cd-hours').textContent = h.toString().padStart(2,'0');
document.getElementById('cd-mins').textContent = m.toString().padStart(2,'0');
}
updateCountdown();
setInterval(updateCountdown, 60000);
// Build checklist item
const ICONS = { done: '<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>', pending: '⏳' };
function buildItem(c) {
const div = document.createElement('div');
div.className = 'check-item';
div.innerHTML = `
<div class="check-icon ${c.status}">${ICONS[c.status] || '·'}</div>
<div class="check-body">
<div class="check-title">${c.label}</div>
<div class="check-detail">${c.detail || ''}</div>
</div>
<span class="check-badge ${c.status}">${c.status === 'done' ? 'Klar' : 'Väntar'}</span>
`;
return div;
}
// Load data
async function loadData() {
try {
// Use a dummy JWT for auth (dev mode)
const demoToken = 'eyJhbGciOiJub25lIn0.' +
btoa(JSON.stringify({ sub: 'launch-status-ui', email: 'bernt@wavult.com' })) + '.';
const [readRes, statsRes] = await Promise.all([
fetch('https://api.quixzoom.com/api/qz/launch-readiness', {
headers: { Authorization: 'Bearer ' + demoToken }
}),
fetch('https://api.quixzoom.com/api/qz/stats', {
headers: { Authorization: 'Bearer ' + demoToken }
})
]);
const data = await readRes.json();
const stats = statsRes.ok ? await statsRes.json() : null;
// Stats cards
const activeMissions = stats ? (stats.missions.open + stats.missions.active) : data.summary.done;
const activeZoomers = stats ? (stats.zoomers.available + stats.zoomers.busy) : 3;
document.getElementById('stat-missions').textContent = activeMissions;
document.getElementById('stat-zoomers').textContent = activeZoomers;
document.getElementById('stat-cities').textContent = data.summary.cities;
document.getElementById('stat-days').textContent = data.days_to_launch;
// Readiness
const pct = data.readiness_pct;
document.getElementById('readiness-pct').textContent = pct + '%';
document.getElementById('done-count').textContent = data.summary.done;
document.getElementById('total-count').textContent = data.summary.total;
document.getElementById('readiness-sub').textContent =
data.launch_go
? '🟢 Launch Go — klar för 1 augusti!'
: `${data.summary.pending} saker återstår innan launch`;
setTimeout(() => {
document.getElementById('progress-fill').style.width = pct + '%';
}, 100);
// Go badge
const badge = document.getElementById('go-badge');
if (data.launch_go) {
badge.textContent = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> GO';
badge.className = 'launch-badge';
} else {
badge.textContent = '⏳ ' + data.days_to_launch + 'd kvar';
badge.className = 'launch-badge warning';
}
// Cities
const citiesRow = document.getElementById('cities-row');
citiesRow.innerHTML = '';
(data.summary.cities_list || []).forEach(city => {
const chip = document.createElement('div');
chip.className = 'city-chip';
chip.innerHTML = `<span class="dot"></span>${city}`;
citiesRow.appendChild(chip);
});
// Checklist
const doneList = document.getElementById('done-list');
const pendingList = document.getElementById('pending-list');
doneList.innerHTML = '';
pendingList.innerHTML = '';
data.checklist
.filter(c => c.status === 'done')
.forEach(c => doneList.appendChild(buildItem(c)));
data.checklist
.filter(c => c.status !== 'done')
.forEach(c => pendingList.appendChild(buildItem(c)));
if (doneList.children.length === 0)
doneList.innerHTML = '<div class="check-item" style="justify-content:center;color:var(--ios-gray);font-size:14px;padding:20px;">Inga klara ännu</div>';
if (pendingList.children.length === 0)
pendingList.innerHTML = '<div class="check-item" style="justify-content:center;color:var(--ios-green);font-size:14px;padding:20px;">Allt klart! </div>';
// Timestamp
document.getElementById('last-updated').textContent =
'Uppdaterat ' + new Date().toLocaleTimeString('sv-SE', { hour:'2-digit', minute:'2-digit' });
} catch (err) {
console.error('Launch readiness load error:', err);
document.getElementById('readiness-sub').textContent = '<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> Kunde inte ladda data — kontrollera API';
document.getElementById('go-badge').textContent = '<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> Fel';
document.getElementById('go-badge').className = 'launch-badge warning';
}
}
// Load on start and auto-refresh every 60s
loadData();
setInterval(loadData, 60000);
</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>