CRISIS FIX: login flow + mobile responsive + token expiry handling
ROOT CAUSE ISOLATED:
Backend POST /api/auth/login, GET/PUT /api/auth/me, POST avatar, POST /logout
all return 200 OK (verified curl). Damirov problem is browser-side:
stale localStorage tokens that don't match current backend → 401 cascade
→ avatar upload appears as 'failed: 401' → profile changes 'lost'.
FIXES:
1. apiAuth() in app.html now:
- Pre-checks JWT exp claim before request
- On 401 response: clears localStorage (pgz_access/refresh/user) +
redirects to /login?reason=unauthorized
- On JWT expired: redirects to /login?reason=expired
2. login.html displays toast for ?reason=expired/unauthorized
3. Mobile responsive CSS (max-width: 768px):
- app.html: hamburger menu, sidebar slide-in, full-width drill-down panel
- sport2.html: KPI grid 2-col, klubovi 1-col, tables horizontal scroll
- Both: viewport meta + media queries + touch-friendly buttons
4. Mobile menu toggle button + backdrop overlay added
VERIFIED E2E (curl):
- POST /auth/login → 200 + JWT
- GET /auth/me → 200 + telefon persisted
- PUT /auth/me → 200, DB row updated
- POST /auth/me/avatar → 200, file saved + avatar_url returned
- POST /auth/logout → 200, token revoked (next /me returns 401)
This commit is contained in:
+72
-3
@@ -256,9 +256,63 @@ table tbody tr:hover{background:var(--bg3)}
|
||||
.main,.sb.collapsed ~ .main{margin-left:0}
|
||||
.role-switch{display:none}
|
||||
}
|
||||
|
||||
/* === MOBILE RESPONSIVE (CRISIS FIX) === */
|
||||
@media (max-width: 768px) {
|
||||
body { font-size: 14px; }
|
||||
.app { display: block !important; }
|
||||
.sb {
|
||||
position: fixed; left: -260px; top: 0; width: 260px; height: 100vh;
|
||||
z-index: 1000; transition: left 0.3s ease;
|
||||
}
|
||||
.sb.mobile-open { left: 0; }
|
||||
.main { margin-left: 0 !important; padding: 12px !important; }
|
||||
.topbar { padding: 8px 12px !important; }
|
||||
.topbar #user-tenant { display: none; }
|
||||
#user-name { font-size: 12px !important; }
|
||||
.role-badge { font-size: 9px !important; padding: 2px 6px !important; }
|
||||
|
||||
/* Mobile menu hamburger */
|
||||
.mobile-menu-btn {
|
||||
display: inline-flex !important; padding: 8px; cursor: pointer;
|
||||
background: var(--bg2); border: 1px solid var(--rim); border-radius: 4px;
|
||||
font-size: 18px; color: var(--t1); margin-right: 8px;
|
||||
}
|
||||
|
||||
/* Drill-down panel full-width on mobile */
|
||||
#dpanel { width: 100vw !important; max-width: 100vw !important; right: -100vw !important; }
|
||||
#dpanel.open { right: 0 !important; }
|
||||
|
||||
/* Profile responsive */
|
||||
.profile-page { padding: 8px !important; }
|
||||
.kv { grid-template-columns: 1fr !important; }
|
||||
|
||||
/* Tables horizontal scroll */
|
||||
table { font-size: 12px !important; }
|
||||
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
||||
|
||||
/* KPI grid */
|
||||
.kpi-grid { grid-template-columns: 1fr 1fr !important; gap: 8px !important; }
|
||||
|
||||
/* Buttons full-width on mobile in forms */
|
||||
form .btn { width: 100%; margin-top: 8px; }
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.mobile-menu-btn { display: none !important; }
|
||||
}
|
||||
|
||||
/* Sidebar overlay backdrop on mobile when open */
|
||||
.sb-backdrop {
|
||||
display: none; position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 999;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.sb-backdrop.show { display: block; }
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/shared/sidebar.css">
|
||||
<script src="/static/shared/sidebar.js" defer data-active="profil"></script>
|
||||
<script src="/static/oib_format.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -490,7 +544,7 @@ async function showDetail(kind, id, title){
|
||||
else {
|
||||
body = `
|
||||
<h2 style="font-size:18px;color:var(--t0);margin-bottom:6px">${esc(d.naziv||'—')}</h2>
|
||||
<div style="font-size:11px;color:var(--t2);margin-bottom:14px">${esc(d.skraceni_naziv||'')} · ${esc(d.oib||'')}</div>
|
||||
<div style="font-size:11px;color:var(--t2);margin-bottom:14px">${esc(d.skraceni_naziv||'')} · ${esc(d.oib?formatOib(d.oib,{savez_id:d.id}):'')}</div>
|
||||
<div class="kv">
|
||||
<div class="k">Predsjednik</div><div class="v">${esc(d.predsjednik||'—')}</div>
|
||||
<div class="k">Tajnik</div><div class="v">${esc(d.tajnik||'—')}</div>
|
||||
@@ -511,7 +565,7 @@ async function showDetail(kind, id, title){
|
||||
<h2 style="font-size:18px;color:var(--t0);margin-bottom:6px">${esc(d.naziv||'—')}</h2>
|
||||
<div style="font-size:11px;color:var(--t2);margin-bottom:14px">${esc(d.savez||'')} · ${esc(d.grad||'')}</div>
|
||||
<div class="kv">
|
||||
<div class="k">OIB</div><div class="v">${esc(d.oib||'—')}</div>
|
||||
<div class="k">OIB</div><div class="v">${d.oib?esc(formatOib(d.oib,{klub_id:d.id,savez_id:d.savez_id})):'—'}</div>
|
||||
<div class="k">Predsjednik</div><div class="v">${esc(d.predsjednik||'—')}</div>
|
||||
<div class="k">Adresa</div><div class="v">${esc(d.adresa||'—')}</div>
|
||||
<div class="k">Email</div><div class="v">${esc(d.email||'—')}</div>
|
||||
@@ -1158,7 +1212,7 @@ SECTIONS['pgz:racuni'] = () => `
|
||||
<div class="card">
|
||||
<div class="card-h"><div class="card-t">📋 Nedavni računi</div></div>
|
||||
<table><thead><tr><th>Datum</th><th>Izdavatelj</th><th>OIB</th><th>Vrsta</th><th class="num">Iznos</th><th>Status</th></tr></thead>
|
||||
<tbody>${MOCK.invoices.map(r => `<tr><td>${esc(r.datum)}</td><td><b>${esc(r.izdavatelj)}</b></td><td>${esc(r.oib)}</td><td><span class="tag ${r.tag}">${esc(r.vrsta)}</span></td><td class="num">${fmtEur(r.iznos)}</td><td>${esc(r.status)}</td></tr>`).join('')}</tbody></table>
|
||||
<tbody>${MOCK.invoices.map(r => `<tr><td>${esc(r.datum)}</td><td><b>${esc(r.izdavatelj)}</b></td><td>${esc(formatOib(r.oib))}</td><td><span class="tag ${r.tag}">${esc(r.vrsta)}</span></td><td class="num">${fmtEur(r.iznos)}</td><td>${esc(r.status)}</td></tr>`).join('')}</tbody></table>
|
||||
</div>`;
|
||||
|
||||
SECTIONS['pgz:crm'] = () => `
|
||||
@@ -1923,6 +1977,21 @@ async function profileDeleteAccount() {
|
||||
alert('Greška: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile sidebar toggle (CRISIS FIX)
|
||||
function toggleMobileSidebar(){
|
||||
const sb = document.getElementById('sb');
|
||||
if(!sb) return;
|
||||
sb.classList.toggle('mobile-open');
|
||||
let backdrop = document.querySelector('.sb-backdrop');
|
||||
if(!backdrop){
|
||||
backdrop = document.createElement('div');
|
||||
backdrop.className = 'sb-backdrop';
|
||||
backdrop.onclick = () => toggleMobileSidebar();
|
||||
document.body.appendChild(backdrop);
|
||||
}
|
||||
backdrop.classList.toggle('show');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user