6989a98d75
- Arkitektur: docs/auth/passwordless-architecture.md - Backend: iom/quixzoom-auth-service/ (FastAPI + Redis) - Webb: quixzoom-market-pages/se/login/ (QR-kod + polling) - App: iom/quixzoom-app/src/features/auth/ (push + deep links) Flöde: QR-kod → app-godkännande → webb-inloggad
108 lines
5.8 KiB
HTML
108 lines
5.8 KiB
HTML
<!DOCTYPE html><html lang="sv"><head>
|
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Zoomer Admin — AAMOS</title>
|
|
<style>
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#F2F2F7;color:#000}
|
|
.header{position:sticky;top:0;background:rgba(242,242,247,.9);backdrop-filter:blur(20px);border-bottom:1px solid rgba(60,60,67,.12);padding:12px 16px;display:flex;align-items:center;gap:12px}
|
|
.header h1{font-size:17px;font-weight:600}
|
|
.search{flex:1;background:rgba(120,120,128,.12);border:none;border-radius: var(--radius-md);padding:7px 12px;font-size:15px;outline:none}
|
|
.stats{display:flex;gap:8px;padding:16px;overflow-x:auto}
|
|
.stat{background:#fff;border-radius: var(--radius-md);padding:12px 16px;min-width:100px;text-align:center}
|
|
.stat-val{font-size:26px;font-weight:700}.stat-lbl{font-size:12px;color:rgba(60,60,67,.6);margin-top:2px}
|
|
.filters{padding:0 16px 8px;display:flex;gap:8px;overflow-x:auto}
|
|
.chip{background:rgba(120,120,128,.12);border:none;border-radius: var(--radius-xl);padding:6px 14px;font-size:14px;font-weight:500;cursor:pointer}
|
|
.chip.active{background:#007AFF;color:#fff}
|
|
.list{padding:0 16px 80px}
|
|
.card{background:#fff;border-radius: var(--radius-md);padding:14px;margin-bottom:8px;display:flex;align-items:center;gap:12px}
|
|
.avatar{width:42px;height:42px;border-radius: 21px;background:#007AFF15;display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:600;color:#007AFF;flex-shrink:0}
|
|
.info{flex:1}
|
|
.name{font-size:15px;font-weight:600}.sub{font-size:13px;color:rgba(60,60,67,.6);margin-top:2px}
|
|
.badge{font-size:12px;padding:3px 8px;border-radius: var(--radius-sm);font-weight:500}
|
|
.badge.pending{background:#FF950015;color:#FF9500}
|
|
.badge.verified{background:#34C75915;color:#34C759}
|
|
.badge.active{background:#007AFF15;color:#007AFF}
|
|
.btn-verify{background:#34C759;color:#fff;border:none;border-radius: var(--radius-md);padding:7px 14px;font-size:13px;font-weight:600;cursor:pointer}
|
|
@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; }
|
|
}
|
|
</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; }
|
|
}
|
|
</style>
|
|
</head><body>
|
|
<div class="header">
|
|
<h1> Zoomer Admin</h1>
|
|
<input class="search" type="text" placeholder="Sök namn eller email…" id="q" oninput="filter()">
|
|
</div>
|
|
<div class="stats" id="stats"></div>
|
|
<div class="filters">
|
|
<button class="chip active" onclick="setFilter(null,this)">Alla</button>
|
|
<button class="chip" onclick="setFilter('pending',this)">Väntande</button>
|
|
<button class="chip" onclick="setFilter('verified',this)">Verifierade</button>
|
|
<button class="chip" onclick="setFilter('active',this)">Aktiva</button>
|
|
</div>
|
|
<div class="list" id="list"><p style="padding:20px;color:#aaa">Laddar...</p></div>
|
|
|
|
<script>
|
|
let all=[], activeFilter=null;
|
|
|
|
async function load(){
|
|
const [zr,sr] = await Promise.all([fetch('/api/zoomers?limit=200'),fetch('/api/zoomers/stats')]);
|
|
const zd=await zr.json(), sd=await sr.json();
|
|
all=zd.zoomers||[];
|
|
document.getElementById('stats').innerHTML=`
|
|
<div class="stat"><div class="stat-val">${sd.verified||0}</div><div class="stat-lbl">Verifierade</div></div>
|
|
<div class="stat"><div class="stat-val" style="color:#FF9500">${sd.pending||0}</div><div class="stat-lbl">Väntande</div></div>
|
|
<div class="stat"><div class="stat-val" style="color:#007AFF">${sd.active_missions||0}</div><div class="stat-lbl">Aktiva uppdrag</div></div>
|
|
`;
|
|
filter();
|
|
}
|
|
|
|
function setFilter(f,el){activeFilter=f;document.querySelectorAll('.chip').forEach(c=>c.classList.remove('active'));el.classList.add('active');filter();}
|
|
|
|
function filter(){
|
|
const q=document.getElementById('q').value.toLowerCase();
|
|
let items=all;
|
|
if(activeFilter) items=items.filter(z=>z.status===activeFilter);
|
|
if(q) items=items.filter(z=>(z.first_name+' '+z.last_name+' '+z.email).toLowerCase().includes(q));
|
|
document.getElementById('list').innerHTML=items.length?items.map(z=>`
|
|
<div class="card">
|
|
<div class="avatar">${(z.first_name||'?')[0].toUpperCase()}</div>
|
|
<div class="info">
|
|
<div class="name">${z.first_name||''} ${z.last_name||''}</div>
|
|
<div class="sub">${z.email||''} · ${z.missions_completed||0} uppdrag · ${z.total_earned||0} kr</div>
|
|
</div>
|
|
<span class="badge ${z.status}">${z.status}</span>
|
|
${z.status==='pending'?`<button class="btn-verify" onclick="verify('${z.id}',this)">Verifiera</button>`:''}
|
|
</div>`).join(''):'<p style="padding:20px;color:#aaa">Inga zoomers hittades</p>';
|
|
}
|
|
|
|
async function verify(id,btn){
|
|
btn.disabled=true; btn.textContent='...';
|
|
await fetch('/api/zoomers/'+id+'/verify',{method:'POST'});
|
|
await load();
|
|
}
|
|
load();
|
|
</script></body></html>
|