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
1855 lines
66 KiB
HTML
1855 lines
66 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 API Status</title>
|
||
<style>
|
||
/* iOS 26 / Apple Native Design System */
|
||
:root {
|
||
--bg: #000000;
|
||
--bg-secondary: #1c1c1e;
|
||
--bg-tertiary: #2c2c2e;
|
||
--bg-grouped: #1c1c1e;
|
||
--separator: rgba(84,84,88,0.65);
|
||
--label: #ffffff;
|
||
--label-secondary: rgba(235,235,245,0.6);
|
||
--label-tertiary: rgba(235,235,245,0.3);
|
||
--tint: #007AFF;
|
||
--green: #30D158;
|
||
--red: #FF453A;
|
||
--yellow: #FFD60A;
|
||
--orange: #FF9F0A;
|
||
--purple: #BF5AF2;
|
||
--fill: rgba(118,118,128,0.24);
|
||
--fill-secondary: rgba(118,118,128,0.16);
|
||
--radius: 12px;
|
||
--radius-lg: 18px;
|
||
--radius-sm: 8px;
|
||
--font: -apple-system, "SF Pro Display", "Helvetica Neue", sans-serif;
|
||
--font-rounded: -apple-system, "SF Pro Rounded", sans-serif;
|
||
--safe-top: env(safe-area-inset-top, 0px);
|
||
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
||
}
|
||
|
||
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
|
||
|
||
body {
|
||
font-family: var(--font);
|
||
background: var(--bg);
|
||
color: var(--label);
|
||
min-height: 100dvh;
|
||
padding-top: calc(var(--safe-top) + 16px);
|
||
padding-bottom: calc(var(--safe-bottom) + 32px);
|
||
overscroll-behavior-y: contain;
|
||
}
|
||
|
||
/* Navigation Bar */
|
||
.nav-bar {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
background: rgba(242,242,247,0.82);
|
||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||
backdrop-filter: blur(20px) saturate(180%);
|
||
border-bottom: 0.5px solid var(--separator);
|
||
padding: 12px 20px 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
.nav-bar h1 {
|
||
font-size: 17px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.4px;
|
||
color: var(--label);
|
||
}
|
||
.nav-bar .nav-badge {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: var(--label-secondary);
|
||
}
|
||
|
||
/* Large Title */
|
||
.page-header {
|
||
padding: 24px 20px 8px;
|
||
}
|
||
.page-title {
|
||
font-size: 34px;
|
||
font-weight: 700;
|
||
letter-spacing: -0.5px;
|
||
line-height: 1.1;
|
||
color: var(--label);
|
||
}
|
||
.page-subtitle {
|
||
font-size: 15px;
|
||
color: var(--label-secondary);
|
||
margin-top: 4px;
|
||
}
|
||
|
||
/* Summary Cards */
|
||
.summary-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 12px;
|
||
padding: 16px 20px 4px;
|
||
}
|
||
.stat-card {
|
||
background: var(--bg-secondary);
|
||
border-radius: var(--radius);
|
||
padding: 14px 12px;
|
||
text-align: center;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
.stat-card::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0; left: 0; right: 0;
|
||
height: 2px;
|
||
border-radius: var(--radius-sm) 2px 0 0;
|
||
}
|
||
.stat-card.green::before { background: var(--green); }
|
||
.stat-card.red::before { background: var(--red); }
|
||
.stat-card.yellow::before{ background: var(--yellow); }
|
||
.stat-number {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
font-variant-numeric: tabular-nums;
|
||
letter-spacing: -0.5px;
|
||
line-height: 1;
|
||
margin-bottom: 4px;
|
||
}
|
||
.stat-card.green .stat-number { color: var(--green); }
|
||
.stat-card.red .stat-number { color: var(--red); }
|
||
.stat-card.yellow .stat-number{ color: var(--yellow); }
|
||
.stat-label {
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
color: var(--label-secondary);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.3px;
|
||
}
|
||
|
||
/* Section */
|
||
.section {
|
||
margin-top: 28px;
|
||
}
|
||
.section-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 20px 8px;
|
||
}
|
||
.section-title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.6px;
|
||
color: var(--label-secondary);
|
||
}
|
||
.section-action {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: var(--tint);
|
||
background: none;
|
||
border: none;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
}
|
||
.section-action:active { opacity: 0.6; }
|
||
|
||
/* Endpoint List */
|
||
.endpoint-list {
|
||
background: var(--bg-secondary);
|
||
border-radius: var(--radius-lg);
|
||
margin: 0 16px;
|
||
overflow: hidden;
|
||
}
|
||
.endpoint-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 14px 16px;
|
||
border-bottom: 0.5px solid var(--separator);
|
||
transition: background 0.15s;
|
||
cursor: pointer;
|
||
-webkit-user-select: none;
|
||
user-select: none;
|
||
}
|
||
.endpoint-item:last-child { border-bottom: none; }
|
||
.endpoint-item:active { background: var(--fill); }
|
||
|
||
.method-badge {
|
||
flex-shrink: 0;
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.4px;
|
||
padding: 3px 7px;
|
||
border-radius: var(--radius-sm);
|
||
font-family: "SF Mono", "Menlo", monospace;
|
||
}
|
||
.method-GET { background: rgba(48,209,88,0.2); color: var(--green); }
|
||
.method-POST { background: rgba(0,122,255,0.2); color: var(--tint); }
|
||
.method-PATCH { background: rgba(255,159,10,0.2); color: var(--orange); }
|
||
.method-DELETE { background: rgba(255,69,58,0.2); color: var(--red); }
|
||
|
||
.endpoint-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
.endpoint-path {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: var(--label);
|
||
font-family: "SF Mono", "Menlo", monospace;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.endpoint-desc {
|
||
font-size: 12px;
|
||
color: var(--label-secondary);
|
||
margin-top: 1px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.endpoint-meta {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: 3px;
|
||
flex-shrink: 0;
|
||
}
|
||
.status-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: var(--radius-full);
|
||
flex-shrink: 0;
|
||
}
|
||
.dot-green { background: var(--green); box-shadow: 0 0 6px rgba(48,209,88,0.5); }
|
||
.dot-red { background: var(--red); box-shadow: 0 0 6px rgba(255,69,58,0.5); }
|
||
.dot-yellow { background: var(--yellow);}
|
||
.dot-grey { background: var(--label-tertiary); }
|
||
|
||
.resp-time {
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
color: var(--label-secondary);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.resp-time.fast { color: var(--green); }
|
||
.resp-time.slow { color: var(--orange); }
|
||
|
||
/* Test Button */
|
||
.test-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
background: var(--tint);
|
||
color: #fff;
|
||
border: none;
|
||
border-radius: var(--radius-sm);
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
padding: 6px 12px;
|
||
cursor: pointer;
|
||
transition: opacity 0.15s, transform 0.1s;
|
||
font-family: var(--font);
|
||
-webkit-user-select: none;
|
||
user-select: none;
|
||
}
|
||
.test-btn:active {
|
||
opacity: 0.7;
|
||
transform: scale(0.97);
|
||
}
|
||
.test-btn.testing {
|
||
background: var(--bg-tertiary);
|
||
color: var(--label-secondary);
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* Detail Row (expandable) */
|
||
.endpoint-detail {
|
||
display: none;
|
||
background: var(--bg-tertiary);
|
||
border-radius: 0;
|
||
padding: 12px 16px;
|
||
border-top: 0.5px solid var(--separator);
|
||
}
|
||
.endpoint-detail.open { display: block; }
|
||
.detail-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
padding: 3px 0;
|
||
}
|
||
.detail-key {
|
||
font-size: 12px;
|
||
color: var(--label-tertiary);
|
||
font-family: "SF Mono", monospace;
|
||
}
|
||
.detail-val {
|
||
font-size: 12px;
|
||
color: var(--label-secondary);
|
||
font-family: "SF Mono", monospace;
|
||
text-align: right;
|
||
max-width: 60%;
|
||
word-break: break-all;
|
||
}
|
||
|
||
/* Run All Button */
|
||
.run-all-container {
|
||
padding: 20px 16px 8px;
|
||
}
|
||
.run-all-btn {
|
||
width: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
background: var(--tint);
|
||
color: #fff;
|
||
border: none;
|
||
border-radius: var(--radius);
|
||
font-size: 17px;
|
||
font-weight: 600;
|
||
padding: 16px;
|
||
cursor: pointer;
|
||
font-family: var(--font);
|
||
transition: opacity 0.15s, transform 0.1s;
|
||
letter-spacing: -0.3px;
|
||
}
|
||
.run-all-btn:active {
|
||
opacity: 0.8;
|
||
transform: scale(0.98);
|
||
}
|
||
.run-all-btn:disabled {
|
||
background: var(--bg-tertiary);
|
||
color: var(--label-secondary);
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* Last Run */
|
||
.last-run {
|
||
text-align: center;
|
||
font-size: 12px;
|
||
color: var(--label-tertiary);
|
||
padding: 8px 20px 0;
|
||
}
|
||
|
||
/* Toast */
|
||
.toast {
|
||
position: fixed;
|
||
bottom: calc(var(--safe-bottom) + 20px);
|
||
left: 50%;
|
||
transform: translateX(-50%) translateY(100px);
|
||
background: rgba(50,50,52,0.97);
|
||
-webkit-backdrop-filter: blur(20px);
|
||
backdrop-filter: blur(20px);
|
||
color: #fff;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
padding: 12px 20px;
|
||
border-radius: var(--radius-full);
|
||
z-index: 999;
|
||
transition: transform 0.3s cubic-bezier(0.34,1.56,0.64,1), opacity 0.3s;
|
||
opacity: 0;
|
||
white-space: nowrap;
|
||
border: 0.5px solid rgba(255,255,255,0.1);
|
||
}
|
||
.toast.show {
|
||
transform: translateX(-50%) translateY(0);
|
||
opacity: 1;
|
||
}
|
||
|
||
/* Progress bar */
|
||
.progress-bar {
|
||
height: 3px;
|
||
background: var(--bg-tertiary);
|
||
border-radius: var(--radius-sm);
|
||
margin: 0 16px 0;
|
||
overflow: hidden;
|
||
}
|
||
.progress-fill {
|
||
height: 100%;
|
||
background: var(--tint);
|
||
border-radius: var(--radius-sm);
|
||
width: 0%;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
/* Spinner */
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
.spinner {
|
||
width: 14px; height: 14px;
|
||
border: 2px solid rgba(255,255,255,0.3);
|
||
border-top-color: #fff;
|
||
border-radius: var(--radius-full);
|
||
animation: spin 0.6s linear infinite;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* Footer */
|
||
.footer {
|
||
text-align: center;
|
||
padding: 32px 20px 8px;
|
||
font-size: 12px;
|
||
color: var(--label-tertiary);
|
||
}
|
||
.footer strong { color: var(--label-secondary); }
|
||
@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 Bar -->
|
||
<div class="nav-bar">
|
||
<h1>quiXzoom API</h1>
|
||
<span class="nav-badge" id="nav-status">Kontrollerar…</span>
|
||
</div>
|
||
|
||
<!-- Page Header -->
|
||
<div class="page-header">
|
||
<div class="page-title">iOS Backend Status</div>
|
||
<div class="page-subtitle">Realtids-kontroll av alla quiXzoom-endpoints</div>
|
||
</div>
|
||
|
||
<!-- Summary -->
|
||
<div class="summary-row">
|
||
<div class="stat-card green">
|
||
<div class="stat-number" id="count-ok">–</div>
|
||
<div class="stat-label">OK</div>
|
||
</div>
|
||
<div class="stat-card red">
|
||
<div class="stat-number" id="count-err">–</div>
|
||
<div class="stat-label">Fel</div>
|
||
</div>
|
||
<div class="stat-card yellow">
|
||
<div class="stat-number" id="avg-ms">–</div>
|
||
<div class="stat-label">Median ms</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Progress -->
|
||
<div class="progress-bar" style="margin-top:16px;">
|
||
<div class="progress-fill" id="progress-fill"></div>
|
||
</div>
|
||
|
||
<!-- Run All -->
|
||
<div class="run-all-container">
|
||
<button class="run-all-btn" id="run-all-btn" onclick="runAll()">
|
||
<span> Testa alla endpoints</span>
|
||
</button>
|
||
</div>
|
||
<div class="last-run" id="last-run">Inte testad ännu</div>
|
||
|
||
<!-- Endpoint Sections -->
|
||
|
||
<!-- Zoomers / Profiles -->
|
||
<div class="section">
|
||
<div class="section-header">
|
||
<span class="section-title">Zoomer Profiler</span>
|
||
<button class="section-action" onclick="testSection('profiles')">Testa alla</button>
|
||
</div>
|
||
<div class="endpoint-list" id="list-profiles">
|
||
|
||
<div class="endpoint-item" onclick="toggleDetail('ep-z-list')">
|
||
<span class="method-badge method-GET">GET</span>
|
||
<div class="endpoint-info">
|
||
<div class="endpoint-path">/api/quixzoom/zoomers</div>
|
||
<div class="endpoint-desc">Lista alla zoomers</div>
|
||
</div>
|
||
<div class="endpoint-meta">
|
||
<div class="status-dot dot-grey" id="dot-ep-z-list"></div>
|
||
<div class="resp-time" id="ms-ep-z-list">–</div>
|
||
</div>
|
||
<button class="test-btn" id="btn-ep-z-list" onclick="event.stopPropagation(); testEndpoint('ep-z-list')">Test</button>
|
||
</div>
|
||
<div class="endpoint-detail" id="detail-ep-z-list">
|
||
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/zoomers</span></div>
|
||
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-z-list">–</span></div>
|
||
</div>
|
||
|
||
<div class="endpoint-item" onclick="toggleDetail('ep-z-profile')">
|
||
<span class="method-badge method-GET">GET</span>
|
||
<div class="endpoint-info">
|
||
<div class="endpoint-path">/zoomers/:id/profile</div>
|
||
<div class="endpoint-desc">Fullständig zoomerprofil</div>
|
||
</div>
|
||
<div class="endpoint-meta">
|
||
<div class="status-dot dot-grey" id="dot-ep-z-profile"></div>
|
||
<div class="resp-time" id="ms-ep-z-profile">–</div>
|
||
</div>
|
||
<button class="test-btn" id="btn-ep-z-profile" onclick="event.stopPropagation(); testEndpoint('ep-z-profile')">Test</button>
|
||
</div>
|
||
<div class="endpoint-detail" id="detail-ep-z-profile">
|
||
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/zoomers/zoomer-001/profile</span></div>
|
||
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-z-profile">–</span></div>
|
||
</div>
|
||
|
||
<div class="endpoint-item" onclick="toggleDetail('ep-z-patch')">
|
||
<span class="method-badge method-PATCH">PATCH</span>
|
||
<div class="endpoint-info">
|
||
<div class="endpoint-path">/zoomers/:id/profile</div>
|
||
<div class="endpoint-desc">Uppdatera bio/utrustning/plats</div>
|
||
</div>
|
||
<div class="endpoint-meta">
|
||
<div class="status-dot dot-grey" id="dot-ep-z-patch"></div>
|
||
<div class="resp-time" id="ms-ep-z-patch">–</div>
|
||
</div>
|
||
<button class="test-btn" id="btn-ep-z-patch" onclick="event.stopPropagation(); testEndpoint('ep-z-patch')">Test</button>
|
||
</div>
|
||
<div class="endpoint-detail" id="detail-ep-z-patch">
|
||
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/zoomers/zoomer-001/profile</span></div>
|
||
<div class="detail-row"><span class="detail-key">Body</span><span class="detail-val">{"bio":"Test"}</span></div>
|
||
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-z-patch">–</span></div>
|
||
</div>
|
||
|
||
<div class="endpoint-item" onclick="toggleDetail('ep-z-portfolio')">
|
||
<span class="method-badge method-GET">GET</span>
|
||
<div class="endpoint-info">
|
||
<div class="endpoint-path">/zoomers/:id/portfolio</div>
|
||
<div class="endpoint-desc">Portfolio foton/videos</div>
|
||
</div>
|
||
<div class="endpoint-meta">
|
||
<div class="status-dot dot-grey" id="dot-ep-z-portfolio"></div>
|
||
<div class="resp-time" id="ms-ep-z-portfolio">–</div>
|
||
</div>
|
||
<button class="test-btn" id="btn-ep-z-portfolio" onclick="event.stopPropagation(); testEndpoint('ep-z-portfolio')">Test</button>
|
||
</div>
|
||
<div class="endpoint-detail" id="detail-ep-z-portfolio">
|
||
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/zoomers/zoomer-001/portfolio</span></div>
|
||
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-z-portfolio">–</span></div>
|
||
</div>
|
||
|
||
<div class="endpoint-item" onclick="toggleDetail('ep-z-portfolio-post')">
|
||
<span class="method-badge method-POST">POST</span>
|
||
<div class="endpoint-info">
|
||
<div class="endpoint-path">/zoomers/:id/portfolio</div>
|
||
<div class="endpoint-desc">Lägg till portfolioobjekt</div>
|
||
</div>
|
||
<div class="endpoint-meta">
|
||
<div class="status-dot dot-grey" id="dot-ep-z-portfolio-post"></div>
|
||
<div class="resp-time" id="ms-ep-z-portfolio-post">–</div>
|
||
</div>
|
||
<button class="test-btn" id="btn-ep-z-portfolio-post" onclick="event.stopPropagation(); testEndpoint('ep-z-portfolio-post')">Test</button>
|
||
</div>
|
||
<div class="endpoint-detail" id="detail-ep-z-portfolio-post">
|
||
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/zoomers/zoomer-002/portfolio</span></div>
|
||
<div class="detail-row"><span class="detail-key">Body</span><span class="detail-val">{"url":"…","type":"photo"}</span></div>
|
||
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-z-portfolio-post">–</span></div>
|
||
</div>
|
||
|
||
<div class="endpoint-item" onclick="toggleDetail('ep-z-earnings')">
|
||
<span class="method-badge method-GET">GET</span>
|
||
<div class="endpoint-info">
|
||
<div class="endpoint-path">/zoomers/:id/earnings</div>
|
||
<div class="endpoint-desc">Inkomstöversikt per period</div>
|
||
</div>
|
||
<div class="endpoint-meta">
|
||
<div class="status-dot dot-grey" id="dot-ep-z-earnings"></div>
|
||
<div class="resp-time" id="ms-ep-z-earnings">–</div>
|
||
</div>
|
||
<button class="test-btn" id="btn-ep-z-earnings" onclick="event.stopPropagation(); testEndpoint('ep-z-earnings')">Test</button>
|
||
</div>
|
||
<div class="endpoint-detail" id="detail-ep-z-earnings">
|
||
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/zoomers/zoomer-001/earnings?period=month</span></div>
|
||
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-z-earnings">–</span></div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Missions -->
|
||
<div class="section">
|
||
<div class="section-header">
|
||
<span class="section-title">Uppdrag / Missions</span>
|
||
<button class="section-action" onclick="testSection('missions')">Testa alla</button>
|
||
</div>
|
||
<div class="endpoint-list" id="list-missions">
|
||
|
||
<div class="endpoint-item" onclick="toggleDetail('ep-m-list')">
|
||
<span class="method-badge method-GET">GET</span>
|
||
<div class="endpoint-info">
|
||
<div class="endpoint-path">/api/quixzoom/missions</div>
|
||
<div class="endpoint-desc">Lista alla uppdrag</div>
|
||
</div>
|
||
<div class="endpoint-meta">
|
||
<div class="status-dot dot-grey" id="dot-ep-m-list"></div>
|
||
<div class="resp-time" id="ms-ep-m-list">–</div>
|
||
</div>
|
||
<button class="test-btn" id="btn-ep-m-list" onclick="event.stopPropagation(); testEndpoint('ep-m-list')">Test</button>
|
||
</div>
|
||
<div class="endpoint-detail" id="detail-ep-m-list">
|
||
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/missions</span></div>
|
||
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-m-list">–</span></div>
|
||
</div>
|
||
|
||
<div class="endpoint-item" onclick="toggleDetail('ep-m-stats')">
|
||
<span class="method-badge method-GET">GET</span>
|
||
<div class="endpoint-info">
|
||
<div class="endpoint-path">/missions/stats</div>
|
||
<div class="endpoint-desc">Aggregerad statistik</div>
|
||
</div>
|
||
<div class="endpoint-meta">
|
||
<div class="status-dot dot-grey" id="dot-ep-m-stats"></div>
|
||
<div class="resp-time" id="ms-ep-m-stats">–</div>
|
||
</div>
|
||
<button class="test-btn" id="btn-ep-m-stats" onclick="event.stopPropagation(); testEndpoint('ep-m-stats')">Test</button>
|
||
</div>
|
||
<div class="endpoint-detail" id="detail-ep-m-stats">
|
||
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/missions/stats</span></div>
|
||
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-m-stats">–</span></div>
|
||
</div>
|
||
|
||
<div class="endpoint-item" onclick="toggleDetail('ep-m-post')">
|
||
<span class="method-badge method-POST">POST</span>
|
||
<div class="endpoint-info">
|
||
<div class="endpoint-path">/api/quixzoom/missions</div>
|
||
<div class="endpoint-desc">Skapa nytt uppdrag</div>
|
||
</div>
|
||
<div class="endpoint-meta">
|
||
<div class="status-dot dot-grey" id="dot-ep-m-post"></div>
|
||
<div class="resp-time" id="ms-ep-m-post">–</div>
|
||
</div>
|
||
<button class="test-btn" id="btn-ep-m-post" onclick="event.stopPropagation(); testEndpoint('ep-m-post')">Test</button>
|
||
</div>
|
||
<div class="endpoint-detail" id="detail-ep-m-post">
|
||
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/missions</span></div>
|
||
<div class="detail-row"><span class="detail-key">Body</span><span class="detail-val">{"location":"Phuket","description":"API test uppdrag","start_time":"…"}</span></div>
|
||
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-m-post">–</span></div>
|
||
</div>
|
||
|
||
<div class="endpoint-item" onclick="toggleDetail('ep-m-get')">
|
||
<span class="method-badge method-GET">GET</span>
|
||
<div class="endpoint-info">
|
||
<div class="endpoint-path">/missions/:id</div>
|
||
<div class="endpoint-desc">Uppdragsdetaljer</div>
|
||
</div>
|
||
<div class="endpoint-meta">
|
||
<div class="status-dot dot-grey" id="dot-ep-m-get"></div>
|
||
<div class="resp-time" id="ms-ep-m-get">–</div>
|
||
</div>
|
||
<button class="test-btn" id="btn-ep-m-get" onclick="event.stopPropagation(); testEndpoint('ep-m-get')">Test</button>
|
||
</div>
|
||
<div class="endpoint-detail" id="detail-ep-m-get">
|
||
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/missions/mission-001</span></div>
|
||
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-m-get">–</span></div>
|
||
</div>
|
||
|
||
<div class="endpoint-item" onclick="toggleDetail('ep-m-zoomers')">
|
||
<span class="method-badge method-GET">GET</span>
|
||
<div class="endpoint-info">
|
||
<div class="endpoint-path">/missions/:id/zoomers</div>
|
||
<div class="endpoint-desc">Tilldelade zoomers</div>
|
||
</div>
|
||
<div class="endpoint-meta">
|
||
<div class="status-dot dot-grey" id="dot-ep-m-zoomers"></div>
|
||
<div class="resp-time" id="ms-ep-m-zoomers">–</div>
|
||
</div>
|
||
<button class="test-btn" id="btn-ep-m-zoomers" onclick="event.stopPropagation(); testEndpoint('ep-m-zoomers')">Test</button>
|
||
</div>
|
||
<div class="endpoint-detail" id="detail-ep-m-zoomers">
|
||
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/missions/mission-001/zoomers</span></div>
|
||
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-m-zoomers">–</span></div>
|
||
</div>
|
||
|
||
<div class="endpoint-item" onclick="toggleDetail('ep-m-assign')">
|
||
<span class="method-badge method-POST">POST</span>
|
||
<div class="endpoint-info">
|
||
<div class="endpoint-path">/missions/:id/assign</div>
|
||
<div class="endpoint-desc">Tilldela zoomer</div>
|
||
</div>
|
||
<div class="endpoint-meta">
|
||
<div class="status-dot dot-grey" id="dot-ep-m-assign"></div>
|
||
<div class="resp-time" id="ms-ep-m-assign">–</div>
|
||
</div>
|
||
<button class="test-btn" id="btn-ep-m-assign" onclick="event.stopPropagation(); testEndpoint('ep-m-assign')">Test</button>
|
||
</div>
|
||
<div class="endpoint-detail" id="detail-ep-m-assign">
|
||
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/missions/mission-006/assign</span></div>
|
||
<div class="detail-row"><span class="detail-key">Body</span><span class="detail-val">{"zoomer_id":"zoomer-003"}</span></div>
|
||
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-m-assign">–</span></div>
|
||
</div>
|
||
|
||
<div class="endpoint-item" onclick="toggleDetail('ep-m-status')">
|
||
<span class="method-badge method-PATCH">PATCH</span>
|
||
<div class="endpoint-info">
|
||
<div class="endpoint-path">/missions/:id/status</div>
|
||
<div class="endpoint-desc">Ändra uppdragsstatus</div>
|
||
</div>
|
||
<div class="endpoint-meta">
|
||
<div class="status-dot dot-grey" id="dot-ep-m-status"></div>
|
||
<div class="resp-time" id="ms-ep-m-status">–</div>
|
||
</div>
|
||
<button class="test-btn" id="btn-ep-m-status" onclick="event.stopPropagation(); testEndpoint('ep-m-status')">Test</button>
|
||
</div>
|
||
<div class="endpoint-detail" id="detail-ep-m-status">
|
||
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/missions/mission-007/status</span></div>
|
||
<div class="detail-row"><span class="detail-key">Body</span><span class="detail-val">{"status":"active"}</span></div>
|
||
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-m-status">–</span></div>
|
||
</div>
|
||
|
||
<div class="endpoint-item" onclick="toggleDetail('ep-m-checkin')">
|
||
<span class="method-badge method-POST">POST</span>
|
||
<div class="endpoint-info">
|
||
<div class="endpoint-path">/missions/:id/checkin</div>
|
||
<div class="endpoint-desc">Zoomer incheckning</div>
|
||
</div>
|
||
<div class="endpoint-meta">
|
||
<div class="status-dot dot-grey" id="dot-ep-m-checkin"></div>
|
||
<div class="resp-time" id="ms-ep-m-checkin">–</div>
|
||
</div>
|
||
<button class="test-btn" id="btn-ep-m-checkin" onclick="event.stopPropagation(); testEndpoint('ep-m-checkin')">Test</button>
|
||
</div>
|
||
<div class="endpoint-detail" id="detail-ep-m-checkin">
|
||
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/missions/mission-005/checkin</span></div>
|
||
<div class="detail-row"><span class="detail-key">Body</span><span class="detail-val">{"zoomer_id":"zoomer-005","lat":7.99,"lng":98.26}</span></div>
|
||
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-m-checkin">–</span></div>
|
||
</div>
|
||
|
||
<div class="endpoint-item" onclick="toggleDetail('ep-m-checkout')">
|
||
<span class="method-badge method-POST">POST</span>
|
||
<div class="endpoint-info">
|
||
<div class="endpoint-path">/missions/:id/checkout</div>
|
||
<div class="endpoint-desc">Leverans + utcheckning</div>
|
||
</div>
|
||
<div class="endpoint-meta">
|
||
<div class="status-dot dot-grey" id="dot-ep-m-checkout"></div>
|
||
<div class="resp-time" id="ms-ep-m-checkout">–</div>
|
||
</div>
|
||
<button class="test-btn" id="btn-ep-m-checkout" onclick="event.stopPropagation(); testEndpoint('ep-m-checkout')">Test</button>
|
||
</div>
|
||
<div class="endpoint-detail" id="detail-ep-m-checkout">
|
||
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/missions/mission-004/checkout</span></div>
|
||
<div class="detail-row"><span class="detail-key">Body</span><span class="detail-val">{"zoomer_id":"zoomer-004","delivery_url":"https://cdn.test/file.zip"}</span></div>
|
||
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-m-checkout">–</span></div>
|
||
</div>
|
||
|
||
<div class="endpoint-item" onclick="toggleDetail('ep-m-deliverables')">
|
||
<span class="method-badge method-GET">GET</span>
|
||
<div class="endpoint-info">
|
||
<div class="endpoint-path">/missions/:id/deliverables</div>
|
||
<div class="endpoint-desc">Levererade filer</div>
|
||
</div>
|
||
<div class="endpoint-meta">
|
||
<div class="status-dot dot-grey" id="dot-ep-m-deliverables"></div>
|
||
<div class="resp-time" id="ms-ep-m-deliverables">–</div>
|
||
</div>
|
||
<button class="test-btn" id="btn-ep-m-deliverables" onclick="event.stopPropagation(); testEndpoint('ep-m-deliverables')">Test</button>
|
||
</div>
|
||
<div class="endpoint-detail" id="detail-ep-m-deliverables">
|
||
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/missions/mission-001/deliverables</span></div>
|
||
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-m-deliverables">–</span></div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Payouts -->
|
||
<div class="section">
|
||
<div class="section-header">
|
||
<span class="section-title">Utbetalningar</span>
|
||
<button class="section-action" onclick="testSection('payouts')">Testa alla</button>
|
||
</div>
|
||
<div class="endpoint-list" id="list-payouts">
|
||
|
||
<div class="endpoint-item" onclick="toggleDetail('ep-p-zoomer')">
|
||
<span class="method-badge method-GET">GET</span>
|
||
<div class="endpoint-info">
|
||
<div class="endpoint-path">/payouts/zoomer/:id</div>
|
||
<div class="endpoint-desc">Utbetalningshistorik</div>
|
||
</div>
|
||
<div class="endpoint-meta">
|
||
<div class="status-dot dot-grey" id="dot-ep-p-zoomer"></div>
|
||
<div class="resp-time" id="ms-ep-p-zoomer">–</div>
|
||
</div>
|
||
<button class="test-btn" id="btn-ep-p-zoomer" onclick="event.stopPropagation(); testEndpoint('ep-p-zoomer')">Test</button>
|
||
</div>
|
||
<div class="endpoint-detail" id="detail-ep-p-zoomer">
|
||
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/payouts/zoomer/zoomer-001</span></div>
|
||
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-p-zoomer">–</span></div>
|
||
</div>
|
||
|
||
<div class="endpoint-item" onclick="toggleDetail('ep-p-pending')">
|
||
<span class="method-badge method-GET">GET</span>
|
||
<div class="endpoint-info">
|
||
<div class="endpoint-path">/payouts/pending</div>
|
||
<div class="endpoint-desc">Väntande utbetalningar</div>
|
||
</div>
|
||
<div class="endpoint-meta">
|
||
<div class="status-dot dot-grey" id="dot-ep-p-pending"></div>
|
||
<div class="resp-time" id="ms-ep-p-pending">–</div>
|
||
</div>
|
||
<button class="test-btn" id="btn-ep-p-pending" onclick="event.stopPropagation(); testEndpoint('ep-p-pending')">Test</button>
|
||
</div>
|
||
<div class="endpoint-detail" id="detail-ep-p-pending">
|
||
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/payouts/pending</span></div>
|
||
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-p-pending">–</span></div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Ratings -->
|
||
<div class="section">
|
||
<div class="section-header">
|
||
<span class="section-title">Betyg / Ratings</span>
|
||
<button class="section-action" onclick="testSection('ratings')">Testa alla</button>
|
||
</div>
|
||
<div class="endpoint-list" id="list-ratings">
|
||
|
||
<div class="endpoint-item" onclick="toggleDetail('ep-r-zoomer')">
|
||
<span class="method-badge method-GET">GET</span>
|
||
<div class="endpoint-info">
|
||
<div class="endpoint-path">/ratings/zoomer/:id</div>
|
||
<div class="endpoint-desc">Betyg för zoomer</div>
|
||
</div>
|
||
<div class="endpoint-meta">
|
||
<div class="status-dot dot-grey" id="dot-ep-r-zoomer"></div>
|
||
<div class="resp-time" id="ms-ep-r-zoomer">–</div>
|
||
</div>
|
||
<button class="test-btn" id="btn-ep-r-zoomer" onclick="event.stopPropagation(); testEndpoint('ep-r-zoomer')">Test</button>
|
||
</div>
|
||
<div class="endpoint-detail" id="detail-ep-r-zoomer">
|
||
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/ratings/zoomer/z001</span></div>
|
||
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-r-zoomer">–</span></div>
|
||
</div>
|
||
|
||
<div class="endpoint-item" onclick="toggleDetail('ep-r-post')">
|
||
<span class="method-badge method-POST">POST</span>
|
||
<div class="endpoint-info">
|
||
<div class="endpoint-path">/ratings</div>
|
||
<div class="endpoint-desc">Skicka in betyg</div>
|
||
</div>
|
||
<div class="endpoint-meta">
|
||
<div class="status-dot dot-grey" id="dot-ep-r-post"></div>
|
||
<div class="resp-time" id="ms-ep-r-post">–</div>
|
||
</div>
|
||
<button class="test-btn" id="btn-ep-r-post" onclick="event.stopPropagation(); testEndpoint('ep-r-post')">Test</button>
|
||
</div>
|
||
<div class="endpoint-detail" id="detail-ep-r-post">
|
||
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/ratings</span></div>
|
||
<div class="detail-row"><span class="detail-key">Body</span><span class="detail-val">{"missionId":"m_test","zoomerId":"z001","rating":5,"comment":"Perfekt!","customerId":"c_test"}</span></div>
|
||
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-r-post">–</span></div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Footer -->
|
||
<div class="footer">
|
||
quiXzoom Backend · iOS deadline <strong>5 augusti 2026</strong><br>
|
||
Byggs av Wavult & AAMOS · Phuket Workcamp 2026
|
||
</div>
|
||
|
||
<!-- Toast -->
|
||
<div class="toast" id="toast"></div>
|
||
|
||
<script>
|
||
// Endpoint definitions
|
||
const BASE = '';
|
||
|
||
const ENDPOINTS = {
|
||
// Profiles
|
||
'ep-z-list': { method: 'GET', url: `${BASE}/api/quixzoom/zoomers`, section: 'profiles' },
|
||
'ep-z-profile': { method: 'GET', url: `${BASE}/api/quixzoom/zoomers/zoomer-001/profile`, section: 'profiles' },
|
||
'ep-z-patch': { method: 'PATCH', url: `${BASE}/api/quixzoom/zoomers/zoomer-001/profile`, body: { bio: 'API status test' }, section: 'profiles' },
|
||
'ep-z-portfolio': { method: 'GET', url: `${BASE}/api/quixzoom/zoomers/zoomer-001/portfolio`, section: 'profiles' },
|
||
'ep-z-portfolio-post': { method: 'POST', url: `${BASE}/api/quixzoom/zoomers/zoomer-002/portfolio`, body: { url: 'https://example.com/test.jpg', type: 'photo', caption: 'API test' }, section: 'profiles' },
|
||
'ep-z-earnings': { method: 'GET', url: `${BASE}/api/quixzoom/zoomers/zoomer-001/earnings?period=month`, section: 'profiles' },
|
||
// Missions
|
||
'ep-m-list': { method: 'GET', url: `${BASE}/api/quixzoom/missions`, section: 'missions' },
|
||
'ep-m-stats': { method: 'GET', url: `${BASE}/api/quixzoom/missions/stats`, section: 'missions' },
|
||
'ep-m-post': { method: 'POST', url: `${BASE}/api/quixzoom/missions`, body: { location: 'Phuket API Test', description: 'API health check test mission', start_time: new Date(Date.now() + 86400000).toISOString(), duration_hours: 2, budget_sek: 1000, camera_count: 1 }, section: 'missions' },
|
||
'ep-m-get': { method: 'GET', url: `${BASE}/api/quixzoom/missions/mission-001`, section: 'missions' },
|
||
'ep-m-zoomers': { method: 'GET', url: `${BASE}/api/quixzoom/missions/mission-001/zoomers`, section: 'missions' },
|
||
'ep-m-assign': { method: 'POST', url: `${BASE}/api/quixzoom/missions/mission-009/assign`, body: { zoomer_id: 'zoomer-004' }, section: 'missions' },
|
||
'ep-m-status': { method: 'PATCH', url: `${BASE}/api/quixzoom/missions/mission-010/status`, body: { status: 'active' }, section: 'missions' },
|
||
'ep-m-checkin': { method: 'POST', url: `${BASE}/api/quixzoom/missions/mission-005/checkin`, body: { zoomer_id: 'zoomer-005', lat: 7.9901, lng: 98.2631 }, section: 'missions' },
|
||
'ep-m-checkout': { method: 'POST', url: `${BASE}/api/quixzoom/missions/mission-004/checkout`, body: { zoomer_id: 'zoomer-004', delivery_url: 'https://storage.quixzoom.se/test/delivery.zip' }, section: 'missions' },
|
||
'ep-m-deliverables': { method: 'GET', url: `${BASE}/api/quixzoom/missions/mission-001/deliverables`, section: 'missions' },
|
||
// Payouts
|
||
'ep-p-zoomer': { method: 'GET', url: `${BASE}/api/quixzoom/payouts/zoomer/zoomer-001`, section: 'payouts' },
|
||
'ep-p-pending': { method: 'GET', url: `${BASE}/api/quixzoom/payouts/pending`, section: 'payouts' },
|
||
// Ratings
|
||
'ep-r-zoomer': { method: 'GET', url: `${BASE}/api/quixzoom/ratings/zoomer/z001`, section: 'ratings' },
|
||
'ep-r-post': { method: 'POST', url: `${BASE}/api/quixzoom/ratings`, body: { missionId: 'm_api_test', zoomerId: 'z001', rating: 5, comment: 'API status test', customerId: 'c_test' }, section: 'ratings' },
|
||
};
|
||
|
||
const results = {};
|
||
let testingAll = false;
|
||
|
||
// Test single endpoint
|
||
async function testEndpoint(id) {
|
||
const ep = ENDPOINTS[id];
|
||
if (!ep) return;
|
||
const btn = document.getElementById(`btn-${id}`);
|
||
const dot = document.getElementById(`dot-${id}`);
|
||
const ms = document.getElementById(`ms-${id}`);
|
||
const resp = document.getElementById(`resp-${id}`);
|
||
|
||
btn.classList.add('testing');
|
||
btn.innerHTML = '<div class="spinner"></div>';
|
||
dot.className = 'status-dot dot-yellow';
|
||
|
||
const t0 = performance.now();
|
||
try {
|
||
const opts = {
|
||
method: ep.method,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
};
|
||
if (ep.body) opts.body = JSON.stringify(ep.body);
|
||
const r = await fetch(ep.url, opts);
|
||
const elapsed = Math.round(performance.now() - t0);
|
||
let data;
|
||
try { data = await r.json(); } catch { data = {}; }
|
||
|
||
const ok = r.status >= 200 && r.status < 300;
|
||
results[id] = { ok, status: r.status, ms: elapsed };
|
||
|
||
dot.className = `status-dot ${ok ? 'dot-green' : 'dot-red'}`;
|
||
ms.textContent = `${elapsed}ms`;
|
||
ms.className = `resp-time ${elapsed < 200 ? 'fast' : elapsed < 800 ? '' : 'slow'}`;
|
||
if (resp) resp.textContent = JSON.stringify(data).slice(0, 120) + (JSON.stringify(data).length > 120 ? '…' : '');
|
||
|
||
btn.classList.remove('testing');
|
||
btn.textContent = ok ? '<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>' : '<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>';
|
||
btn.style.background = ok ? 'var(--green)' : 'var(--red)';
|
||
setTimeout(() => {
|
||
btn.textContent = 'Test';
|
||
btn.style.background = '';
|
||
}, 2000);
|
||
|
||
updateSummary();
|
||
} catch (e) {
|
||
const elapsed = Math.round(performance.now() - t0);
|
||
results[id] = { ok: false, status: 0, ms: elapsed };
|
||
dot.className = 'status-dot dot-red';
|
||
ms.textContent = `${elapsed}ms`;
|
||
if (resp) resp.textContent = String(e).slice(0, 120);
|
||
btn.classList.remove('testing');
|
||
btn.textContent = '<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>';
|
||
btn.style.background = 'var(--red)';
|
||
setTimeout(() => { btn.textContent = 'Test'; btn.style.background = ''; }, 2000);
|
||
updateSummary();
|
||
}
|
||
}
|
||
|
||
// Test a section
|
||
async function testSection(section) {
|
||
const ids = Object.keys(ENDPOINTS).filter(id => ENDPOINTS[id].section === section);
|
||
for (const id of ids) {
|
||
await testEndpoint(id);
|
||
await delay(120);
|
||
}
|
||
}
|
||
|
||
// Run All
|
||
async function runAll() {
|
||
if (testingAll) return;
|
||
testingAll = true;
|
||
const btn = document.getElementById('run-all-btn');
|
||
const prog = document.getElementById('progress-fill');
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<div class="spinner" style="border-color:rgba(255,255,255,.3);border-top-color:#fff;"></div><span>Testar…</span>';
|
||
|
||
const ids = Object.keys(ENDPOINTS);
|
||
for (let i = 0; i < ids.length; i++) {
|
||
await testEndpoint(ids[i]);
|
||
prog.style.width = `${Math.round(((i + 1) / ids.length) * 100)}%`;
|
||
await delay(80);
|
||
}
|
||
|
||
prog.style.width = '100%';
|
||
testingAll = false;
|
||
btn.disabled = false;
|
||
btn.innerHTML = '<span> Kör igen</span>';
|
||
|
||
const now = new Date().toLocaleTimeString('sv-SE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||
document.getElementById('last-run').textContent = `Senast testad: ${now}`;
|
||
const okCount = Object.values(results).filter(r => r.ok).length;
|
||
showToast(`${okCount}/${ids.length} endpoints <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>`);
|
||
}
|
||
|
||
// Update summary bar
|
||
function updateSummary() {
|
||
const vals = Object.values(results);
|
||
const ok = vals.filter(r => r.ok).length;
|
||
const err = vals.filter(r => !r.ok).length;
|
||
const times = vals.map(r => r.ms).sort((a, b) => a - b);
|
||
const median = times.length ? times[Math.floor(times.length / 2)] : 0;
|
||
|
||
document.getElementById('count-ok').textContent = ok;
|
||
document.getElementById('count-err').textContent = err;
|
||
document.getElementById('avg-ms').textContent = median ? `${median}` : '–';
|
||
|
||
const total = Object.keys(ENDPOINTS).length;
|
||
const tested = vals.length;
|
||
document.getElementById('nav-status').textContent = `${tested}/${total} testad`;
|
||
}
|
||
|
||
// Toggle detail row
|
||
function toggleDetail(id) {
|
||
const el = document.getElementById(`detail-${id}`);
|
||
if (!el) return;
|
||
el.classList.toggle('open');
|
||
}
|
||
|
||
// Toast
|
||
function showToast(msg) {
|
||
const t = document.getElementById('toast');
|
||
t.textContent = msg;
|
||
t.classList.add('show');
|
||
setTimeout(() => t.classList.remove('show'), 3000);
|
||
}
|
||
|
||
function delay(ms) { return new Promise(r => setTimeout(r, ms)); }
|
||
|
||
// Auto-run on load
|
||
window.addEventListener('load', () => {
|
||
setTimeout(runAll, 600);
|
||
});
|
||
</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>
|