Files
boc/quixzoom-landing-fixed/customer-dashboard.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

1298 lines
51 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="sv" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="color-scheme" content="light">
<meta name="apple-native-compliance" content="pass">
<title>quiXzoom — Kunddashboard</title>
<link rel="stylesheet" >
<link rel="stylesheet" >
<script defer></script>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html{-webkit-text-size-adjust:100%;color-scheme:light}
body{font-family:-apple-system,'SF Pro Text',system-ui,sans-serif;background:#F2F2F7;color:#000;-webkit-font-smoothing:antialiased;min-height:100dvh;padding-bottom:calc(83px + env(safe-area-inset-bottom,16px))}
/* Nav */
.nav-bar{position:sticky;top:0;z-index:200;padding-top:env(safe-area-inset-top,0px);background:rgba(249,249,249,.94);backdrop-filter:blur(40px) saturate(180%);-webkit-backdrop-filter:blur(40px) saturate(180%);border-bottom:.33px solid rgba(60,60,67,.29)}
.nav-inner{height:44px;display:flex;align-items:center;padding:0 16px;gap:12px}
.nav-title{flex:1;text-align:center;font-size:17px;font-weight:600;letter-spacing:-.4px}
.nav-logo{font-size:20px;font-weight:900;background:linear-gradient(135deg,#FF6B35,#F7C948);-webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-.5px}
.nav-btn-icon{width:32px;height:32px;border-radius: var(--radius-lg);background:#007AFF;color:#fff;border:none;font-size:14px;display:flex;align-items:center;justify-content:center;cursor:pointer}
/* Customer switcher */
.customer-switcher{background:#fff;border-bottom:.33px solid rgba(60,60,67,.12);padding:10px 16px;display:flex;gap:8px;overflow-x:auto;scrollbar-width:none}
.customer-switcher::-webkit-scrollbar{display:none}
.cust-chip{padding:7px 14px;border-radius: var(--radius-xl);font-size:13px;font-weight:600;cursor:pointer;border:none;background:#F2F2F7;color:#000;white-space:nowrap;transition:all .15s;-webkit-tap-highlight-color:transparent}
.cust-chip.active{background:#007AFF;color:#fff}
/* Large title */
.large-title-area{padding:12px 16px 8px}
.large-title{font-size:34px;font-weight:700;letter-spacing:.37px;line-height:1.1}
.large-sub{font-size:15px;color:#666666;margin-top:3px}
/* Tab bar */
.tab-bar{position:fixed;bottom:0;left:0;right:0;z-index:200;padding-bottom:env(safe-area-inset-bottom,0px);background:rgba(249,249,249,.94);backdrop-filter:blur(40px) saturate(180%);-webkit-backdrop-filter:blur(40px) saturate(180%);border-top:.33px solid rgba(60,60,67,.29);display:flex}
.tab-item{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:8px 4px 4px;cursor:pointer;gap:2px;-webkit-tap-highlight-color:transparent}
.tab-icon{font-size:22px;line-height:1}
.tab-label{font-size:10px;font-weight:500;color:#666666}
.tab-item.active .tab-label{color:#007AFF}
.tab-item:not(.active) .tab-icon{opacity:.5}
/* Content */
.content{display:none;padding-bottom:8px}
.content.active{display:block}
/* KPI strip */
.kpi-strip{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;padding:12px 16px 8px}
.kpi-box{background:#fff;border-radius: var(--radius-md);padding:14px 12px;text-align:center;box-shadow:0 1px 0 rgba(0,0,0,.08)}
.kpi-val{font-size:20px;font-weight:700;letter-spacing:-.5px}
.kpi-val-accent{color:#FF6B35}
.kpi-lbl{font-size:10px;color:#666666;font-weight:500;margin-top:2px}
/* Section */
.section{margin:8px 0}
.section-header{font-size:13px;font-weight:600;color:#555555;text-transform:uppercase;padding:0 32px 6px}
/* Card list */
.card-list{background:#fff;border-radius: var(--radius-md);margin:0 16px;overflow:hidden;box-shadow:0 1px 0 rgba(0,0,0,.08)}
.card-item{display:flex;align-items:center;gap:12px;padding:13px 16px;border-bottom:.33px solid rgba(60,60,67,.12);-webkit-tap-highlight-color:transparent;transition:background .1s}
.card-item:last-child{border-bottom:none}
.card-item:active{background:#F2F2F7}
.card-icon{width:40px;height:40px;border-radius: var(--radius-md);display:flex;align-items:center;justify-content:center;font-size:20px;flex-shrink:0}
.ci-blue{background:#EBF5FF}.ci-green{background:#E8F8EE}.ci-orange{background:#FFF3E0}.ci-red{background:#FFE8E6}.ci-purple{background:#F4EEFF}.ci-gray{background:#F2F2F7}
.card-body{flex:1;min-width:0}
.card-title{font-size:15px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.card-sub{font-size:13px;color:#666666;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.card-right{display:flex;align-items:center;gap:6px;flex-shrink:0}
.chevron{color:#C7C7CC;font-size:16px}
/* Badges */
.badge{display:inline-flex;align-items:center;padding:3px 8px;border-radius: var(--radius-sm);font-size:12px;font-weight:600;white-space:nowrap}
.bg-green{background:#E8F8EE;color:#1D7A3F}
.bg-orange{background:#FFF3E0;color:#C07000}
.bg-blue{background:#EBF5FF;color:#0051A8}
.bg-red{background:#FFE8E6;color:#C0392B}
.bg-gray{background:#F2F2F7;color:#555555}
.bg-purple{background:#F4EEFF;color:#6B2FA0}
/* Star rating */
.stars{color:#FFD60A;font-size:13px}
/* Favorite zoomer card */
.fav-card{background:#fff;border-radius: var(--radius-lg);margin:0 16px;padding:16px;display:flex;align-items:center;gap:14px;box-shadow:0 1px 0 rgba(0,0,0,.08);margin-bottom:8px}
.fav-avatar{width:52px;height:52px;border-radius: 26px;background:linear-gradient(135deg,#FF6B35,#F7C948);display:flex;align-items:center;justify-content:center;font-size:24px;flex-shrink:0}
.fav-name{font-size:16px;font-weight:700}
.fav-spec{font-size:13px;color:#666666}
.fav-meta{font-size:12px;color:#666666;margin-top:4px}
.fav-actions{margin-left:auto;display:flex;gap:8px}
/* Chart bar */
.bar-chart-wrap{padding:16px}
.bar-chart-title{font-size:15px;font-weight:600;margin-bottom:14px}
.bar-row{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.bar-label{font-size:12px;color:#555555;width:80px;flex-shrink:0}
.bar-track{flex:1;height:8px;background:#E9E9EB;border-radius: var(--radius-sm);overflow:hidden}
.bar-fill{height:8px;border-radius: var(--radius-sm);background:linear-gradient(90deg,#FF6B35,#F7C948);transition:width .7s ease}
.bar-val{font-size:12px;color:#555555;width:80px;text-align:right;flex-shrink:0}
/* Book modal */
.modal-overlay{position:fixed;inset:0;background: rgba(242,242,247,.4);z-index:300;display:none;align-items:flex-end}
.modal-overlay.open{display:flex}
.modal{background:#fff;border-radius: var(--radius-xl) 20px 0 0;padding:20px 20px calc(20px + env(safe-area-inset-bottom,0px));width:100%;max-height:90dvh;overflow-y:auto}
.modal-handle{width:36px;height:4px;border-radius: var(--radius-sm);background:#C7C7CC;margin:0 auto 16px}
.modal-title{font-size:20px;font-weight:700;margin-bottom:4px}
.modal-sub{font-size:14px;color:#666666;margin-bottom:20px}
.form-group{margin-bottom:14px}
.form-label{font-size:13px;font-weight:600;color:#555555;margin-bottom:6px;display:block}
.form-input,.form-select{width:100%;background:#F2F2F7;border:none;border-radius: var(--radius-md);padding:12px 14px;color:#000;font-size:15px;font-family:inherit;outline:none}
.btn-row{display:flex;flex-direction:column;gap:10px;margin-top:20px}
.btn{display:flex;align-items:center;justify-content:center;gap:8px;padding:13px;border-radius: var(--radius-md);font-size:17px;font-weight:600;cursor:pointer;border:none;-webkit-tap-highlight-color:transparent;transition:opacity .15s}
.btn:active{opacity:.7}
.btn-orange{background:linear-gradient(135deg,#FF6B35,#FF9500);color:#fff}
.btn-gray{background:#F2F2F7;color:#000}
/* Empty */
.empty{padding:32px;text-align:center;color:#666666}
.empty-icon{font-size:36px;margin-bottom:8px}
/* Toast */
.toast{position:fixed;bottom:calc(83px + 12px + env(safe-area-inset-bottom,0px));left:50%;transform:translateX(-50%) translateY(60px);background:#323232;color:#fff;padding:10px 18px;border-radius: var(--radius-md);font-size:14px;font-weight:600;z-index:999;transition:transform .3s;white-space:nowrap}
.toast.show{transform:translateX(-50%) translateY(0)}
@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>
<!-- Nav -->
<div class="nav-bar">
<div class="nav-inner">
<div class="nav-logo">quiX</div>
<div class="nav-title" id="nav-title">Kunddashboard</div>
<button class="btn btn-orange" style="padding:7px 14px;font-size:14px;font-weight:700;border-radius: var(--radius-md);border:none;color:#fff;background:linear-gradient(135deg,#FF6B35,#FF9500);cursor:pointer" onclick="openBookModal()">+ Boka</button>
</div>
</div>
<!-- Customer switcher -->
<div class="customer-switcher" id="customer-switcher"></div>
<!-- Large title -->
<div class="large-title-area">
<div class="large-title" id="large-title">Laddar…</div>
<div class="large-sub" id="large-sub"></div>
</div>
<!-- DASHBOARD -->
<div class="content active" id="tab-dashboard">
<div class="kpi-strip" id="kpi-strip"></div>
<div style="height:8px"></div>
<div class="section">
<div class="section-header">Aktiva uppdrag</div>
<div class="card-list" id="active-missions"></div>
</div>
<div style="height:8px"></div>
<div class="section">
<div class="section-header">Senaste uppdrag</div>
<div class="card-list" id="recent-missions"></div>
</div>
</div>
<!-- HISTORY -->
<div class="content" id="tab-history">
<div style="height:12px"></div>
<div style="background:#fff;border-radius: var(--radius-md);margin:0 16px" id="spending-chart"></div>
<div style="height:12px"></div>
<div class="section">
<div class="section-header">Alla uppdrag</div>
<div class="card-list" id="all-missions"></div>
</div>
</div>
<!-- BILLING -->
<div class="content" id="tab-billing">
<div style="height:12px"></div>
<div class="section">
<div class="section-header">Fakturor</div>
<div class="card-list" id="invoices-list"></div>
</div>
</div>
<!-- FAVORITES -->
<div class="content" id="tab-favorites">
<div style="height:12px"></div>
<div style="padding:0 16px 12px;display:flex;justify-content:flex-end">
<button class="btn btn-orange" style="font-size:14px;padding:8px 16px;border-radius: var(--radius-md);border:none;color:#fff;background:linear-gradient(135deg,#FF6B35,#FF9500);cursor:pointer" onclick="openAddFavModal()">+ Favorit</button>
</div>
<div id="favs-list"></div>
</div>
<!-- Tab bar -->
<nav class="qz-nav" class="tab-bar">
<div class="tab-item active" onclick="showTab('dashboard',this,'Hem')"><div class="tab-icon"></div><div class="tab-label">Hem</div></div>
<div class="tab-item" onclick="showTab('history',this,'Historik')"><div class="tab-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H10M6 8H10M6 11H8" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg></div><div class="tab-label">Historik</div></div>
<div class="tab-item" onclick="showTab('billing',this,'Fakturering')"><div class="tab-icon"></div><div class="tab-label">Fakturering</div></div>
<div class="tab-item" onclick="showTab('favorites',this,'Favoriter')"><div class="tab-icon"></div><div class="tab-label">Favoriter</div></div>
</nav>
<!-- Book modal -->
<div class="modal-overlay" id="book-modal">
<div class="modal">
<div class="modal-handle"></div>
<div class="modal-title">Boka uppdrag</div>
<div class="modal-sub" id="book-sub">Beskriv ditt uppdrag</div>
<div class="form-group"><label class="form-label">Titel</label><input class="form-input" id="bk-title" placeholder="Vad behöver du hjälp med?"></div>
<div class="form-group"><label class="form-label">Typ</label><select class="form-select" id="bk-type"><option value="photography">Fotografi </option><option value="video">Video </option><option value="drone">Drönare <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg></option></select></div>
<div class="form-group"><label class="form-label">Startdatum & tid</label><input class="form-input" type="datetime-local" id="bk-start"></div>
<div class="form-group"><label class="form-label">Duration (timmar)</label><input class="form-input" type="number" id="bk-duration" value="2" min="1"></div>
<div class="form-group"><label class="form-label">Plats</label><input class="form-input" id="bk-location" placeholder="Var ska uppdraget utföras?"></div>
<div class="form-group"><label class="form-label">Budget (SEK)</label><input class="form-input" type="number" id="bk-budget" placeholder="2500"></div>
<div class="btn-row">
<button class="btn btn-orange" onclick="submitBook()"><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> Boka nu</button>
<button class="btn btn-gray" onclick="closeModal('book-modal')">Avbryt</button>
</div>
</div>
</div>
<!-- Add favorite modal -->
<div class="modal-overlay" id="fav-modal">
<div class="modal">
<div class="modal-handle"></div>
<div class="modal-title">Lägg till favorit</div>
<div class="modal-sub">Lägg till en zoomer som favorit</div>
<div class="form-group"><label class="form-label">Zoomer ID</label><input class="form-input" id="fav-id" placeholder="t.ex. zoomer-xyz"></div>
<div class="form-group"><label class="form-label">Namn</label><input class="form-input" id="fav-name" placeholder="Förnamn E."></div>
<div class="form-group"><label class="form-label">Specialitet</label><select class="form-select" id="fav-spec"><option>Photography</option><option>Video</option><option>Drone</option><option>General</option></select></div>
<div class="btn-row">
<button class="btn btn-orange" onclick="submitFav()"> Lägg till</button>
<button class="btn btn-gray" onclick="closeModal('fav-modal')">Avbryt</button>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
// State
const CUSTOMERS = ['cust-101','cust-102','cust-103'];
let activeCustomer = 'cust-101';
let customerCache = {};
let missionCache = {};
let spendingCache = {};
let invoiceCache = {};
let favCache = {};
// Utils
function fmtMoney(n){return new Intl.NumberFormat('sv-SE').format(n)+' kr'}
function fmtDate(s){if(!s)return'—';return new Date(s).toLocaleDateString('sv-SE',{day:'2-digit',month:'short'})}
function fmtDateLong(s){if(!s)return'—';return new Date(s).toLocaleDateString('sv-SE',{day:'2-digit',month:'long',year:'numeric'})}
function toast(msg){const el=document.getElementById('toast');el.textContent=msg;el.classList.add('show');setTimeout(()=>el.classList.remove('show'),2500)}
function statusBadge(s){return{completed:'<span class="badge bg-green"><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> Klar</span>',active:'<span class="badge bg-blue"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8C2 11.3 4.7 14 8 14C10.5 14 12.7 12.5 13.7 10.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M14 8C14 4.7 11.3 2 8 2C5.5 2 3.3 3.5 2.3 5.7" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M13.5 7.5L14 10L11.5 9.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 8.5L2 6L4.5 6.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> Aktiv</span>',open:'<span class="badge bg-orange"><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> Öppen</span>',cancelled:'<span class="badge bg-red"><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> Avbruten</span>'}[s]||`<span class="badge bg-gray">${s}</span>`}
function missionIcon(t){return{photography:'',video:'',drone:'<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg>'}[t]||'<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>'}
function missionIconClass(s){return{completed:'ci-green',active:'ci-blue',open:'ci-orange',cancelled:'ci-red'}[s]||'ci-gray'}
function stars(r){if(!r)return'<span style="color:#666666;font-size:12px">Väntar</span>';return'<span class="stars">'+(''.repeat(Math.round(r))+''.repeat(5-Math.round(r)))+'</span><span style="font-size:12px;color:#666666;margin-left:4px">'+r+'</span>'}
function invStatusBadge(s){return s==='paid'?'<span class="badge bg-green">Betald</span>':'<span class="badge bg-orange">Obetald</span>'}
function closeModal(id){document.getElementById(id).classList.remove('open')}
document.querySelectorAll('.modal-overlay').forEach(m=>m.addEventListener('click',e=>{if(e.target===m)m.classList.remove('open')}));
// Tab
function showTab(id,el,title){
document.querySelectorAll('.content').forEach(c=>c.classList.remove('active'));
document.querySelectorAll('.tab-item').forEach(t=>t.classList.remove('active'));
document.getElementById('tab-'+id).classList.add('active');
el.classList.add('active');
document.getElementById('nav-title').textContent=title;
if(id==='history')renderHistory();
if(id==='billing')renderBilling();
if(id==='favorites')renderFavorites();
}
// Customer switcher
function renderSwitcher(){
const c = Object.values(customerCache);
document.getElementById('customer-switcher').innerHTML = c.map(cust=>`
<button class="cust-chip${cust.id===activeCustomer?' active':''}" onclick="switchCustomer('${cust.id}')">${cust.name.split(' ').slice(0,2).join(' ')}</button>
`).join('');
}
async function switchCustomer(id){
activeCustomer=id;
renderSwitcher();
await loadCustomerData(id);
renderDashboard();
}
// Data loading
async function loadAll(){
// Load all customers
const custR=await fetch('https://api.quixzoom.com/api/qz/customers').then(r=>r.json()).catch(()=>({ok:false}));
if(custR.ok){
custR.items.forEach(c=>customerCache[c.id]=c);
renderSwitcher();
}
await loadCustomerData(activeCustomer);
renderDashboard();
}
async function loadCustomerData(id){
const [mR,sR,iR,fR]=await Promise.all([
fetch(`/api/quixzoom/customers/${id}/missions`).then(r=>r.json()).catch(()=>({ok:false})),
fetch(`/api/quixzoom/customers/${id}/spending`).then(r=>r.json()).catch(()=>({ok:false})),
fetch(`/api/quixzoom/customers/${id}/invoices`).then(r=>r.json()).catch(()=>({ok:false})),
fetch(`/api/quixzoom/customers/${id}/favorites`).then(r=>r.json()).catch(()=>({ok:false})),
]);
if(mR.ok)missionCache[id]=mR.items||[];
if(sR.ok)spendingCache[id]=sR.spending;
if(iR.ok)invoiceCache[id]=iR.items||[];
if(fR.ok)favCache[id]=fR.items||[];
}
// Dashboard
function renderDashboard(){
const cust=customerCache[activeCustomer];
const missions=missionCache[activeCustomer]||[];
const spending=spendingCache[activeCustomer]||{};
if(cust){
document.getElementById('large-title').textContent=cust.name;
document.getElementById('large-sub').textContent=`${cust.plan.toUpperCase()} · ${cust.contact}`;
document.getElementById('book-sub').textContent=`Bokar åt ${cust.name}`;
}
// KPIs
const active=missions.filter(m=>m.status==='active'||m.status==='open');
const completed=missions.filter(m=>m.status==='completed');
document.getElementById('kpi-strip').innerHTML=`
<div class="kpi-box"><div class="kpi-val kpi-val-accent">${active.length}</div><div class="kpi-lbl">Aktiva</div></div>
<div class="kpi-box"><div class="kpi-val">${completed.length}</div><div class="kpi-lbl">Avslutade</div></div>
<div class="kpi-box"><div class="kpi-val">${fmtMoney(spending.total_completed_sek||0).replace(' kr','')}</div><div class="kpi-lbl">Totalt (kr)</div></div>
`;
// Active missions
const activeMissions=missions.filter(m=>['active','open'].includes(m.status));
document.getElementById('active-missions').innerHTML=activeMissions.length
? activeMissions.map(renderMissionCard).join('')
: '<div class="empty"><div class="empty-icon"></div><div style="font-weight:600">Inga aktiva uppdrag</div><div style="font-size:13px;margin-top:4px">Boka ett nytt uppdrag!</div></div>';
// Recent (completed, max 3)
const recent=missions.filter(m=>m.status==='completed').slice(0,3);
document.getElementById('recent-missions').innerHTML=recent.length
? recent.map(renderMissionCard).join('')
: '<div class="empty"><div class="empty-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H10M6 8H10M6 11H8" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg></div><div style="font-weight:600">Inga avslutade uppdrag</div></div>';
}
function renderMissionCard(m){
return `<div class="card-item">
<div class="card-icon ${missionIconClass(m.status)}">${missionIcon(m.type)}</div>
<div class="card-body">
<div class="card-title">${m.title}</div>
<div class="card-sub">${m.location} · ${fmtDate(m.start_time)}${m.zoomer_name?' · '+m.zoomer_name:''}</div>
</div>
<div class="card-right">
${statusBadge(m.status)}
<div style="text-align:right;margin-left:6px"><div style="font-size:14px;font-weight:600">${fmtMoney(m.amount_sek)}</div>${m.zoomer_rating?stars(m.zoomer_rating):''}</div>
<span class="chevron"></span>
</div>
</div>`;
}
// History tab
function renderHistory(){
const missions=missionCache[activeCustomer]||[];
const spending=spendingCache[activeCustomer]||{};
// Spending chart
const byMonth=spending.by_month||{};
const months=Object.keys(byMonth).sort();
const maxVal=Math.max(...Object.values(byMonth),1);
document.getElementById('spending-chart').innerHTML=`
<div class="bar-chart-wrap">
<div class="bar-chart-title">Utgifter per månad</div>
${months.map(mo=>`
<div class="bar-row">
<div class="bar-label">${mo.slice(5)}/${mo.slice(2,4)}</div>
<div class="bar-track"><div class="bar-fill" style="width:${Math.round(byMonth[mo]/maxVal*100)}%"></div></div>
<div class="bar-val">${fmtMoney(byMonth[mo])}</div>
</div>`).join('')}
${!months.length?'<div style="color:#666666;font-size:14px">Ingen historik ännu</div>':''}
</div>`;
// All missions
document.getElementById('all-missions').innerHTML=missions.length
? missions.map(renderMissionCard).join('')
: '<div class="empty"><div class="empty-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H10M6 8H10M6 11H8" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg></div>Inga uppdrag</div>';
}
// Billing tab
function renderBilling(){
const invs=invoiceCache[activeCustomer]||[];
document.getElementById('invoices-list').innerHTML=invs.length
? invs.map(inv=>`
<div class="card-item">
<div class="card-icon ${inv.status==='paid'?'ci-green':'ci-orange'}">${inv.status==='paid'?'<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="card-body"><div class="card-title">${inv.number}</div><div class="card-sub">Förfaller ${fmtDate(inv.due_date)}</div></div>
<div class="card-right">${invStatusBadge(inv.status)}<div style="font-size:14px;font-weight:700;margin-left:8px">${fmtMoney(inv.amount_sek)}</div><span class="chevron"></span></div>
</div>`).join('')
: '<div class="empty"><div class="empty-icon"></div>Inga fakturor</div>';
}
// Favorites tab
function renderFavorites(){
const favs=favCache[activeCustomer]||[];
document.getElementById('favs-list').innerHTML=favs.length
? favs.map(f=>`
<div class="fav-card">
<div class="fav-avatar">${f.avatar||'<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg>'}</div>
<div>
<div class="fav-name">${f.name}</div>
<div class="fav-spec">${f.specialty}</div>
<div class="fav-meta">${f.missions_completed} uppdrag · ${f.rating?' '+f.rating:'Ny'}</div>
</div>
<div class="fav-actions">
<button class="btn btn-gray" style="font-size:13px;padding:7px 12px;border-radius: var(--radius-md);border:none;cursor:pointer" onclick="removeFav('${f.zoomer_id}')"></button>
</div>
</div>`).join('')
: '<div class="empty"><div class="empty-icon"></div><div style="font-weight:600">Inga favoriter</div><div style="font-size:13px;margin-top:4px">Lägg till zoomers du gillar!</div></div>';
}
async function removeFav(zid){
const r=await fetch(`/api/quixzoom/customers/${activeCustomer}/favorites/${zid}`,{method:'DELETE'}).then(r=>r.json());
if(r.ok){
toast('Favorit borttagen');
const fR=await fetch(`/api/quixzoom/customers/${activeCustomer}/favorites`).then(r=>r.json());
favCache[activeCustomer]=fR.items||[];
renderFavorites();
} else toast('Fel: '+r.error);
}
// Book modal
function openBookModal(){document.getElementById('book-modal').classList.add('open')}
async function submitBook(){
const title=document.getElementById('bk-title').value;
const start=document.getElementById('bk-start').value;
if(!title||!start){toast('Fyll i titel och startdatum');return}
const body={
title,type:document.getElementById('bk-type').value,
start_time:new Date(start).toISOString(),
duration_hours:+document.getElementById('bk-duration').value||2,
location:document.getElementById('bk-location').value||'Phuket',
budget_sek:+document.getElementById('bk-budget').value||2500,
};
const r=await fetch(`/api/quixzoom/customers/${activeCustomer}/book`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}).then(r=>r.json());
if(r.ok){
toast('<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> Uppdrag bokat!');closeModal('book-modal');
const mR=await fetch(`/api/quixzoom/customers/${activeCustomer}/missions`).then(r=>r.json());
missionCache[activeCustomer]=mR.items||[];renderDashboard();
document.getElementById('bk-title').value='';document.getElementById('bk-location').value='';document.getElementById('bk-budget').value='';
} else toast('Fel: '+r.error);
}
// Add favorite modal
function openAddFavModal(){document.getElementById('fav-modal').classList.add('open')}
async function submitFav(){
const body={zoomer_id:document.getElementById('fav-id').value,name:document.getElementById('fav-name').value,specialty:document.getElementById('fav-spec').value};
if(!body.zoomer_id||!body.name){toast('Fyll i ID och namn');return}
const r=await fetch(`/api/quixzoom/customers/${activeCustomer}/favorites`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}).then(r=>r.json());
if(r.ok){
toast(' Favorit tillagd!');closeModal('fav-modal');
const fR=await fetch(`/api/quixzoom/customers/${activeCustomer}/favorites`).then(r=>r.json());
favCache[activeCustomer]=fR.items||[];renderFavorites();
document.getElementById('fav-id').value='';document.getElementById('fav-name').value='';
} else toast('Fel: '+r.error);
}
// Init
loadAll();
</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>