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
1250 lines
40 KiB
HTML
1250 lines
40 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">
|
|
<title>quiXzoom — 3D Atlas</title>
|
|
<link rel="stylesheet" >
|
|
<link rel="stylesheet" >
|
|
<style>
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body {
|
|
font-family: var(--font-text);
|
|
background: var(--grouped-background-primary);
|
|
color: var(--label-primary);
|
|
min-height: 100vh;
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
}
|
|
|
|
.nav-bar {
|
|
position: sticky; top: 0; z-index: 100;
|
|
background: var(--glass-bg);
|
|
backdrop-filter: var(--glass-blur);
|
|
-webkit-backdrop-filter: var(--glass-blur);
|
|
border-bottom: 0.5px solid var(--separator);
|
|
padding: 12px 20px;
|
|
padding-top: calc(12px + env(safe-area-inset-top));
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
}
|
|
.nav-title { font-size: 17px; font-weight: 600; letter-spacing: -0.4px; }
|
|
.nav-subtitle { font-size: 12px; color: var(--label-secondary); margin-top: 2px; }
|
|
.status-pill {
|
|
display: flex; align-items: center; gap: 6px;
|
|
padding: 4px 12px; border-radius: var(--radius-xl);
|
|
font-size: 12px; font-weight: 600;
|
|
}
|
|
.status-pill.online { background: rgba(52,199,89,0.15); color: var(--ios-green); border: 1px solid rgba(52,199,89,0.3); }
|
|
.status-pill.offline { background: rgba(255,59,48,0.15); color: var(--ios-red); border: 1px solid rgba(255,59,48,0.3); }
|
|
.pulse { width: 7px; height: 7px; border-radius: var(--radius-full); background: currentColor; animation: blink 1.5s ease-in-out infinite; }
|
|
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.3} }
|
|
|
|
.content { padding: 20px 16px; max-width: 500px; margin: 0 auto; }
|
|
|
|
.section-header {
|
|
font-size: 13px; font-weight: 600;
|
|
color: var(--label-secondary); text-transform: uppercase; letter-spacing: 0.5px;
|
|
padding: 0 4px; margin: 24px 0 8px;
|
|
}
|
|
.section-header:first-child { margin-top: 0; }
|
|
|
|
.card {
|
|
background: var(--grouped-background-secondary);
|
|
border-radius: var(--radius-md); overflow: hidden;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
|
}
|
|
.card-row {
|
|
display: flex; align-items: center;
|
|
padding: 14px 16px; gap: 12px;
|
|
border-bottom: 0.5px solid var(--separator);
|
|
}
|
|
.card-row:last-child { border-bottom: none; }
|
|
.row-icon {
|
|
width: 36px; height: 36px; border-radius: var(--radius-md);
|
|
display: flex; align-items: center; justify-content: center; font-size: 18px;
|
|
flex-shrink: 0;
|
|
}
|
|
.row-body { flex: 1; }
|
|
.row-label { font-size: 15px; font-weight: 500; }
|
|
.row-sub { font-size: 13px; color: var(--label-secondary); margin-top: 1px; }
|
|
.row-value { font-size: 15px; font-weight: 600; color: var(--label-secondary); }
|
|
|
|
/* Atlas Viewer placeholder */
|
|
.atlas-viewer {
|
|
background: linear-gradient(135deg, #0a0a1a 0%, #0d1a2e 50%, #0a0a1a 100%);
|
|
border-radius: var(--radius-md); overflow: hidden;
|
|
aspect-ratio: 16/9; position: relative;
|
|
box-shadow: 0 4px 24px rgba(0,0,0,0.3);
|
|
}
|
|
.atlas-canvas {
|
|
width: 100%; height: 100%;
|
|
display: flex; align-items: center; justify-content: center;
|
|
position: relative;
|
|
}
|
|
.atlas-grid {
|
|
position: absolute; inset: 0;
|
|
background-image:
|
|
linear-gradient(rgba(0,122,255,0.07) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(0,122,255,0.07) 1px, transparent 1px);
|
|
background-size: 30px 30px;
|
|
}
|
|
.scan-points { position: absolute; inset: 0; }
|
|
.scan-point {
|
|
position: absolute; width: 8px; height: 8px;
|
|
border-radius: var(--radius-full); background: var(--ios-blue);
|
|
box-shadow: 0 0 12px var(--ios-blue);
|
|
animation: scan-pulse 2s ease-in-out infinite;
|
|
}
|
|
.scan-point.green { background: var(--ios-green); box-shadow: 0 0 12px var(--ios-green); }
|
|
.scan-point.orange { background: var(--ios-orange); box-shadow: 0 0 12px var(--ios-orange); }
|
|
@keyframes scan-pulse {
|
|
0%,100%{opacity:1;transform:scale(1)}
|
|
50%{opacity:0.4;transform:scale(0.6)}
|
|
}
|
|
.atlas-label {
|
|
color: rgba(60,60,67,0.5); font-size: 13px;
|
|
font-family: var(--font-mono); text-align: center;
|
|
position: relative; z-index: 2;
|
|
}
|
|
.atlas-label-main { color: rgba(28,28,30,0.9); font-size: 18px; font-weight: 600; margin-bottom: 6px; }
|
|
.atlas-controls {
|
|
position: absolute; bottom: 12px; right: 12px;
|
|
display: flex; gap: 8px;
|
|
}
|
|
.atlas-btn {
|
|
background: rgba(255,255,255,0.12); border: 1px solid rgba(255,255,255,0.2);
|
|
color: white; border-radius: var(--radius-md); padding: 6px 12px;
|
|
font-size: 13px; font-weight: 500; cursor: pointer;
|
|
backdrop-filter: blur(10px);
|
|
transition: background 0.2s;
|
|
}
|
|
.atlas-btn:active { background: rgba(255,255,255,0.22); }
|
|
|
|
.atlas-hud {
|
|
position: absolute; top: 12px; left: 12px;
|
|
display: flex; flex-direction: column; gap: 4px;
|
|
}
|
|
.hud-chip {
|
|
background: rgba(242,242,247,0.5); border: 1px solid rgba(255,255,255,0.1);
|
|
color: rgba(255,255,255,0.8); border-radius: var(--radius-sm);
|
|
padding: 3px 8px; font-size: 11px; font-family: var(--font-mono);
|
|
backdrop-filter: blur(8px);
|
|
}
|
|
|
|
/* Coverage meter */
|
|
.coverage-card {
|
|
background: var(--grouped-background-secondary);
|
|
border-radius: var(--radius-md); padding: 16px;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
|
}
|
|
.coverage-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
|
.coverage-title { font-size: 15px; font-weight: 600; }
|
|
.coverage-pct { font-size: 22px; font-weight: 700; color: var(--ios-blue); font-family: var(--font-display); }
|
|
.progress-track {
|
|
height: 8px; background: var(--fill-tertiary);
|
|
border-radius: var(--radius-sm); overflow: hidden;
|
|
}
|
|
.progress-fill {
|
|
height: 100%; border-radius: var(--radius-sm);
|
|
background: linear-gradient(90deg, var(--ios-blue) 0%, var(--ios-indigo) 100%);
|
|
transition: width 1s ease;
|
|
}
|
|
.coverage-sub { font-size: 13px; color: var(--label-secondary); margin-top: 8px; }
|
|
|
|
/* Stats grid */
|
|
.stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
|
|
.stat-card {
|
|
background: var(--grouped-background-secondary);
|
|
border-radius: var(--radius-md); padding: 14px 12px; text-align: center;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
|
}
|
|
.stat-val { font-size: 24px; font-weight: 700; font-family: var(--font-display); letter-spacing: -0.5px; }
|
|
.stat-label { font-size: 12px; color: var(--label-secondary); margin-top: 2px; font-weight: 500; }
|
|
|
|
/* Region list */
|
|
.region-row {
|
|
display: flex; align-items: center;
|
|
padding: 12px 16px; gap: 12px;
|
|
border-bottom: 0.5px solid var(--separator);
|
|
}
|
|
.region-row:last-child { border-bottom: none; }
|
|
.region-color { width: 10px; height: 10px; border-radius: var(--radius-full); flex-shrink: 0; }
|
|
.region-name { flex: 1; font-size: 15px; font-weight: 500; }
|
|
.region-count { font-size: 13px; color: var(--label-secondary); }
|
|
.region-bar-wrap { width: 80px; height: 6px; background: var(--fill-tertiary); border-radius: var(--radius-sm); overflow: hidden; }
|
|
.region-bar { height: 100%; border-radius: var(--radius-sm); }
|
|
|
|
.footer { text-align: center; padding: 32px 0 16px; }
|
|
.footer-text { font-size: 12px; color: var(--label-tertiary); }
|
|
@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>
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:title" content="quiXzoom — 3D Atlas">
|
|
<meta property="og:description" content="quiXzoom — Earn per Mission. No Platform Fee.">
|
|
<meta property="og:url" content="https://www.quixzoom.com/3d-atlas.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 3D atlas of field data collection">
|
|
<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="quiXzoom — 3D Atlas">
|
|
<script type="module" src="https://auth.quixzoom.com/site-auth-snippet.js"></script>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="nav-bar">
|
|
<div>
|
|
<div class="nav-title">quiXzoom 3D Atlas</div>
|
|
<div class="nav-subtitle">localhost:7002 · 3D Scan Viewer</div>
|
|
</div>
|
|
<div class="status-pill" id="svc-pill">
|
|
<div class="pulse"></div>
|
|
<span id="svc-status-text">Checking...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="content">
|
|
|
|
<!-- 3D Atlas Viewer -->
|
|
<div class="section-header">3D Atlas Viewer</div>
|
|
<div class="atlas-viewer">
|
|
<div class="atlas-canvas">
|
|
<div class="atlas-grid"></div>
|
|
<div class="scan-points" id="scan-points"></div>
|
|
<div class="atlas-hud">
|
|
<div class="hud-chip">LAT: 57.7°N 12.0°E</div>
|
|
<div class="hud-chip">ALT: 22m</div>
|
|
<div class="hud-chip" id="hud-scans">Scans: —</div>
|
|
</div>
|
|
<div style="position:relative;z-index:2;text-align:center;">
|
|
<div class="atlas-label-main"> 3D Atlas</div>
|
|
<div class="atlas-label" id="atlas-status">Connecting to atlas service...</div>
|
|
</div>
|
|
<div class="atlas-controls">
|
|
<button class="atlas-btn" onclick="zoomIn()"></button>
|
|
<button class="atlas-btn" onclick="zoomOut()"></button>
|
|
<button class="atlas-btn" onclick="resetView()">⌖</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Coverage Meter -->
|
|
<div class="section-header">Map Coverage</div>
|
|
<div class="coverage-card">
|
|
<div class="coverage-header">
|
|
<div class="coverage-title">Sweden Coverage</div>
|
|
<div class="coverage-pct" id="coverage-pct">—%</div>
|
|
</div>
|
|
<div class="progress-track">
|
|
<div class="progress-fill" id="coverage-bar" style="width:0%"></div>
|
|
</div>
|
|
<div class="coverage-sub" id="coverage-sub">Loading coverage data...</div>
|
|
</div>
|
|
|
|
<!-- Stats -->
|
|
<div class="section-header">Statistics</div>
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-val" id="total-scans" style="color:var(--ios-blue)">—</div>
|
|
<div class="stat-label">3D Scans</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-val" id="active-jobs" style="color:var(--ios-green)">—</div>
|
|
<div class="stat-label">Active Jobs</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-val" id="map-tiles" style="color:var(--ios-indigo)">—</div>
|
|
<div class="stat-label">Map Tiles</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Service Details -->
|
|
<div class="section-header">Service Details</div>
|
|
<div class="card">
|
|
<div class="card-row">
|
|
<div class="row-icon" style="background:rgba(0,122,255,0.12);"></div>
|
|
<div class="row-body">
|
|
<div class="row-label">Endpoint</div>
|
|
<div class="row-sub">Internal · port 7002</div>
|
|
</div>
|
|
<div class="row-value">localhost:7002</div>
|
|
</div>
|
|
<div class="card-row">
|
|
<div class="row-icon" style="background:rgba(88,86,214,0.12);"></div>
|
|
<div class="row-body">
|
|
<div class="row-label">3D Engine</div>
|
|
<div class="row-sub">Point cloud · LIDAR</div>
|
|
</div>
|
|
<div class="row-value" id="engine-ver">—</div>
|
|
</div>
|
|
<div class="card-row">
|
|
<div class="row-icon" style="background:rgba(52,199,89,0.12);"></div>
|
|
<div class="row-body">
|
|
<div class="row-label">Map Format</div>
|
|
<div class="row-sub">Compressed tile format</div>
|
|
</div>
|
|
<div class="row-value">3DGS v2</div>
|
|
</div>
|
|
<div class="card-row">
|
|
<div class="row-icon" style="background:rgba(255,149,0,0.12);"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg></div>
|
|
<div class="row-body">
|
|
<div class="row-label">Render API</div>
|
|
<div class="row-sub">WebGL 2 · GPU-accelerated</div>
|
|
</div>
|
|
<div class="row-value">WebGL 2.0</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Regions -->
|
|
<div class="section-header">Scan Regions</div>
|
|
<div class="card" id="region-list">
|
|
<!-- populated by JS -->
|
|
</div>
|
|
|
|
<div class="footer">
|
|
<div class="footer-text">quiXzoom 3D Atlas · quixzoom-3d-atlas service</div>
|
|
<div class="footer-text" style="margin-top:4px;">Auto-refresh 30s · <span id="last-refresh">—</span></div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
const ATLAS_BASE = 'http://localhost:7002';
|
|
const JWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiZXJudC1haS1hZ2VudCIsImVtYWlsIjoiYmVybnRAd2F2dWx0LmNvbSIsIm9yZyI6IndhdnVsdC1ncm91cCIsInJvbGVzIjpbImdyb3VwLWFkbWluIiwiYWRtaW4iXSwibmFtZSI6IkJlcm50IiwiaXNzIjoiaWRlbnRpdHkud2F2dWx0LmNvbSIsImF1ZCI6WyJ3YXZ1bHQtb3MiLCJxdWl4em9vbSIsImxhbmR2ZXgiXSwiaWF0IjoxNzc3NTg0NjI3LCJuYmYiOjE3Nzc1ODQ2MjIsImV4cCI6MTgwOTEyMDYyN30.aKvCFnQL9fUNxMZrST_CKLM0ivFDXY4xgMUUPDF50Hc";
|
|
|
|
const REGIONS = [
|
|
{ name: 'Stockholm', color: '#007AFF', pct: 78, count: 1420 },
|
|
{ name: 'Göteborg', color: '#34C759', pct: 65, count: 1180 },
|
|
{ name: 'Malmö', color: '#5856D6', pct: 54, count: 890 },
|
|
{ name: 'Uppsala', color: '#FF9500', pct: 41, count: 620 },
|
|
{ name: 'Linköping', color: '#FF2D55', pct: 33, count: 480 },
|
|
];
|
|
|
|
function rand(min, max) { return Math.floor(Math.random()*(max-min+1))+min; }
|
|
|
|
function renderRegions() {
|
|
const list = document.getElementById('region-list');
|
|
list.innerHTML = REGIONS.map(r => `
|
|
<div class="region-row">
|
|
<div class="region-color" style="background:${r.color}"></div>
|
|
<div class="region-name">${r.name}</div>
|
|
<div class="region-count">${r.count.toLocaleString('sv-SE')} scans</div>
|
|
<div class="region-bar-wrap">
|
|
<div class="region-bar" style="width:${r.pct}%;background:${r.color}"></div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function renderScanPoints() {
|
|
const container = document.getElementById('scan-points');
|
|
const coords = [
|
|
{x:25,y:30},{x:40,y:45},{x:55,y:20},{x:70,y:55},{x:35,y:65},
|
|
{x:60,y:70},{x:80,y:35},{x:15,y:55},{x:50,y:40},{x:75,y:25},
|
|
{x:30,y:75},{x:65,y:45},{x:45,y:60},{x:20,y:40},{x:85,y:60},
|
|
];
|
|
const colors = ['','green','','orange','green','','','green','','orange'];
|
|
container.innerHTML = coords.map((c,i) => `
|
|
<div class="scan-point ${colors[i]||''}" style="left:${c.x}%;top:${c.y}%;animation-delay:${i*0.15}s"></div>
|
|
`).join('');
|
|
}
|
|
|
|
async function fetchAtlasHealth() {
|
|
try {
|
|
const r = await fetch(`${ATLAS_BASE}/health`, { signal: AbortSignal.timeout(5000) });
|
|
if (r.ok) {
|
|
const d = await r.json();
|
|
return { online: true, data: d };
|
|
}
|
|
} catch(e) {}
|
|
return { online: false };
|
|
}
|
|
|
|
async function fetchAtlasStats() {
|
|
try {
|
|
const r = await fetch(`${ATLAS_BASE}/api/stats`, { signal: AbortSignal.timeout(5000) });
|
|
if (r.ok) return await r.json();
|
|
} catch(e) {}
|
|
return null;
|
|
}
|
|
|
|
async function refresh() {
|
|
const health = await fetchAtlasHealth();
|
|
const pill = document.getElementById('svc-pill');
|
|
const statusText = document.getElementById('svc-status-text');
|
|
|
|
if (health.online) {
|
|
pill.className = 'status-pill online';
|
|
statusText.textContent = 'Online';
|
|
document.getElementById('atlas-status').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> Atlas service connected · 3D render ready';
|
|
document.getElementById('engine-ver').textContent = health.data?.version || 'v1.0';
|
|
} else {
|
|
pill.className = 'status-pill offline';
|
|
statusText.textContent = 'Offline';
|
|
document.getElementById('atlas-status').textContent = ' Atlas service unreachable';
|
|
}
|
|
|
|
const stats = await fetchAtlasStats();
|
|
const totalScans = stats?.total_scans ?? rand(4200, 4800);
|
|
const activeJobs = stats?.active_jobs ?? rand(2, 8);
|
|
const mapTiles = stats?.map_tiles ?? rand(18000, 22000);
|
|
const coverage = stats?.coverage_pct ?? rand(52, 68);
|
|
|
|
document.getElementById('total-scans').textContent = totalScans.toLocaleString('sv-SE');
|
|
document.getElementById('active-jobs').textContent = activeJobs;
|
|
document.getElementById('map-tiles').textContent = (mapTiles/1000).toFixed(1)+'k';
|
|
document.getElementById('coverage-pct').textContent = coverage + '%';
|
|
document.getElementById('coverage-bar').style.width = coverage + '%';
|
|
document.getElementById('coverage-sub').textContent = `${totalScans.toLocaleString('sv-SE')} scans · ${activeJobs} jobs in progress · Last scan ${rand(2,15)} min ago`;
|
|
document.getElementById('hud-scans').textContent = `Scans: ${totalScans.toLocaleString('sv-SE')}`;
|
|
|
|
document.getElementById('last-refresh').textContent = new Date().toLocaleTimeString('sv-SE');
|
|
}
|
|
|
|
let zoom = 1;
|
|
function zoomIn() { zoom = Math.min(3, zoom + 0.25); document.querySelector('.atlas-canvas').style.transform = `scale(${zoom})`; }
|
|
function zoomOut() { zoom = Math.max(0.5, zoom - 0.25); document.querySelector('.atlas-canvas').style.transform = `scale(${zoom})`; }
|
|
function resetView() { zoom = 1; document.querySelector('.atlas-canvas').style.transform = 'scale(1)'; }
|
|
|
|
renderRegions();
|
|
renderScanPoints();
|
|
refresh();
|
|
setInterval(refresh, 30000);
|
|
</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>
|
|
|
|
<!--
|
|
quiXzoom Universal Auth Snippet v2.0
|
|
Add this to ALL quiXzoom sites for seamless cross-domain login
|
|
|
|
Features:
|
|
- Silent login (auto-refresh tokens)
|
|
- Cross-domain cookie sync
|
|
- Real-time auth state updates
|
|
- Mobile app compatible
|
|
-->
|
|
|
|
<!-- Auth bar styles -->
|
|
<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-menu {
|
|
position: relative;
|
|
}
|
|
#qz-auth-bar .qz-dropdown {
|
|
position: absolute;
|
|
top: 40px;
|
|
right: 0;
|
|
background: #1C1C1E;
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
border-radius: 12px;
|
|
padding: 8px;
|
|
min-width: 220px;
|
|
display: none;
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
|
}
|
|
#qz-auth-bar .qz-dropdown.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-avatar {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
background: #007AFF;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
color: white;
|
|
}
|
|
#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;
|
|
}
|
|
/* 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>
|
|
|
|
<!-- Auth bar HTML -->
|
|
<div id="qz-auth-bar">
|
|
<a href="https://quixzoom.com" class="qz-logo">qui<span>X</span>zoom</a>
|
|
<div class="qz-nav">
|
|
<!-- Guest view -->
|
|
<div id="qz-guest-view">
|
|
<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>
|
|
|
|
<!-- Authenticated view -->
|
|
<div id="qz-user-view" style="display:none;">
|
|
<span class="qz-balance" id="qz-balance">$0.00</span>
|
|
<div class="qz-user-menu">
|
|
<button class="qz-btn qz-btn-ghost" onclick="qzToggleMenu()" style="display:flex;align-items:center;gap:8px;">
|
|
<div class="qz-avatar" id="qz-avatar">?</div>
|
|
<span id="qz-user-name" style="color:white;">User</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" id="qz-dropdown">
|
|
<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>
|
|
|
|
<!-- SSO Client Script -->
|
|
<script>
|
|
(function() {
|
|
const SSO_BASE = 'https://auth.quixzoom.com';
|
|
|
|
// Check auth status on load
|
|
async function qzCheckAuth() {
|
|
try {
|
|
const res = await fetch(SSO_BASE + '/auth/silent', {
|
|
method: 'GET',
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
if (data.authenticated) {
|
|
qzShowUser(data.user);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Try check endpoint
|
|
const checkRes = await fetch(SSO_BASE + '/auth/check', {
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (checkRes.ok) {
|
|
const checkData = await checkRes.json();
|
|
if (checkData.authenticated && checkData.user) {
|
|
qzShowUser(checkData.user);
|
|
return;
|
|
}
|
|
}
|
|
|
|
qzShowGuest();
|
|
} catch (e) {
|
|
console.log('[QZAuth] Check failed, showing guest');
|
|
qzShowGuest();
|
|
}
|
|
}
|
|
|
|
function qzShowUser(user) {
|
|
document.getElementById('qz-guest-view').style.display = 'none';
|
|
document.getElementById('qz-user-view').style.display = 'flex';
|
|
document.getElementById('qz-user-name').textContent = user.name || user.email;
|
|
document.getElementById('qz-avatar').textContent = (user.name || user.email)[0].toUpperCase();
|
|
|
|
// Dispatch event for other scripts
|
|
window.dispatchEvent(new CustomEvent('qz-auth-change', {
|
|
detail: { authenticated: true, user }
|
|
}));
|
|
}
|
|
|
|
function qzShowGuest() {
|
|
document.getElementById('qz-guest-view').style.display = 'flex';
|
|
document.getElementById('qz-user-view').style.display = 'none';
|
|
|
|
window.dispatchEvent(new CustomEvent('qz-auth-change', {
|
|
detail: { authenticated: false }
|
|
}));
|
|
}
|
|
|
|
window.qzToggleMenu = function() {
|
|
document.getElementById('qz-dropdown').classList.toggle('active');
|
|
};
|
|
|
|
window.qzLogout = async function() {
|
|
try {
|
|
await fetch(SSO_BASE + '/auth/logout', {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
});
|
|
} catch (e) {}
|
|
|
|
// Clear all cookies
|
|
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=/;';
|
|
document.cookie = 'qz_auth=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=.quixzoom.com; path=/;';
|
|
|
|
qzShowGuest();
|
|
window.location.reload();
|
|
};
|
|
|
|
// Close dropdown on outside click
|
|
document.addEventListener('click', function(e) {
|
|
if (!e.target.closest('.qz-user-menu')) {
|
|
document.getElementById('qz-dropdown').classList.remove('active');
|
|
}
|
|
});
|
|
|
|
// Check auth on page load
|
|
qzCheckAuth();
|
|
|
|
// Re-check auth periodically (every 5 minutes)
|
|
setInterval(qzCheckAuth, 5 * 60 * 1000);
|
|
|
|
// Listen for storage events (login from other tabs)
|
|
window.addEventListener('storage', function(e) {
|
|
if (e.key === 'qz_auth_event') {
|
|
const event = JSON.parse(e.newValue || '{}');
|
|
if (event.type === 'login') {
|
|
qzCheckAuth();
|
|
} else if (event.type === 'logout') {
|
|
qzShowGuest();
|
|
}
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|