623561d30a
- Backend: Docker-container på port 8082 - Redis: Intern container (ingen exposed port) - Nginx-config: nginx-passwordless.conf (väntar på deploy) - Webb: API_BASE uppdaterad till api.quixzoom.com - Fix: datetime timezone-aware i approve-endpoint - Test: End-to-end flöde verifierat
751 lines
18 KiB
HTML
751 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="sv">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
<title>Logga in — quiXzoom Zoomer</title>
|
|
<meta name="description" content="Logga in på ditt Zoomer-konto. Använd appen för snabb inloggning.">
|
|
<link rel="canonical" href="https://quixzoom.se/login/">
|
|
<meta name="theme-color" content="#0a0a0a">
|
|
<style>
|
|
:root{
|
|
--qx-black:#0a0a0a;
|
|
--qx-white:#FFFFFF;
|
|
--qx-blue:#0066FF;
|
|
--qx-blue-light:#E8F0FF;
|
|
--qx-green:#00C853;
|
|
--qx-red:#dc2626;
|
|
--qx-gray:#F5F5F7;
|
|
--qx-gray-dark:#86868b;
|
|
--qx-radius:12px;
|
|
--font:-apple-system,BlinkMacSystemFont,'SF Pro Display','Helvetica Neue',Arial,sans-serif;
|
|
}
|
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
html{-webkit-text-size-adjust:100%}
|
|
body{
|
|
font-family:var(--font);
|
|
background:var(--qx-black);
|
|
color:var(--qx-white);
|
|
line-height:1.6;
|
|
min-height:100vh;
|
|
display:flex;
|
|
flex-direction:column;
|
|
align-items:center;
|
|
justify-content:center;
|
|
padding:24px;
|
|
}
|
|
.login-container{
|
|
width:100%;
|
|
max-width:420px;
|
|
}
|
|
.logo{
|
|
text-align:center;
|
|
margin-bottom:40px;
|
|
}
|
|
.logo h1{
|
|
font-size:2rem;
|
|
font-weight:800;
|
|
letter-spacing:-.03em;
|
|
}
|
|
.logo h1 .x{color:var(--qx-blue)}
|
|
.logo p{
|
|
color:rgba(255,255,255,.5);
|
|
font-size:.9375rem;
|
|
margin-top:8px;
|
|
}
|
|
.card{
|
|
background:rgba(255,255,255,.05);
|
|
border:1px solid rgba(255,255,255,.1);
|
|
border-radius:var(--qx-radius);
|
|
padding:32px;
|
|
}
|
|
|
|
/* Passwordless Section */
|
|
.passwordless-section{
|
|
text-align:center;
|
|
margin-bottom:32px;
|
|
}
|
|
.passwordless-title{
|
|
font-size:1.125rem;
|
|
font-weight:700;
|
|
margin-bottom:8px;
|
|
}
|
|
.passwordless-desc{
|
|
color:rgba(255,255,255,.5);
|
|
font-size:.875rem;
|
|
margin-bottom:20px;
|
|
}
|
|
.qr-container{
|
|
background:var(--qx-white);
|
|
border-radius:var(--qx-radius);
|
|
padding:20px;
|
|
margin:0 auto 16px;
|
|
width:200px;
|
|
height:200px;
|
|
display:flex;
|
|
align-items:center;
|
|
justify-content:center;
|
|
}
|
|
.qr-container svg,
|
|
.qr-container img{
|
|
width:160px;
|
|
height:160px;
|
|
}
|
|
.qr-loading{
|
|
color:var(--qx-black);
|
|
font-size:.875rem;
|
|
}
|
|
.qr-error{
|
|
color:var(--qx-red);
|
|
font-size:.875rem;
|
|
}
|
|
.status-text{
|
|
font-size:.875rem;
|
|
color:rgba(255,255,255,.6);
|
|
margin-top:12px;
|
|
}
|
|
.status-text.approved{
|
|
color:var(--qx-green);
|
|
}
|
|
.status-text.rejected{
|
|
color:var(--qx-red);
|
|
}
|
|
|
|
/* App buttons */
|
|
.app-buttons{
|
|
display:flex;
|
|
gap:12px;
|
|
justify-content:center;
|
|
margin-top:16px;
|
|
}
|
|
.app-btn{
|
|
display:inline-flex;
|
|
align-items:center;
|
|
gap:8px;
|
|
padding:12px 20px;
|
|
background:var(--qx-blue);
|
|
color:var(--qx-white);
|
|
border-radius:10px;
|
|
text-decoration:none;
|
|
font-size:.9375rem;
|
|
font-weight:600;
|
|
transition:opacity .15s;
|
|
}
|
|
.app-btn:hover{opacity:.9}
|
|
.app-btn.secondary{
|
|
background:rgba(255,255,255,.1);
|
|
}
|
|
|
|
/* Divider */
|
|
.divider{
|
|
display:flex;
|
|
align-items:center;
|
|
gap:16px;
|
|
margin:24px 0;
|
|
color:rgba(255,255,255,.4);
|
|
font-size:.875rem;
|
|
}
|
|
.divider::before,
|
|
.divider::after{
|
|
content:'';
|
|
flex:1;
|
|
height:1px;
|
|
background:rgba(255,255,255,.1);
|
|
}
|
|
|
|
/* Traditional login */
|
|
.tabs{
|
|
display:flex;
|
|
gap:8px;
|
|
margin-bottom:24px;
|
|
}
|
|
.tab{
|
|
flex:1;
|
|
padding:12px;
|
|
background:transparent;
|
|
border:1px solid rgba(255,255,255,.15);
|
|
border-radius:8px;
|
|
color:rgba(255,255,255,.6);
|
|
font-size:.9375rem;
|
|
font-weight:600;
|
|
cursor:pointer;
|
|
transition:all .15s;
|
|
font-family:var(--font);
|
|
}
|
|
.tab.active{
|
|
background:var(--qx-blue);
|
|
border-color:var(--qx-blue);
|
|
color:var(--qx-white);
|
|
}
|
|
.form-group{
|
|
margin-bottom:16px;
|
|
}
|
|
label{
|
|
display:block;
|
|
font-size:.8125rem;
|
|
font-weight:600;
|
|
color:rgba(255,255,255,.7);
|
|
margin-bottom:6px;
|
|
}
|
|
input{
|
|
width:100%;
|
|
padding:14px 16px;
|
|
background:rgba(255,255,255,.08);
|
|
border:1px solid rgba(255,255,255,.15);
|
|
border-radius:10px;
|
|
color:var(--qx-white);
|
|
font-size:1rem;
|
|
font-family:var(--font);
|
|
transition:border-color .15s,box-shadow .15s;
|
|
-webkit-appearance:none;
|
|
}
|
|
input:focus{
|
|
outline:none;
|
|
border-color:var(--qx-blue);
|
|
box-shadow:0 0 0 3px rgba(0,102,255,.2);
|
|
}
|
|
input::placeholder{color:rgba(255,255,255,.3)}
|
|
.btn{
|
|
width:100%;
|
|
padding:16px;
|
|
background:var(--qx-blue);
|
|
border:none;
|
|
border-radius:10px;
|
|
color:var(--qx-white);
|
|
font-size:1rem;
|
|
font-weight:700;
|
|
cursor:pointer;
|
|
transition:opacity .15s,transform .15s;
|
|
font-family:var(--font);
|
|
margin-top:8px;
|
|
}
|
|
.btn:hover:not(:disabled){opacity:.9;transform:translateY(-1px)}
|
|
.btn:disabled{opacity:.5;cursor:not-allowed}
|
|
.error{
|
|
background:rgba(220,38,38,.15);
|
|
border:1px solid rgba(220,38,38,.3);
|
|
color:#ff6b6b;
|
|
padding:12px 16px;
|
|
border-radius:8px;
|
|
font-size:.875rem;
|
|
margin-bottom:16px;
|
|
display:none;
|
|
}
|
|
.error.show{display:block}
|
|
.success{
|
|
background:rgba(0,200,83,.15);
|
|
border:1px solid rgba(0,200,83,.3);
|
|
color:#4ade80;
|
|
padding:12px 16px;
|
|
border-radius:8px;
|
|
font-size:.875rem;
|
|
margin-bottom:16px;
|
|
display:none;
|
|
}
|
|
.success.show{display:block}
|
|
.forgot{
|
|
text-align:center;
|
|
margin-top:16px;
|
|
}
|
|
.forgot a{
|
|
color:rgba(255,255,255,.5);
|
|
font-size:.875rem;
|
|
text-decoration:none;
|
|
transition:color .15s;
|
|
}
|
|
.forgot a:hover{color:var(--qx-blue)}
|
|
.back{
|
|
text-align:center;
|
|
margin-top:24px;
|
|
}
|
|
.back a{
|
|
color:rgba(255,255,255,.5);
|
|
font-size:.875rem;
|
|
text-decoration:none;
|
|
display:inline-flex;
|
|
align-items:center;
|
|
gap:6px;
|
|
transition:color .15s;
|
|
}
|
|
.back a:hover{color:var(--qx-white)}
|
|
.spinner{
|
|
display:inline-block;
|
|
width:16px;
|
|
height:16px;
|
|
border:2px solid rgba(255,255,255,.3);
|
|
border-top-color:var(--qx-white);
|
|
border-radius:50%;
|
|
animation:spin .8s linear infinite;
|
|
margin-right:8px;
|
|
vertical-align:middle;
|
|
}
|
|
@keyframes spin{to{transform:rotate(360deg)}}
|
|
.hidden{display:none !important}
|
|
|
|
/* Dashboard */
|
|
.dashboard{
|
|
display:none;
|
|
width:100%;
|
|
max-width:420px;
|
|
}
|
|
.dashboard.show{display:block}
|
|
.dashboard-header{
|
|
display:flex;
|
|
justify-content:space-between;
|
|
align-items:center;
|
|
margin-bottom:24px;
|
|
}
|
|
.dashboard-header h2{font-size:1.25rem;font-weight:700}
|
|
.logout-btn{
|
|
background:rgba(255,255,255,.1);
|
|
border:none;
|
|
color:rgba(255,255,255,.7);
|
|
padding:8px 16px;
|
|
border-radius:8px;
|
|
font-size:.875rem;
|
|
cursor:pointer;
|
|
font-family:var(--font);
|
|
}
|
|
.logout-btn:hover{background:rgba(255,255,255,.15);color:var(--qx-white)}
|
|
.stats-grid{
|
|
display:grid;
|
|
grid-template-columns:1fr 1fr;
|
|
gap:12px;
|
|
margin-bottom:24px;
|
|
}
|
|
.stat-card{
|
|
background:rgba(255,255,255,.05);
|
|
border:1px solid rgba(255,255,255,.1);
|
|
border-radius:var(--qx-radius);
|
|
padding:20px;
|
|
text-align:center;
|
|
}
|
|
.stat-value{
|
|
font-size:1.75rem;
|
|
font-weight:800;
|
|
color:var(--qx-blue);
|
|
}
|
|
.stat-label{
|
|
font-size:.75rem;
|
|
color:rgba(255,255,255,.5);
|
|
margin-top:4px;
|
|
text-transform:uppercase;
|
|
letter-spacing:.05em;
|
|
}
|
|
.nav-links{
|
|
display:flex;
|
|
flex-direction:column;
|
|
gap:8px;
|
|
}
|
|
.nav-links a{
|
|
display:flex;
|
|
align-items:center;
|
|
gap:12px;
|
|
padding:16px;
|
|
background:rgba(255,255,255,.05);
|
|
border:1px solid rgba(255,255,255,.1);
|
|
border-radius:var(--qx-radius);
|
|
color:var(--qx-white);
|
|
text-decoration:none;
|
|
font-weight:600;
|
|
transition:background .15s;
|
|
}
|
|
.nav-links a:hover{background:rgba(255,255,255,.1)}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Login Form -->
|
|
<div class="login-container" id="loginContainer">
|
|
<div class="logo">
|
|
<h1>qui<span class="x">X</span>zoom</h1>
|
|
<p>Zoomer Portal</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<!-- Passwordless Section -->
|
|
<div class="passwordless-section" id="passwordlessSection">
|
|
<div class="passwordless-title">🔐 Snabb inloggning</div>
|
|
<div class="passwordless-desc">Öppna quiXzoom-appen och skanna QR-koden</div>
|
|
|
|
<div class="qr-container" id="qrContainer">
|
|
<div class="qr-loading">Laddar...</div>
|
|
</div>
|
|
|
|
<div class="status-text" id="statusText">Väntar på godkännande...</div>
|
|
|
|
<div class="app-buttons" id="appButtons">
|
|
<a href="#" class="app-btn" id="openAppBtn">📱 Öppna appen</a>
|
|
<a href="https://app.quixzoom.com/join" class="app-btn secondary">⬇️ Ladda ner app</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="divider">eller</div>
|
|
|
|
<!-- Traditional Login -->
|
|
<div class="tabs">
|
|
<button class="tab active" data-mode="login">Logga in</button>
|
|
<button class="tab" data-mode="signup">Bli zoomer</button>
|
|
</div>
|
|
|
|
<div class="error" id="errorMsg"></div>
|
|
<div class="success" id="successMsg"></div>
|
|
|
|
<form id="authForm">
|
|
<div class="form-group" id="nameGroup" style="display:none">
|
|
<label for="displayName">Namn (valfritt)</label>
|
|
<input type="text" id="displayName" placeholder="Vad ska folk kalla dig?">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="email">E-post</label>
|
|
<input type="email" id="email" placeholder="din@email.se" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="password">Lösenord</label>
|
|
<input type="password" id="password" placeholder="Minst 8 tecken" required minlength="8">
|
|
</div>
|
|
|
|
<button type="submit" class="btn" id="submitBtn">Logga in</button>
|
|
</form>
|
|
|
|
<div class="forgot">
|
|
<a href="/help/">Glömt lösenordet?</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="back">
|
|
<a href="/">← Tillbaka till quixzoom.se</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Dashboard (shown after login) -->
|
|
<div class="dashboard" id="dashboard">
|
|
<div class="logo" style="margin-bottom:24px">
|
|
<h1>qui<span class="x">X</span>zoom</h1>
|
|
</div>
|
|
|
|
<div class="dashboard-header">
|
|
<h2 id="welcomeName">Hej!</h2>
|
|
<button class="logout-btn" id="logoutBtn">Logga ut</button>
|
|
</div>
|
|
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-value" id="statEarnings">0 kr</div>
|
|
<div class="stat-label">Intjänat</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value" id="statMissions">0</div>
|
|
<div class="stat-label">Uppdrag</div>
|
|
</div>
|
|
</div>
|
|
|
|
<nav class="nav-links">
|
|
<a href="/app/">📱 Öppna appen</a>
|
|
<a href="/earnings/">💰 Intjäning & utbetalning</a>
|
|
<a href="/profile/">👤 Profil & inställningar</a>
|
|
<a href="/help/">❓ Hjälp & support</a>
|
|
</nav>
|
|
|
|
<div class="back" style="margin-top:24px">
|
|
<a href="/">← Till quixzoom.se</a>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API_BASE = 'https://api.quixzoom.com';
|
|
const TOKEN_KEY = '***';
|
|
const REFRESH_KEY = '***';
|
|
|
|
let currentMode = 'login';
|
|
let pollingInterval = null;
|
|
let currentSessionId = null;
|
|
let currentRequestToken = null;
|
|
|
|
// DOM elements
|
|
const loginContainer = document.getElementById('loginContainer');
|
|
const dashboard = document.getElementById('dashboard');
|
|
const authForm = document.getElementById('authForm');
|
|
const tabs = document.querySelectorAll('.tab');
|
|
const nameGroup = document.getElementById('nameGroup');
|
|
const submitBtn = document.getElementById('submitBtn');
|
|
const errorMsg = document.getElementById('errorMsg');
|
|
const successMsg = document.getElementById('successMsg');
|
|
const qrContainer = document.getElementById('qrContainer');
|
|
const statusText = document.getElementById('statusText');
|
|
const openAppBtn = document.getElementById('openAppBtn');
|
|
|
|
// ─── Passwordless Flow ─────────────────────────────────────────────────────
|
|
|
|
async function initiatePasswordless() {
|
|
try {
|
|
const res = await fetch(`${API_BASE}/auth/passwordless/initiate`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
client_id: 'web-dashboard',
|
|
redirect_url: window.location.href,
|
|
device_info: {
|
|
user_agent: navigator.userAgent,
|
|
ip: '' // Backend will detect
|
|
}
|
|
})
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const err = await res.json();
|
|
throw new Error(err.detail || 'Kunde inte initiera inloggning');
|
|
}
|
|
|
|
const data = await res.json();
|
|
currentSessionId = data.session_id;
|
|
currentRequestToken = data.request_token;
|
|
|
|
// Generate QR code
|
|
generateQR(data.qr_data);
|
|
|
|
// Set up deep link
|
|
openAppBtn.href = data.qr_data;
|
|
|
|
// Start polling
|
|
startPolling(data.session_id, data.request_token);
|
|
|
|
} catch (err) {
|
|
qrContainer.innerHTML = `<div class="qr-error">${err.message}</div>`;
|
|
statusText.textContent = 'Kunde inte ladda QR-kod. Försök med e-post/lösenord istället.';
|
|
}
|
|
}
|
|
|
|
function generateQR(data) {
|
|
// Simple QR code generation using Google Chart API
|
|
// For production, use a proper QR library
|
|
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=160x160&data=${encodeURIComponent(data)}`;
|
|
qrContainer.innerHTML = `<img src="${qrUrl}" alt="QR-kod för inloggning" width="160" height="160">`;
|
|
}
|
|
|
|
function startPolling(sessionId, requestToken) {
|
|
if (pollingInterval) clearInterval(pollingInterval);
|
|
|
|
let attempts = 0;
|
|
const maxAttempts = 180; // 6 minutes (2 second intervals)
|
|
|
|
pollingInterval = setInterval(async () => {
|
|
attempts++;
|
|
if (attempts > maxAttempts) {
|
|
clearInterval(pollingInterval);
|
|
statusText.textContent = 'Tiden gick ut. Försök igen.';
|
|
statusText.className = 'status-text rejected';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(
|
|
`${API_BASE}/auth/passwordless/status?session_id=${sessionId}&request_token=${requestToken}`
|
|
);
|
|
|
|
if (!res.ok) return;
|
|
|
|
const data = await res.json();
|
|
|
|
if (data.status === 'approved' || data.status === 'completed') {
|
|
clearInterval(pollingInterval);
|
|
statusText.textContent = '✅ Inloggning godkänd!';
|
|
statusText.className = 'status-text approved';
|
|
|
|
// Store tokens
|
|
if (data.access_token) {
|
|
localStorage.setItem(TOKEN_KEY, data.access_token);
|
|
if (data.refresh_token) {
|
|
localStorage.setItem(REFRESH_KEY, data.refresh_token);
|
|
}
|
|
}
|
|
|
|
// Show dashboard
|
|
setTimeout(() => {
|
|
showDashboard({ display_name: 'Zoomer', total_earned: 0, missions_completed: 0 });
|
|
}, 1000);
|
|
|
|
} else if (data.status === 'rejected') {
|
|
clearInterval(pollingInterval);
|
|
statusText.textContent = '❌ Inloggning nekad';
|
|
statusText.className = 'status-text rejected';
|
|
} else if (data.status === 'expired') {
|
|
clearInterval(pollingInterval);
|
|
statusText.textContent = 'Tiden gick ut. Försök igen.';
|
|
statusText.className = 'status-text rejected';
|
|
}
|
|
|
|
} catch (e) {
|
|
// Ignore polling errors
|
|
}
|
|
}, 2000);
|
|
}
|
|
|
|
// ─── Traditional Auth ──────────────────────────────────────────────────────
|
|
|
|
// Check if already logged in
|
|
async function checkAuth() {
|
|
const token = localStorage.getItem(TOKEN_KEY);
|
|
if (!token) {
|
|
// Start passwordless flow
|
|
initiatePasswordless();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/auth/me`, {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
if (res.ok) {
|
|
const user = await res.json();
|
|
showDashboard(user);
|
|
} else {
|
|
localStorage.removeItem(TOKEN_KEY);
|
|
localStorage.removeItem(REFRESH_KEY);
|
|
initiatePasswordless();
|
|
}
|
|
} catch (e) {
|
|
localStorage.removeItem(TOKEN_KEY);
|
|
localStorage.removeItem(REFRESH_KEY);
|
|
initiatePasswordless();
|
|
}
|
|
}
|
|
|
|
// Tab switching
|
|
tabs.forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
tabs.forEach(t => t.classList.remove('active'));
|
|
tab.classList.add('active');
|
|
currentMode = tab.dataset.mode;
|
|
|
|
if (currentMode === 'signup') {
|
|
nameGroup.style.display = 'block';
|
|
submitBtn.textContent = 'Skapa konto';
|
|
} else {
|
|
nameGroup.style.display = 'none';
|
|
submitBtn.textContent = 'Logga in';
|
|
}
|
|
hideMessages();
|
|
});
|
|
});
|
|
|
|
// Form submit
|
|
authForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
hideMessages();
|
|
|
|
const email = document.getElementById('email').value.trim();
|
|
const password = document.getElementById('password').value;
|
|
const displayName = document.getElementById('displayName').value.trim();
|
|
|
|
if (!email || !password) {
|
|
showError('Fyll i e-post och lösenord');
|
|
return;
|
|
}
|
|
|
|
if (currentMode === 'signup' && password.length < 8) {
|
|
showError('Lösenordet måste vara minst 8 tecken');
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
|
|
try {
|
|
const endpoint = currentMode === 'login' ? '/auth/login' : '/auth/register';
|
|
const body = currentMode === 'login'
|
|
? { email, password }
|
|
: { email, password, display_name: displayName || undefined };
|
|
|
|
const res = await fetch(`${API_BASE}${endpoint}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
});
|
|
|
|
const data = await res.json();
|
|
|
|
if (!res.ok) {
|
|
throw new Error(data.message || data.error || 'Inloggning misslyckades');
|
|
}
|
|
|
|
localStorage.setItem(TOKEN_KEY, data.access_token);
|
|
if (data.refresh_token) {
|
|
localStorage.setItem(REFRESH_KEY, data.refresh_token);
|
|
}
|
|
|
|
showSuccess(currentMode === 'login' ? 'Välkommen tillbaka!' : 'Konto skapat!');
|
|
showDashboard(data.zoomer);
|
|
|
|
} catch (err) {
|
|
showError(err.message || 'Något gick fel. Försök igen.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
});
|
|
|
|
// Logout
|
|
document.getElementById('logoutBtn').addEventListener('click', async () => {
|
|
const token = localStorage.getItem(TOKEN_KEY);
|
|
if (token) {
|
|
try {
|
|
await fetch(`${API_BASE}/auth/logout`, {
|
|
method: 'POST',
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
} catch (e) {
|
|
// best-effort
|
|
}
|
|
}
|
|
localStorage.removeItem(TOKEN_KEY);
|
|
localStorage.removeItem(REFRESH_KEY);
|
|
if (pollingInterval) clearInterval(pollingInterval);
|
|
location.reload();
|
|
});
|
|
|
|
function showDashboard(user) {
|
|
loginContainer.style.display = 'none';
|
|
dashboard.classList.add('show');
|
|
|
|
document.getElementById('welcomeName').textContent = `Hej ${user.display_name || user.email || 'Zoomer'}!`;
|
|
|
|
const earned = (user.total_earned || 0) / 100;
|
|
document.getElementById('statEarnings').textContent = earned > 0
|
|
? `${earned.toLocaleString('sv-SE')} kr`
|
|
: '0 kr';
|
|
|
|
document.getElementById('statMissions').textContent = user.missions_completed || 0;
|
|
}
|
|
|
|
function showError(msg) {
|
|
errorMsg.textContent = msg;
|
|
errorMsg.classList.add('show');
|
|
}
|
|
|
|
function showSuccess(msg) {
|
|
successMsg.textContent = msg;
|
|
successMsg.classList.add('show');
|
|
}
|
|
|
|
function hideMessages() {
|
|
errorMsg.classList.remove('show');
|
|
successMsg.classList.remove('show');
|
|
}
|
|
|
|
function setLoading(loading) {
|
|
submitBtn.disabled = loading;
|
|
if (loading) {
|
|
submitBtn.innerHTML = '<span class="spinner"></span>Vänta...';
|
|
} else {
|
|
submitBtn.textContent = currentMode === 'login' ? 'Logga in' : 'Skapa konto';
|
|
}
|
|
}
|
|
|
|
// Init
|
|
checkAuth();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|