CC3: Unified sidebar with external portal links + collapsible icon mode
Shared module:
- /static/shared/sidebar.css ← unified CSS (#pgz-sb, .pgz-collapsed, mobile overlay, tooltip)
- /static/shared/sidebar.js ← auto-mounting JS shell + PGZSidebar API
* Auto-renders #pgz-sb na <body> start (data-inline=1 to opt out)
* NAV_EXTERNAL: Prijava, Aplikacija, Administracija, CRM, ERP, KPI, Audit, Public portal
* Toggle (≡) -> localStorage 'sidebarCollapsed' (perzistira preko SVIH stranica)
* Mobile <768px: ≡ burger + ✕ close, body backdrop
* Loads /api/auth/me u footer (avatar/username/uloga); ⎋ logout briše JWT i ide na /login
* data-active="<key>" highlight aktivnog portala
Page integration:
- sport2.html ← inline NAV_EXTERNAL u buildNav() + "Portali" separator (zadrži postojeći sidebar)
- app.html ← inline NAV_EXTERNAL u buildNav() (zadrži role-based interni nav, dopuni Portalima)
- admin.html ← Portali stavke u <aside class="sidebar"> (matching .nav-item style)
- erp.html ← Portali stavke u <aside class="sidebar"> (matching .nav-item style)
- crm.html ← include shared sidebar.css + sidebar.js data-active="crm"
- audit.html ← include shared sidebar.css + sidebar.js data-active="audit"
- kpi.html ← include shared sidebar.css + sidebar.js data-active="kpi"
- login.html ← include shared sidebar.css + sidebar.js data-active="login"
Backups: _backups/{*.cc3_pre_unified_sidebar.*}
Live verified: 8 pages serve HTTP 200; sidebar.css/js HTTP 200; portal markers per page OK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -204,6 +204,16 @@ td.num { font-family: 'JetBrains Mono', monospace; text-align: right; }
|
|||||||
<label>Aktivan tenant</label>
|
<label>Aktivan tenant</label>
|
||||||
<select id="tenantSel"></select>
|
<select id="tenantSel"></select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style="padding:14px 14px 4px 14px;font-size:9.5px;color:var(--text-dim,#8a95b4);text-transform:uppercase;letter-spacing:1.2px;font-weight:700">Portali</div>
|
||||||
|
<a class="nav-item" href="/sport/login"><span class="icon">🔑</span><span>Prijava</span></a>
|
||||||
|
<a class="nav-item" href="/sport/app"><span class="icon">📱</span><span>Aplikacija</span></a>
|
||||||
|
<a class="nav-item active" href="/sport/admin"><span class="icon">🛡</span><span>Administracija</span></a>
|
||||||
|
<a class="nav-item" href="/sport/crm"><span class="icon">👥</span><span>CRM</span></a>
|
||||||
|
<a class="nav-item" href="/sport/erp"><span class="icon">💰</span><span>ERP</span></a>
|
||||||
|
<a class="nav-item" href="/sport/kpi"><span class="icon">📈</span><span>KPI</span></a>
|
||||||
|
<a class="nav-item" href="/sport/audit"><span class="icon">📋</span><span>Audit</span></a>
|
||||||
|
<a class="nav-item" href="/sport/static/sport2.html" target="_blank"><span class="icon">🌐</span><span>Public portal</span></a>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="main">
|
<main class="main">
|
||||||
|
|||||||
+25
-1
@@ -72,6 +72,11 @@ button,input,select,textarea{font-family:inherit;font-size:inherit;outline:none}
|
|||||||
.sb.collapsed .sb-foot{padding:8px;justify-content:center}
|
.sb.collapsed .sb-foot{padding:8px;justify-content:center}
|
||||||
.sb.collapsed .sb-foot .ui{display:none}
|
.sb.collapsed .sb-foot .ui{display:none}
|
||||||
.sb.collapsed .sb-foot .lo{display:none}
|
.sb.collapsed .sb-foot .lo{display:none}
|
||||||
|
.sb.collapsed .nav-sep{font-size:0;padding:6px 0;text-align:center;border-top:1px dashed var(--rim);margin:6px 8px 4px}
|
||||||
|
.sb.collapsed .nav-ext span:last-child{display:none}
|
||||||
|
.nav-ext{color:var(--cyan)}
|
||||||
|
.nav-ext:hover{color:var(--pgz-gold);background:var(--bg2)}
|
||||||
|
.nav-ext.active{background:linear-gradient(90deg,var(--pgz-blue) 0%,var(--pgz-blue2) 100%);color:#fff}
|
||||||
|
|
||||||
.main{margin-left:240px;flex:1;min-width:0;transition:margin-left .22s ease}
|
.main{margin-left:240px;flex:1;min-width:0;transition:margin-left .22s ease}
|
||||||
.sb.collapsed ~ .main{margin-left:58px}
|
.sb.collapsed ~ .main{margin-left:58px}
|
||||||
@@ -592,15 +597,34 @@ function setRole(r){
|
|||||||
}
|
}
|
||||||
|
|
||||||
//=========== NAV ===========
|
//=========== NAV ===========
|
||||||
|
const NAV_EXTERNAL = [
|
||||||
|
{id:'login', href:'/sport/login', ic:'\u{1F511}', label:'Prijava'},
|
||||||
|
{id:'app', href:'/sport/app', ic:'\u{1F4F1}', label:'Aplikacija'},
|
||||||
|
{id:'admin', href:'/sport/admin', ic:'\u{1F6E1}', label:'Administracija'},
|
||||||
|
{id:'crm', href:'/sport/crm', ic:'\u{1F465}', label:'CRM'},
|
||||||
|
{id:'erp', href:'/sport/erp', ic:'\u{1F4B0}', label:'ERP'},
|
||||||
|
{id:'kpi', href:'/sport/kpi', ic:'\u{1F4C8}', label:'KPI'},
|
||||||
|
{id:'audit', href:'/sport/audit', ic:'\u{1F4CB}', label:'Audit'},
|
||||||
|
{id:'sport2', href:'/sport/static/sport2.html', ic:'\u{1F310}', label:'Public portal'}
|
||||||
|
];
|
||||||
function buildNav(){
|
function buildNav(){
|
||||||
const items = NAV_BY_ROLE[_state.role] || [];
|
const items = NAV_BY_ROLE[_state.role] || [];
|
||||||
$('#nav').innerHTML = items.map(n =>
|
let html = items.map(n =>
|
||||||
`<div class="nav-i ${n.id===_state.section?'active':''}" data-id="${n.id}" data-label="${esc(n.label)}" onclick="navTo('${n.id}')">
|
`<div class="nav-i ${n.id===_state.section?'active':''}" data-id="${n.id}" data-label="${esc(n.label)}" onclick="navTo('${n.id}')">
|
||||||
<span class="ic">${n.ic}</span>
|
<span class="ic">${n.ic}</span>
|
||||||
<span class="lbl">${esc(n.label)}</span>
|
<span class="lbl">${esc(n.label)}</span>
|
||||||
${n.badge?`<span class="badge">${n.badge}</span>`:''}
|
${n.badge?`<span class="badge">${n.badge}</span>`:''}
|
||||||
</div>`
|
</div>`
|
||||||
).join('');
|
).join('');
|
||||||
|
// PORTALI separator + external links (active = 'app')
|
||||||
|
html += '<div class="nav-sep" style="padding:14px 14px 4px 14px;font-size:9.5px;color:var(--t4);text-transform:uppercase;letter-spacing:1.2px;font-weight:700;white-space:nowrap;overflow:hidden">Portali</div>';
|
||||||
|
html += NAV_EXTERNAL.map(n =>
|
||||||
|
`<a class="nav-i nav-ext ${n.id==='app'?'active':''}" href="${n.href}" data-id="${n.id}" data-label="${esc(n.label)}" style="text-decoration:none">
|
||||||
|
<span class="ic">${n.ic}</span><span class="lbl">${esc(n.label)}</span>
|
||||||
|
<span style="margin-left:auto;font-size:10px;opacity:.5">↗</span>
|
||||||
|
</a>`
|
||||||
|
).join('');
|
||||||
|
$('#nav').innerHTML = html;
|
||||||
}
|
}
|
||||||
function navTo(id){
|
function navTo(id){
|
||||||
_state.section = id;
|
_state.section = id;
|
||||||
|
|||||||
@@ -33,7 +33,10 @@
|
|||||||
.stat { background:var(--bg1); padding:14px; border-radius:8px; border-left:3px solid var(--pgz-blue); }
|
.stat { background:var(--bg1); padding:14px; border-radius:8px; border-left:3px solid var(--pgz-blue); }
|
||||||
.stat .v { font-size:1.6rem; font-weight:700; color:var(--pgz-gold); }
|
.stat .v { font-size:1.6rem; font-weight:700; color:var(--pgz-gold); }
|
||||||
.stat .l { color:var(--muted); font-size:0.82rem; text-transform:uppercase; margin-top:3px; }
|
.stat .l { color:var(--muted); font-size:0.82rem; text-transform:uppercase; margin-top:3px; }
|
||||||
|
body{padding:20px}
|
||||||
</style>
|
</style>
|
||||||
|
<link rel="stylesheet" href="/sport/static/shared/sidebar.css">
|
||||||
|
<script src="/sport/static/shared/sidebar.js" defer data-active="audit"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>📜 Audit Log</h1>
|
<h1>📜 Audit Log</h1>
|
||||||
|
|||||||
@@ -135,6 +135,8 @@ table tr:hover td { background: rgba(26, 115, 232, 0.05); }
|
|||||||
.toast.show { transform: translateX(0); }
|
.toast.show { transform: translateX(0); }
|
||||||
.toast.err { border-left-color: var(--err); }
|
.toast.err { border-left-color: var(--err); }
|
||||||
</style>
|
</style>
|
||||||
|
<link rel="stylesheet" href="/sport/static/shared/sidebar.css">
|
||||||
|
<script src="/sport/static/shared/sidebar.js" defer data-active="crm"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,16 @@ tr.clickable:hover { background:var(--bg-3); box-shadow:inset 3px 0 0 var(--acce
|
|||||||
<div class="nav-item" data-tab="invoices"><span>€</span><span>Računi</span></div>
|
<div class="nav-item" data-tab="invoices"><span>€</span><span>Računi</span></div>
|
||||||
<div class="nav-item" data-tab="putni"><span>🚗</span><span>Novi putni nalog</span></div>
|
<div class="nav-item" data-tab="putni"><span>🚗</span><span>Novi putni nalog</span></div>
|
||||||
<div class="nav-item" data-tab="putni-list"><span>📋</span><span>Lista putnih naloga</span></div>
|
<div class="nav-item" data-tab="putni-list"><span>📋</span><span>Lista putnih naloga</span></div>
|
||||||
|
|
||||||
|
<div style="padding:14px 20px 4px;font-size:9.5px;color:var(--text-2);text-transform:uppercase;letter-spacing:1.2px;font-weight:700">Portali</div>
|
||||||
|
<a class="nav-item" href="/sport/login" style="text-decoration:none"><span>🔑</span><span>Prijava</span></a>
|
||||||
|
<a class="nav-item" href="/sport/app" style="text-decoration:none"><span>📱</span><span>Aplikacija</span></a>
|
||||||
|
<a class="nav-item" href="/sport/admin" style="text-decoration:none"><span>🛡</span><span>Administracija</span></a>
|
||||||
|
<a class="nav-item" href="/sport/crm" style="text-decoration:none"><span>👥</span><span>CRM</span></a>
|
||||||
|
<a class="nav-item active" href="/sport/erp" style="text-decoration:none"><span>💰</span><span>ERP</span></a>
|
||||||
|
<a class="nav-item" href="/sport/kpi" style="text-decoration:none"><span>📈</span><span>KPI</span></a>
|
||||||
|
<a class="nav-item" href="/sport/audit" style="text-decoration:none"><span>📋</span><span>Audit</span></a>
|
||||||
|
<a class="nav-item" href="/sport/static/sport2.html" target="_blank" style="text-decoration:none"><span>🌐</span><span>Public portal</span></a>
|
||||||
</aside>
|
</aside>
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
|
|||||||
@@ -21,7 +21,10 @@
|
|||||||
tr:hover { background: #1a2030; }
|
tr:hover { background: #1a2030; }
|
||||||
.updated { color: #678; font-size: 11px; }
|
.updated { color: #678; font-size: 11px; }
|
||||||
.refresh { background: #4af; color: #fff; border: none; padding: 4px 12px; border-radius: 4px; cursor: pointer; }
|
.refresh { background: #4af; color: #fff; border: none; padding: 4px 12px; border-radius: 4px; cursor: pointer; }
|
||||||
|
body{padding:20px}
|
||||||
</style>
|
</style>
|
||||||
|
<link rel="stylesheet" href="/sport/static/shared/sidebar.css">
|
||||||
|
<script src="/sport/static/shared/sidebar.js" defer data-active="kpi"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>RINET KPI Dashboard <span class="updated" id="updated"></span> <button class="refresh" onclick="load()">↻</button></h1>
|
<h1>RINET KPI Dashboard <span class="updated" id="updated"></span> <button class="refresh" onclick="load()">↻</button></h1>
|
||||||
|
|||||||
@@ -310,6 +310,8 @@ body {
|
|||||||
.cookie-actions button:hover { color: var(--text); border-color: var(--accent); }
|
.cookie-actions button:hover { color: var(--text); border-color: var(--accent); }
|
||||||
.cookie a { color: var(--accent); text-decoration: none; }
|
.cookie a { color: var(--accent); text-decoration: none; }
|
||||||
</style>
|
</style>
|
||||||
|
<link rel="stylesheet" href="/sport/static/shared/sidebar.css">
|
||||||
|
<script src="/sport/static/shared/sidebar.js" defer data-active="login"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
/* PGŽ SPORT — Unified Sidebar v1.0
|
||||||
|
* dradulic@outlook.com / damir@rinet.one — 2026-05-05
|
||||||
|
* Used by: sport2.html, app.html, admin.html, crm.html, erp.html, audit.html, kpi.html, login.html
|
||||||
|
* Reference: app.rinet.one/klasik/control
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root{
|
||||||
|
--pgz-blue:#003087; --pgz-blue2:#004CC4; --pgz-gold:#F4C430;
|
||||||
|
--bg0:#08090e; --bg1:#0d1021; --bg2:#111628; --bg3:#161d35; --bg4:#1c2542;
|
||||||
|
--rim:#1e2a50; --rim2:#283560;
|
||||||
|
--t0:#fff; --t1:#e2e6f0; --t2:#8a95b4; --t4:#4e5a7a;
|
||||||
|
--green:#00e88f; --red:#ff2d55; --amber:#f59e0b; --cyan:#00c8e8;
|
||||||
|
--sb-w-exp:230px; --sb-w-col:58px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pgz-sb{
|
||||||
|
position:fixed; top:0; left:0; bottom:0; width:var(--sb-w-exp);
|
||||||
|
background:linear-gradient(180deg,var(--bg1) 0%,var(--bg0) 100%);
|
||||||
|
border-right:1px solid var(--rim);
|
||||||
|
display:flex; flex-direction:column; z-index:100;
|
||||||
|
font-family:'Inter',sans-serif; font-size:13px; color:var(--t1);
|
||||||
|
transition:width .22s ease, transform .22s ease;
|
||||||
|
}
|
||||||
|
#pgz-sb *{box-sizing:border-box}
|
||||||
|
#pgz-sb a{text-decoration:none;color:inherit}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.pgz-sb-h{padding:18px 18px 14px;border-bottom:1px solid var(--rim);position:relative;flex-shrink:0}
|
||||||
|
.pgz-sb-h .pgz-logo{font-weight:800;font-size:14px;color:var(--t0);letter-spacing:.5px;white-space:nowrap;overflow:hidden}
|
||||||
|
.pgz-sb-h .pgz-logo .g{color:var(--pgz-gold)}
|
||||||
|
.pgz-sb-h .pgz-sub{font-size:10px;color:var(--t2);margin-top:4px;text-transform:uppercase;letter-spacing:1px;white-space:nowrap;overflow:hidden}
|
||||||
|
.pgz-sb-toggle{
|
||||||
|
position:absolute;top:14px;right:8px;width:24px;height:24px;
|
||||||
|
display:flex;align-items:center;justify-content:center;cursor:pointer;
|
||||||
|
color:var(--t2);background:var(--bg2);border:1px solid var(--rim);
|
||||||
|
border-radius:5px;font-size:14px;font-weight:700;
|
||||||
|
transition:all .15s;user-select:none;
|
||||||
|
}
|
||||||
|
.pgz-sb-toggle:hover{background:var(--bg3);color:var(--pgz-gold);border-color:var(--pgz-gold)}
|
||||||
|
|
||||||
|
/* Section label / separator */
|
||||||
|
.pgz-sb-sep{padding:14px 14px 4px 14px;font-size:9.5px;color:var(--t4);
|
||||||
|
text-transform:uppercase;letter-spacing:1.2px;font-weight:700;
|
||||||
|
white-space:nowrap;overflow:hidden}
|
||||||
|
|
||||||
|
/* Nav */
|
||||||
|
.pgz-sb-nav{flex:1;padding:6px 8px;overflow-y:auto;overflow-x:hidden}
|
||||||
|
.pgz-sb-nav::-webkit-scrollbar{width:6px}
|
||||||
|
.pgz-sb-nav::-webkit-scrollbar-thumb{background:var(--rim2);border-radius:3px}
|
||||||
|
.pgz-nav-i{
|
||||||
|
padding:9px 12px;border-radius:6px;color:var(--t2);
|
||||||
|
cursor:pointer;display:flex;align-items:center;gap:10px;
|
||||||
|
font-size:12.5px;margin-bottom:2px;white-space:nowrap;
|
||||||
|
transition:background .15s,color .15s;position:relative;
|
||||||
|
}
|
||||||
|
.pgz-nav-i:hover{background:var(--bg2);color:var(--t1)}
|
||||||
|
.pgz-nav-i.active{
|
||||||
|
background:linear-gradient(90deg,var(--pgz-blue) 0%,var(--pgz-blue2) 100%);
|
||||||
|
color:#fff;font-weight:600;
|
||||||
|
}
|
||||||
|
.pgz-nav-i .ic{width:20px;text-align:center;font-size:14px;flex-shrink:0}
|
||||||
|
.pgz-nav-i .lbl{overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0}
|
||||||
|
.pgz-nav-i .badge{margin-left:auto;background:var(--red);color:#fff;font-size:9px;font-weight:700;padding:1px 6px;border-radius:8px;flex-shrink:0}
|
||||||
|
.pgz-nav-ext{color:var(--cyan)}
|
||||||
|
.pgz-nav-ext::after{content:"↗";font-size:10px;opacity:.5;margin-left:auto;flex-shrink:0}
|
||||||
|
.pgz-nav-ext:hover{color:var(--pgz-gold);background:var(--bg2)}
|
||||||
|
.pgz-nav-ext.active{background:linear-gradient(90deg,var(--pgz-blue) 0%,var(--pgz-blue2) 100%);color:#fff}
|
||||||
|
.pgz-nav-ext.active::after{opacity:.85}
|
||||||
|
|
||||||
|
/* Footer (user) */
|
||||||
|
.pgz-sb-foot{padding:10px 12px;border-top:1px solid var(--rim);
|
||||||
|
display:flex;align-items:center;gap:8px;
|
||||||
|
white-space:nowrap;overflow:hidden;flex-shrink:0}
|
||||||
|
.pgz-sb-foot .av{
|
||||||
|
width:30px;height:30px;border-radius:50%;
|
||||||
|
background:linear-gradient(135deg,var(--pgz-blue),var(--pgz-gold));
|
||||||
|
color:#fff;font-weight:800;display:flex;align-items:center;justify-content:center;
|
||||||
|
font-size:11px;flex-shrink:0;overflow:hidden;
|
||||||
|
}
|
||||||
|
.pgz-sb-foot .av img{width:100%;height:100%;object-fit:cover}
|
||||||
|
.pgz-sb-foot .ui{flex:1;min-width:0;overflow:hidden}
|
||||||
|
.pgz-sb-foot .un{font-size:11.5px;color:var(--t1);font-weight:600;line-height:1.2;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.pgz-sb-foot .ur{font-size:9.5px;color:var(--t4);text-transform:uppercase;letter-spacing:.5px;line-height:1.2;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.pgz-sb-foot .lo{cursor:pointer;color:var(--t4);font-size:14px;
|
||||||
|
padding:6px 8px;border-radius:5px;transition:all .15s;flex-shrink:0}
|
||||||
|
.pgz-sb-foot .lo:hover{background:rgba(255,45,85,.15);color:var(--red)}
|
||||||
|
|
||||||
|
/* Mobile burger (shown <768px when sidebar is offscreen) */
|
||||||
|
.pgz-sb-burger{
|
||||||
|
position:fixed;top:10px;left:10px;z-index:99;
|
||||||
|
width:36px;height:36px;display:none;align-items:center;justify-content:center;
|
||||||
|
background:var(--bg2);border:1px solid var(--rim);border-radius:6px;
|
||||||
|
color:var(--t1);font-size:18px;cursor:pointer;
|
||||||
|
}
|
||||||
|
.pgz-sb-burger:hover{background:var(--bg3);color:var(--pgz-gold)}
|
||||||
|
|
||||||
|
/* Mobile X (shown <768px when sidebar is open) */
|
||||||
|
.pgz-sb-mx{display:none;cursor:pointer;color:var(--t2);font-size:18px;
|
||||||
|
width:24px;height:24px;align-items:center;justify-content:center;
|
||||||
|
border-radius:5px;transition:all .15s}
|
||||||
|
.pgz-sb-mx:hover{background:var(--bg3);color:var(--red)}
|
||||||
|
|
||||||
|
/* ─── Collapsed state ─── */
|
||||||
|
#pgz-sb.pgz-collapsed{width:var(--sb-w-col)}
|
||||||
|
#pgz-sb.pgz-collapsed .pgz-sb-h{padding:18px 6px 14px;text-align:center}
|
||||||
|
#pgz-sb.pgz-collapsed .pgz-sb-h .pgz-logo{font-size:0}
|
||||||
|
#pgz-sb.pgz-collapsed .pgz-sb-h .pgz-logo::before{content:"PG";font-size:13px;color:var(--pgz-gold);font-weight:800}
|
||||||
|
#pgz-sb.pgz-collapsed .pgz-sb-h .pgz-sub{display:none}
|
||||||
|
#pgz-sb.pgz-collapsed .pgz-sb-toggle{position:static;margin:6px auto 0;display:flex}
|
||||||
|
#pgz-sb.pgz-collapsed .pgz-sb-sep{font-size:0;padding:6px 0;text-align:center;border-top:1px dashed var(--rim);margin:6px 8px 4px}
|
||||||
|
#pgz-sb.pgz-collapsed .pgz-nav-i{justify-content:center;padding:10px 6px}
|
||||||
|
#pgz-sb.pgz-collapsed .pgz-nav-i .lbl,
|
||||||
|
#pgz-sb.pgz-collapsed .pgz-nav-i .badge,
|
||||||
|
#pgz-sb.pgz-collapsed .pgz-nav-ext::after{display:none}
|
||||||
|
#pgz-sb.pgz-collapsed .pgz-sb-foot{padding:10px 6px;justify-content:center}
|
||||||
|
#pgz-sb.pgz-collapsed .pgz-sb-foot .ui,
|
||||||
|
#pgz-sb.pgz-collapsed .pgz-sb-foot .lo{display:none}
|
||||||
|
|
||||||
|
/* Tooltip when collapsed */
|
||||||
|
#pgz-sb.pgz-collapsed .pgz-nav-i:hover::after{
|
||||||
|
content:attr(data-label);
|
||||||
|
position:absolute;left:calc(var(--sb-w-col) - 4px);top:50%;transform:translateY(-50%);
|
||||||
|
background:var(--bg3);color:var(--t0);
|
||||||
|
padding:5px 10px;border-radius:4px;
|
||||||
|
font-size:11.5px;white-space:nowrap;
|
||||||
|
border:1px solid var(--rim);font-weight:600;
|
||||||
|
box-shadow:2px 2px 10px rgba(0,0,0,.45);
|
||||||
|
pointer-events:none;z-index:200;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout helper — apply on body to push content right of sidebar */
|
||||||
|
body.pgz-has-sb{padding-left:var(--sb-w-exp);transition:padding-left .22s ease}
|
||||||
|
body.pgz-has-sb.pgz-sb-col{padding-left:var(--sb-w-col)}
|
||||||
|
|
||||||
|
/* Mobile: <768px */
|
||||||
|
@media (max-width:768px){
|
||||||
|
#pgz-sb{transform:translateX(-100%)}
|
||||||
|
#pgz-sb.pgz-mobile-open{transform:translateX(0)}
|
||||||
|
#pgz-sb.pgz-collapsed{width:var(--sb-w-exp)} /* full width on mobile when open */
|
||||||
|
body.pgz-has-sb,body.pgz-has-sb.pgz-sb-col{padding-left:0}
|
||||||
|
.pgz-sb-burger{display:flex}
|
||||||
|
.pgz-sb-mx{display:flex}
|
||||||
|
.pgz-sb-toggle{display:none}
|
||||||
|
/* overlay backdrop */
|
||||||
|
body.pgz-mobile-sb-open::before{
|
||||||
|
content:"";position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:99;backdrop-filter:blur(2px)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
/* PGŽ SPORT — Unified Sidebar v1.0
|
||||||
|
* dradulic@outlook.com / damir@rinet.one — 2026-05-05
|
||||||
|
*
|
||||||
|
* Usage on each page:
|
||||||
|
* <link rel="stylesheet" href="/static/shared/sidebar.css">
|
||||||
|
* <script src="/static/shared/sidebar.js" defer
|
||||||
|
* data-active="app" // page key for highlight: app|admin|crm|erp|kpi|audit|login|sport2
|
||||||
|
* data-inline="0"></script> // 0 (default) = render on load. 1 = call PGZSidebar.mount() yourself
|
||||||
|
*
|
||||||
|
* The script renders #pgz-sb at start of <body>, adds class "pgz-has-sb" to body
|
||||||
|
* (so existing layouts can be migrated). Pages that already have their own sidebar
|
||||||
|
* should pass data-skip="1" — only NAV_EXTERNAL portal links will be appended to
|
||||||
|
* an element with id="pgz-portal-mount" if present.
|
||||||
|
*/
|
||||||
|
(function(){
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ────────── Configuration ──────────
|
||||||
|
// Per-portal "internal" sections (left as a hint; pages typically own their own internal nav)
|
||||||
|
// External portal links — same on every page
|
||||||
|
const NAV_EXTERNAL = [
|
||||||
|
{id:'login', href:'/sport/login', ic:'\u{1F511}', label:'Prijava'},
|
||||||
|
{id:'app', href:'/sport/app', ic:'\u{1F4F1}', label:'Aplikacija'},
|
||||||
|
{id:'admin', href:'/sport/admin', ic:'\u{1F6E1}', label:'Administracija'},
|
||||||
|
{id:'crm', href:'/sport/crm', ic:'\u{1F465}', label:'CRM'},
|
||||||
|
{id:'erp', href:'/sport/erp', ic:'\u{1F4B0}', label:'ERP'},
|
||||||
|
{id:'kpi', href:'/sport/kpi', ic:'\u{1F4C8}', label:'KPI'},
|
||||||
|
{id:'audit', href:'/sport/audit', ic:'\u{1F4CB}', label:'Audit'},
|
||||||
|
{id:'sport2', href:'/sport/static/sport2.html', ic:'\u{1F310}', label:'Public portal'}
|
||||||
|
];
|
||||||
|
|
||||||
|
const STATE_KEY = 'sidebarCollapsed'; // shared across all pages
|
||||||
|
const $ = (s, root) => (root||document).querySelector(s);
|
||||||
|
|
||||||
|
function readToken(){
|
||||||
|
try { return localStorage.getItem('jwt') || localStorage.getItem('access_token') || ''; }
|
||||||
|
catch(e){ return ''; }
|
||||||
|
}
|
||||||
|
function logout(){
|
||||||
|
if(!confirm('Odjava iz aplikacije?')) return;
|
||||||
|
try { localStorage.removeItem('jwt'); localStorage.removeItem('access_token'); localStorage.removeItem('app-role'); } catch(e){}
|
||||||
|
location.href = '/sport/login';
|
||||||
|
}
|
||||||
|
function initials(n){
|
||||||
|
if(!n) return '?';
|
||||||
|
const p = String(n).trim().split(/\s+/);
|
||||||
|
return ((p[0]||'')[0]||'').toUpperCase() + ((p[1]||'')[0]||'').toUpperCase();
|
||||||
|
}
|
||||||
|
function esc(s){
|
||||||
|
return String(s==null?'':s).replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to read /api/auth/me for footer display (best effort)
|
||||||
|
async function tryLoadMe(){
|
||||||
|
const tok = readToken(); if(!tok) return null;
|
||||||
|
try {
|
||||||
|
const r = await fetch('/sport/api/auth/me', {headers:{'Authorization':'Bearer '+tok}});
|
||||||
|
if(!r.ok) return null;
|
||||||
|
return await r.json();
|
||||||
|
} catch(e){ return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderShell(activeKey, internalNavHTML){
|
||||||
|
const sb = document.createElement('aside');
|
||||||
|
sb.id = 'pgz-sb';
|
||||||
|
sb.innerHTML = `
|
||||||
|
<div class="pgz-sb-h">
|
||||||
|
<div class="pgz-logo">PGŽ <span class="g">SPORT</span></div>
|
||||||
|
<div class="pgz-sub">Operativna platforma</div>
|
||||||
|
<div class="pgz-sb-toggle" onclick="PGZSidebar.toggle()" title="Skupi/raširi (≡)">≡</div>
|
||||||
|
<div class="pgz-sb-mx" onclick="PGZSidebar.closeMobile()" title="Zatvori">✕</div>
|
||||||
|
</div>
|
||||||
|
${internalNavHTML ? `<div class="pgz-sb-sep">Sekcije</div>` : ''}
|
||||||
|
<nav class="pgz-sb-nav" id="pgz-sb-nav">
|
||||||
|
${internalNavHTML || ''}
|
||||||
|
<div class="pgz-sb-sep" id="pgz-portal-sep">Portali</div>
|
||||||
|
<div id="pgz-portal-mount">${renderExternal(activeKey)}</div>
|
||||||
|
</nav>
|
||||||
|
<div class="pgz-sb-foot" id="pgz-sb-foot">
|
||||||
|
<div class="av" id="pgz-sb-av">PG</div>
|
||||||
|
<div class="ui">
|
||||||
|
<div class="un" id="pgz-sb-un">Gost</div>
|
||||||
|
<div class="ur" id="pgz-sb-ur">Demo</div>
|
||||||
|
</div>
|
||||||
|
<div class="lo" onclick="PGZSidebar.logout()" title="Odjava">⎋</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return sb;
|
||||||
|
}
|
||||||
|
function renderExternal(activeKey){
|
||||||
|
return NAV_EXTERNAL.map(n => `
|
||||||
|
<a class="pgz-nav-i pgz-nav-ext ${n.id===activeKey?'active':''}"
|
||||||
|
href="${n.href}" data-id="${n.id}" data-label="${esc(n.label)}">
|
||||||
|
<span class="ic">${n.ic}</span>
|
||||||
|
<span class="lbl">${esc(n.label)}</span>
|
||||||
|
</a>`).join('');
|
||||||
|
}
|
||||||
|
function renderBurger(){
|
||||||
|
if(document.getElementById('pgz-sb-burger')) return;
|
||||||
|
const b = document.createElement('div');
|
||||||
|
b.id = 'pgz-sb-burger';
|
||||||
|
b.className = 'pgz-sb-burger';
|
||||||
|
b.innerHTML = '≡';
|
||||||
|
b.onclick = () => PGZSidebar.openMobile();
|
||||||
|
document.body.appendChild(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setUserDisplay(me){
|
||||||
|
if(!me){
|
||||||
|
$('#pgz-sb-un') && ($('#pgz-sb-un').textContent = 'Gost');
|
||||||
|
$('#pgz-sb-ur') && ($('#pgz-sb-ur').textContent = 'Demo · click Prijava');
|
||||||
|
$('#pgz-sb-av') && ($('#pgz-sb-av').textContent = '?');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const name = me.full_name || ((me.ime||'')+' '+(me.prezime||'')).trim() || me.email || '—';
|
||||||
|
const role = me.user_type || '';
|
||||||
|
const av = me.avatar_url || me.google_picture;
|
||||||
|
if($('#pgz-sb-un')) $('#pgz-sb-un').textContent = name;
|
||||||
|
if($('#pgz-sb-ur')) $('#pgz-sb-ur').textContent = role;
|
||||||
|
const avEl = $('#pgz-sb-av');
|
||||||
|
if(avEl){
|
||||||
|
if(av) avEl.innerHTML = `<img src="${esc(av)}" alt="">`;
|
||||||
|
else avEl.textContent = initials(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCollapsedFromStorage(){
|
||||||
|
let col = false;
|
||||||
|
try { col = localStorage.getItem(STATE_KEY) === '1'; } catch(e){}
|
||||||
|
const sb = document.getElementById('pgz-sb');
|
||||||
|
if(!sb) return;
|
||||||
|
sb.classList.toggle('pgz-collapsed', col);
|
||||||
|
document.body.classList.toggle('pgz-sb-col', col);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────── Public API ──────────
|
||||||
|
const PGZSidebar = {
|
||||||
|
NAV_EXTERNAL,
|
||||||
|
|
||||||
|
// Render: insert sidebar shell at document start; if a page provides internalNavHTML, use it
|
||||||
|
mount(opts){
|
||||||
|
opts = opts || {};
|
||||||
|
const activeKey = opts.activeKey || (document.currentScript && document.currentScript.dataset.active) || '';
|
||||||
|
const internalNavHTML = opts.internalNavHTML || '';
|
||||||
|
// Skip mount if the page already has its own sidebar AND a portal mount point is provided
|
||||||
|
if(opts.skipShell){
|
||||||
|
const mount = document.getElementById('pgz-portal-mount');
|
||||||
|
if(mount){ mount.innerHTML = renderExternal(activeKey); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const existing = document.getElementById('pgz-sb');
|
||||||
|
if(existing) existing.remove();
|
||||||
|
const sb = renderShell(activeKey, internalNavHTML);
|
||||||
|
document.body.insertBefore(sb, document.body.firstChild);
|
||||||
|
document.body.classList.add('pgz-has-sb');
|
||||||
|
renderBurger();
|
||||||
|
applyCollapsedFromStorage();
|
||||||
|
tryLoadMe().then(setUserDisplay);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Append portal links to an existing custom sidebar (call this from a page's own buildNav)
|
||||||
|
appendPortalLinksTo(navEl, activeKey){
|
||||||
|
if(!navEl) return;
|
||||||
|
activeKey = activeKey || '';
|
||||||
|
navEl.insertAdjacentHTML('beforeend',
|
||||||
|
'<div class="pgz-sb-sep" style="padding:14px 14px 4px 14px;font-size:9.5px;color:var(--t4,#4e5a7a);text-transform:uppercase;letter-spacing:1.2px;font-weight:700">Portali</div>'
|
||||||
|
);
|
||||||
|
navEl.insertAdjacentHTML('beforeend', renderExternal(activeKey));
|
||||||
|
},
|
||||||
|
|
||||||
|
toggle(){
|
||||||
|
const sb = document.getElementById('pgz-sb');
|
||||||
|
if(!sb) return;
|
||||||
|
const col = sb.classList.toggle('pgz-collapsed');
|
||||||
|
document.body.classList.toggle('pgz-sb-col', col);
|
||||||
|
try { localStorage.setItem(STATE_KEY, col ? '1' : '0'); } catch(e){}
|
||||||
|
},
|
||||||
|
openMobile(){
|
||||||
|
const sb = document.getElementById('pgz-sb');
|
||||||
|
if(!sb) return;
|
||||||
|
sb.classList.add('pgz-mobile-open');
|
||||||
|
document.body.classList.add('pgz-mobile-sb-open');
|
||||||
|
// close on backdrop click
|
||||||
|
const closer = (ev) => {
|
||||||
|
if(!sb.contains(ev.target) && ev.target.id !== 'pgz-sb-burger'){
|
||||||
|
PGZSidebar.closeMobile();
|
||||||
|
document.removeEventListener('click', closer, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setTimeout(() => document.addEventListener('click', closer, true), 50);
|
||||||
|
},
|
||||||
|
closeMobile(){
|
||||||
|
const sb = document.getElementById('pgz-sb');
|
||||||
|
if(!sb) return;
|
||||||
|
sb.classList.remove('pgz-mobile-open');
|
||||||
|
document.body.classList.remove('pgz-mobile-sb-open');
|
||||||
|
},
|
||||||
|
logout
|
||||||
|
};
|
||||||
|
window.PGZSidebar = PGZSidebar;
|
||||||
|
|
||||||
|
// Auto-mount unless data-inline=1
|
||||||
|
function autoMount(){
|
||||||
|
const cs = document.currentScript || Array.from(document.scripts).find(s => /sidebar\.js/.test(s.src||''));
|
||||||
|
const inline = cs && cs.dataset && cs.dataset.inline === '1';
|
||||||
|
if(inline) return; // page will call PGZSidebar.mount() itself
|
||||||
|
if(document.readyState === 'loading'){
|
||||||
|
document.addEventListener('DOMContentLoaded', () => PGZSidebar.mount({}));
|
||||||
|
} else {
|
||||||
|
PGZSidebar.mount({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
autoMount();
|
||||||
|
})();
|
||||||
@@ -60,6 +60,8 @@ button,input,select{font-family:inherit;font-size:inherit;outline:none}
|
|||||||
.sb.collapsed .nav-i:hover::after{content:attr(data-label);position:absolute;left:58px;top:50%;transform:translateY(-50%);background:var(--bg3);color:var(--t0);padding:5px 10px;border-radius:4px;font-size:11.5px;white-space:nowrap;border:1px solid var(--rim);z-index:50;font-weight:600;pointer-events:none;box-shadow:2px 2px 8px rgba(0,0,0,.4)}
|
.sb.collapsed .nav-i:hover::after{content:attr(data-label);position:absolute;left:58px;top:50%;transform:translateY(-50%);background:var(--bg3);color:var(--t0);padding:5px 10px;border-radius:4px;font-size:11.5px;white-space:nowrap;border:1px solid var(--rim);z-index:50;font-weight:600;pointer-events:none;box-shadow:2px 2px 8px rgba(0,0,0,.4)}
|
||||||
.sb.collapsed .sb-foot{font-size:0;padding:8px}
|
.sb.collapsed .sb-foot{font-size:0;padding:8px}
|
||||||
.sb.collapsed .sb-foot::before{content:"v2";font-size:9px;color:var(--t4)}
|
.sb.collapsed .sb-foot::before{content:"v2";font-size:9px;color:var(--t4)}
|
||||||
|
.sb.collapsed .nav-sep{font-size:0;padding:6px 0;text-align:center;border-top:1px dashed var(--rim);margin:6px 8px 4px}
|
||||||
|
.sb.collapsed .nav-ext span:last-child{display:none}
|
||||||
|
|
||||||
.main{margin-left:240px;flex:1;min-width:0;transition:margin-left .22s ease}
|
.main{margin-left:240px;flex:1;min-width:0;transition:margin-left .22s ease}
|
||||||
.sb.collapsed ~ .main{margin-left:58px}
|
.sb.collapsed ~ .main{margin-left:58px}
|
||||||
@@ -570,9 +572,21 @@ function closePanel(){
|
|||||||
document.addEventListener('keydown', e => { if(e.key==='Escape') closePanel(); });
|
document.addEventListener('keydown', e => { if(e.key==='Escape') closePanel(); });
|
||||||
|
|
||||||
//=========== NAVIGATION ===========
|
//=========== NAVIGATION ===========
|
||||||
|
const NAV_EXTERNAL = [
|
||||||
|
{id:'login', href:'/sport/login', ic:'\u{1F511}', label:'Prijava'},
|
||||||
|
{id:'app', href:'/sport/app', ic:'\u{1F4F1}', label:'Aplikacija'},
|
||||||
|
{id:'admin', href:'/sport/admin', ic:'\u{1F6E1}', label:'Administracija'},
|
||||||
|
{id:'crm', href:'/sport/crm', ic:'\u{1F465}', label:'CRM'},
|
||||||
|
{id:'erp', href:'/sport/erp', ic:'\u{1F4B0}', label:'ERP'},
|
||||||
|
{id:'kpi', href:'/sport/kpi', ic:'\u{1F4C8}', label:'KPI'},
|
||||||
|
{id:'audit', href:'/sport/audit', ic:'\u{1F4CB}', label:'Audit'}
|
||||||
|
];
|
||||||
function buildNav(){
|
function buildNav(){
|
||||||
const nav = $('#nav');
|
const nav = $('#nav');
|
||||||
nav.innerHTML = NAV_ITEMS.map(n => '<div class="nav-i '+(n.id===_state.section?'active':'')+'" data-id="'+n.id+'" data-label="'+n.label+'" onclick="navTo(\''+n.id+'\')"><span class="ic">'+n.ic+'</span><span class="lbl">'+n.label+'</span></div>').join('');
|
nav.innerHTML = NAV_ITEMS.map(n => '<div class="nav-i '+(n.id===_state.section?'active':'')+'" data-id="'+n.id+'" data-label="'+n.label+'" onclick="navTo(\''+n.id+'\')"><span class="ic">'+n.ic+'</span><span class="lbl">'+n.label+'</span></div>').join('');
|
||||||
|
// PORTALI section + external links
|
||||||
|
nav.innerHTML += '<div class="nav-sep" style="padding:14px 14px 4px 14px;font-size:9.5px;color:var(--t4);text-transform:uppercase;letter-spacing:1.2px;font-weight:700;white-space:nowrap;overflow:hidden">Portali</div>';
|
||||||
|
nav.innerHTML += NAV_EXTERNAL.map(n => '<a class="nav-i nav-ext" href="'+n.href+'" data-id="'+n.id+'" data-label="'+esc(n.label)+'" style="color:var(--cyan);text-decoration:none"><span class="ic">'+n.ic+'</span><span class="lbl">'+esc(n.label)+'</span><span style="margin-left:auto;font-size:10px;opacity:.5">↗</span></a>').join('');
|
||||||
}
|
}
|
||||||
function toggleSidebar(){
|
function toggleSidebar(){
|
||||||
const sb = document.getElementById('sb');
|
const sb = document.getElementById('sb');
|
||||||
|
|||||||
Reference in New Issue
Block a user