Files
GoNtech/web/templates/teme/podrazumevana/base.html
T

437 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{define "base"}}
<!doctype html>
<html lang="sr">
<head>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{block "naslov" .}}NTech{{end}}</title>
<meta name="csrf-token" content="{{.CsrfToken}}">
<script>
if (window.innerWidth > 768 && localStorage.getItem('sidebar-skupljen') === 'true') {
document.documentElement.classList.add('sidebar-init-skupljen');
}
// sakriva poruke-uspeha odmah da ne trepnu pre toast konverzije
document.documentElement.classList.add('js-on');
</script>
<!-- tema — učitava se prva -->
<link rel="stylesheet" href="/static/css/teme/{{.Tema}}.css?v={{.AssetV}}" />
<!-- glavni stilovi -->
<link rel="stylesheet" href="/static/css/main.css?v={{.AssetV}}" />
{{block "dodatni-css" .}}{{end}}
{{if .AppPozadina}}<link rel="preload" as="image" href="{{.AppPozadina}}">{{end}}
{{if .AppPozadina}}
<style>
:root {
--app-blur-bg: {{.AppPozadinaBlurPozadine}}px;
--app-blur-bg-inset: {{if ne .AppPozadinaBlurPozadine "0"}}-20px{{else}}0{{end}};
--app-overlay: {{.AppPozadinaOpacity}}%;
--app-blur: {{.AppPozadinaBlur}}px;
--app-glass-sb: {{if .AppPozadinaGlassOpacity}}{{.AppPozadinaGlassOpacity}}%{{else}}30%{{end}};
--app-glass-el: {{if .AppPozadinaGlassOpacity}}{{.AppPozadinaGlassOpacity}}%{{else}}8%{{end}};
}
html {
background: url('{{.AppPozadina}}') center/cover fixed;
background-color: #1f2228;
}
body { background: transparent; }
body::before {
content: '';
position: fixed;
inset: var(--app-blur-bg-inset);
background: url('{{.AppPozadina}}') center/cover;
filter: blur(var(--app-blur-bg));
z-index: 0;
pointer-events: none;
}
body::after {
content: '';
position: fixed;
inset: 0;
background: rgba(0,0,0,var(--app-overlay));
z-index: 1;
pointer-events: none;
}
.raspored { position: relative; z-index: 2; }
.sidebar { background: rgba(0,0,0,var(--app-glass-sb)) !important; backdrop-filter: blur(var(--app-blur)); -webkit-backdrop-filter: blur(var(--app-blur)); border-right: 1px solid rgba(255,255,255,0.12) !important; }
.sidebar .nav-stavka, .sidebar .logo-naziv, .sidebar .logo-podnazlov { text-shadow: 0 1px 3px rgba(0,0,0,0.8); color: rgba(255,255,255,0.95) !important; }
.sidebar .nav-stavka svg { color: rgba(255,255,255,0.95) !important; stroke: rgba(255,255,255,0.95) !important; }
.sidebar .nav-oznaka { text-shadow: 0 1px 3px rgba(0,0,0,0.8); color: rgba(255,255,255,0.7) !important; }
.topbar { background: rgba(0,0,0,var(--app-glass-el)) !important; backdrop-filter: blur(var(--app-blur)); -webkit-backdrop-filter: blur(var(--app-blur)); border-bottom: 1px solid rgba(255,255,255,0.12) !important; }
.topbar-naslov { color: rgba(255,255,255,0.95) !important; text-shadow: 0 1px 2px rgba(0,0,0,0.9), 0 0 6px rgba(0,0,0,0.55); }
.kartica, .premesti-modal { background: rgba(0,0,0,var(--app-glass-el)) !important; backdrop-filter: blur(var(--app-blur)); -webkit-backdrop-filter: blur(var(--app-blur)); border: 1px solid rgba(255,255,255,0.12) !important; }
body:not([data-hover]) .kartica:hover { background: rgba(0,0,0,0.38) !important; }
body:not([data-hover]) .kartica:hover .dash-ikona { filter: brightness(0.55); }
.kartica p, .kartica span, .kartica h1, .kartica h2, .kartica h3, .kartica h4, .kartica label, .kartica td, .kartica th, .kartica li, .kartica a { color: rgba(255,255,255,0.95) !important; text-shadow: 0 1px 2px rgba(0,0,0,0.9), 0 0 6px rgba(0,0,0,0.55); }
/* naslovi kartica i labele su goli <div> (boju im NE diramo, da namerno obojeni — crveni/akcenat — ostanu) — samo senka da se vide */
.kartica div { text-shadow: 0 1px 2px rgba(0,0,0,0.9), 0 0 6px rgba(0,0,0,0.55); }
/* modal nasleđuje glass izgled od kartica: providno staklo, beli tekst, lakše zatamnjenje da se slika nazire */
.premesti-modal h3, .premesti-modal .premesti-opcija, .premesti-modal .premesti-zatvori { color: rgba(255,255,255,0.95) !important; text-shadow: 0 1px 2px rgba(0,0,0,0.9), 0 0 6px rgba(0,0,0,0.55); }
.premesti-modal[open]::backdrop { background: rgba(0,0,0,0.45) !important; }
table, th, td { color: rgba(255,255,255,0.95) !important; text-shadow: 0 1px 2px rgba(0,0,0,0.9), 0 0 6px rgba(0,0,0,0.55); }
tr { background: rgba(0,0,0,0.2); }
tr:hover { background: rgba(0,0,0,0.35); }
thead th { background: rgba(0,0,0,0.4) !important; }
div:has(> canvas) { background: rgba(0,0,0,0.3); border-radius: 8px; padding: 8px; }
/* elementi van kartica (i preskočeni tagovi) koje glass override gore ne dohvata —
dobijaju sopstvenu staklenu podlogu da se vide bez obzira na sliku ispod */
.nazad-link, .btn-sekundarno, .btn-obrisi-ghost, .cek-filter {
background: rgba(0,0,0,0.28) !important;
backdrop-filter: blur(var(--app-blur)); -webkit-backdrop-filter: blur(var(--app-blur));
border: 1px solid rgba(255,255,255,0.18) !important;
color: rgba(255,255,255,0.95) !important; text-shadow: 0 1px 3px rgba(0,0,0,0.8);
}
/* .nazad-link i checkbox-traka nemaju sopstveni padding — dajemo „pill" oblik */
.nazad-link, .cek-filter { padding: 6px 12px; border-radius: 8px; }
.nazad-link svg, .btn-sekundarno svg, .cek-filter svg { color: rgba(255,255,255,0.95) !important; stroke: rgba(255,255,255,0.95) !important; }
/* sitne napomene (npr. „maksimum 5 MB") i naslovi van kartica — svetli tekst + halo, bez podloge */
.pomocni-tekst { color: rgba(255,255,255,0.9) !important; text-shadow: 0 1px 3px rgba(0,0,0,0.8); }
/* jača dvoslojna senka: oštar tamni obris + širi mekan halo, da naslov „iskoči" i na svetloj slici */
/* tri sloja u opadajućem zatamnjenju: skoro crn obris → upola → bled široki halo */
.naslov-traka { color: rgba(255,255,255,0.95) !important; text-shadow: 0 1px 2px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.5), 0 2px 16px rgba(0,0,0,0.25); }
/* info-banner u kartici: tekst je goli <div> koji glass override ne dohvata — ciljano mu damo svetli tekst + senku */
.info-banner div { color: rgba(255,255,255,0.95) !important; text-shadow: 0 1px 2px rgba(0,0,0,0.9), 0 0 6px rgba(0,0,0,0.55); }
/* Mobilni: backdrop-filter blur preko fiksne pozadinske slike često NE radi na
telefonima (iOS/Android), pa glass topbar propušta svetlu sliku i beli naslov se
gubi. Zato ne računamo na blur — dajemo čvrstu poluprovidnu podlogu i jaču senku. */
@media (max-width: 768px) {
.topbar { background: rgba(0,0,0,0.5) !important; }
.topbar-naslov { text-shadow: 0 1px 3px rgba(0,0,0,1), 0 0 8px rgba(0,0,0,0.85); }
.naslov-traka { text-shadow: 0 1px 3px rgba(0,0,0,1), 0 0 8px rgba(0,0,0,0.9), 0 2px 16px rgba(0,0,0,0.4); }
.pomocni-tekst { text-shadow: 0 1px 3px rgba(0,0,0,1), 0 0 6px rgba(0,0,0,0.85); }
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
var meni = document.getElementById('avatar-meni');
if (!meni) return;
meni.style.backdropFilter = 'none';
meni.style.webkitBackdropFilter = 'none';
meni.style.background = 'var(--kartica)';
});
</script>
{{end}}
</head>
<body{{if .LokalnaAnimacija}} data-animacija="{{.LokalnaAnimacija}}"{{end}}{{if .LokalniHover}} data-hover="{{.LokalniHover}}"{{end}}{{if .LokalnaBrzinaAnimacije}} data-brzina-animacije="{{.LokalnaBrzinaAnimacije}}"{{end}}>
<div class="raspored">
<div class="sidebar-overlay" id="sidebar-overlay"></div>
{{template "sidebar" .}}
<div class="glavni-sadrzaj" id="glavni-sadrzaj">
{{template "topbar" .}}
<main class="sadrzaj" id="sadrzaj">
{{if .Flash}}
<div id="flash-toast" class="flash-toast flash-{{.Flash.Tip}}" role="alert">
<span class="flash-ikona">{{if eq .Flash.Tip "uspeh"}}✓{{else}}!{{end}}</span>
<span class="flash-tekst">{{.Flash.Poruka}}</span>
<button class="flash-zatvori" onclick="this.parentElement.remove()" aria-label="Zatvori">×</button>
</div>
<script>
(function() {
var t = document.getElementById('flash-toast');
if (!t) return;
setTimeout(function() {
t.classList.add('flash-izlaz');
setTimeout(function() { if (t.parentElement) t.remove(); }, 350);
}, 4000);
})();
</script>
{{end}}
{{block "sadrzaj" .}}{{end}}
</main>
</div>
</div>
<!-- alpine.js komponente (mora biti pre Alpine-a) -->
<script src="/static/js/ntech.js?v={{.AssetV}}" defer></script>
<!-- alpine.js CSP build (lokalno, bez unsafe-eval) -->
<script src="/static/js/alpine.csp.min.js" defer></script>
<!-- htmx za komunikaciju sa serverom (lokalno) -->
<script src="/static/js/htmx.min.js"></script>
<!-- sidebar logika — sidebar se NIKAD ne menja pri HTMX navigaciji, pa nema potrebe za swap logikom -->
<script>
(function() {
if (window._ntechSidebarDodato) return;
window._ntechSidebarDodato = true;
function mobilni() { return window.innerWidth <= 768; }
var sidebar = document.getElementById("sidebar");
var hamburger = document.getElementById("hamburger");
var overlay = document.getElementById("sidebar-overlay");
// skupljeno stanje pri učitavanju stranice
if (sidebar && !mobilni() && localStorage.getItem("sidebar-skupljen") === "true") {
sidebar.classList.add("skupljen");
}
document.documentElement.classList.remove('sidebar-init-skupljen');
if (typeof ntechInicijalizujPodmeni === 'function') ntechInicijalizujPodmeni();
// hamburger — sidebar element živi ceo vek stranice, listener se dodaje jednom
if (hamburger && sidebar && overlay) {
hamburger.addEventListener("click", function() {
if (mobilni()) {
sidebar.classList.toggle("otvoren");
overlay.classList.toggle("aktivan");
} else {
sidebar.classList.toggle("skupljen");
localStorage.setItem("sidebar-skupljen", sidebar.classList.contains("skupljen"));
}
});
overlay.addEventListener("click", function() {
sidebar.classList.remove("otvoren");
overlay.classList.remove("aktivan");
});
}
window.addEventListener("resize", function() {
if (!mobilni() && sidebar && overlay) {
sidebar.classList.remove("otvoren");
overlay.classList.remove("aktivan");
}
});
// aktivna stavka — ažurira se pri svakoj HTMX navigaciji
function azurirajAktivne() {
var putanja = window.location.pathname;
document.querySelectorAll('#sidebar .nav-stavka').forEach(function(el) {
el.classList.remove('aktivan');
});
document.querySelectorAll('#sidebar .nav-podmeni').forEach(function(el) {
el.classList.remove('otvoren');
var strelica = el.previousElementSibling && el.previousElementSibling.querySelector('.nav-strelica svg');
if (strelica) strelica.style.transform = 'rotate(0deg)';
});
document.querySelectorAll('#sidebar a.nav-stavka').forEach(function(el) {
var href = el.getAttribute('href');
if (!href) return;
if (putanja === href || putanja.startsWith(href + '/')) {
el.classList.add('aktivan');
var podmeni = el.closest('.nav-podmeni');
if (podmeni) {
podmeni.classList.add('otvoren');
var roditelj = podmeni.previousElementSibling;
if (roditelj) {
roditelj.classList.add('aktivan');
var str = roditelj.querySelector('.nav-strelica svg');
if (str) str.style.transform = 'rotate(180deg)';
}
}
}
});
}
document.addEventListener('htmx:afterSwap', azurirajAktivne);
})();
</script>
{{block "dodatni-js" .}}{{end}}
<!-- modal za potvrdu akcije -->
<div id="potvrda-modal" style="display:none;position:fixed;inset:0;z-index:9999;align-items:center;justify-content:center;">
<div id="potvrda-pozadina" style="position:absolute;inset:0;background:rgba(0,0,0,0.55);backdrop-filter:blur(3px);"></div>
<div style="position:relative;background:var(--kartica-pozadina);border:0.5px solid var(--ivica);border-radius:12px;padding:28px 28px 22px;max-width:380px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,0.35);">
<div style="font-size:15px;font-weight:500;color:var(--tekst-glavni);margin-bottom:20px;line-height:1.5;" id="potvrda-poruka"></div>
<div style="display:flex;gap:10px;justify-content:flex-end;">
<button id="potvrda-odustani" class="btn-sekundarno">Odustani</button>
<button id="potvrda-potvrdi" class="btn-opasno">Potvrdi</button>
</div>
</div>
</div>
<!-- CSRF i potvrda: inicijalizacija na učitavanju i posle htmx swap-a -->
<script>
(function() {
var modal = document.getElementById('potvrda-modal');
var poruka = document.getElementById('potvrda-poruka');
var btnPotvrdi = document.getElementById('potvrda-potvrdi');
var btnOdustani = document.getElementById('potvrda-odustani');
var pozadina = document.getElementById('potvrda-pozadina');
var _resolveFn = null;
function prikaziModal(tekst) {
return new Promise(function(resolve) {
poruka.textContent = tekst;
modal.style.display = 'flex';
_resolveFn = resolve;
});
}
function zatvoriModal(rezultat) {
modal.style.display = 'none';
if (_resolveFn) { _resolveFn(rezultat); _resolveFn = null; }
}
btnPotvrdi.addEventListener('click', function() { zatvoriModal(true); });
btnOdustani.addEventListener('click', function() { zatvoriModal(false); });
pozadina.addEventListener('click', function() { zatvoriModal(false); });
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && modal.style.display === 'flex') zatvoriModal(false);
});
window._ntechPotvrdi = prikaziModal;
})();
// toast obaveštenja — prikazuje poruku u uglu ekrana, nestaje posle 4s
window.ntechToast = function(tekst, tip) {
var t = document.createElement('div');
t.className = 'toast ' + (tip === 'greska' ? 'toast-greska' : 'toast-uspeh');
// ikonica
var svg = tip === 'greska'
? '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>'
: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>';
t.innerHTML = svg + '<span>' + tekst + '</span>';
// prilagođavamo poziciju za mobilne uređaje
t.style.bottom = '24px';
t.style.right = '16px';
document.body.appendChild(t);
var ukloni = function() {
t.classList.add('nestaje');
setTimeout(function() { if (t.parentNode) t.parentNode.removeChild(t); }, 320);
};
t.addEventListener('click', ukloni);
setTimeout(ukloni, 4000);
};
// konvertuje sve .poruka-uspeh u toast i sklanja original
function ntechKonvertujPoruke() {
document.querySelectorAll('.poruka-uspeh').forEach(function(el) {
if (el.dataset.toastPrikazan) return;
el.dataset.toastPrikazan = '1';
var tekst = el.textContent.trim();
el.style.display = 'none';
if (tekst) window.ntechToast(tekst, 'uspeh');
});
}
function ntechInicijalizuj() {
ntechKonvertujPoruke();
var m = document.querySelector('meta[name="csrf-token"]');
if (m && m.content) {
document.querySelectorAll('form[method="POST"],form[method="post"]').forEach(function(f) {
if (f.querySelector('input[name="_csrf"]')) return;
var i = document.createElement('input');
i.type = 'hidden'; i.name = '_csrf'; i.value = m.content;
f.appendChild(i);
});
}
// toast pri učitavanju stranice ako URL sadrži ?sacuvano=1 (cross-page redirect)
if (!window._ntechSacuvanoPrikazano && location.search.indexOf('sacuvano=1') !== -1) {
window._ntechSacuvanoPrikazano = true;
window.ntechToast('Sačuvano', 'uspeh');
var _p = new URLSearchParams(location.search);
_p.delete('sacuvano');
var _q = _p.toString() ? '?' + _p.toString() : '';
history.replaceState(null, '', location.pathname + _q + location.hash);
}
document.querySelectorAll('[data-potvrda]').forEach(function(el) {
if (el._potvrda) return;
el._potvrda = true;
el.addEventListener('click', function(e) {
e.preventDefault();
window._ntechPotvrdi(el.getAttribute('data-potvrda')).then(function(ok) {
if (!ok) return;
var forma = el.closest('form');
if (forma) { forma.submit(); return; }
if (el.href) { window.location.href = el.href; }
});
});
});
// AJAX submit — forme koje serveru vraćaju ?sacuvano= ostaju na stranici (scroll se ne gubi)
document.querySelectorAll('form[method="POST"],form[method="post"]').forEach(function(f) {
if (f._ajaxSave) return;
f._ajaxSave = true;
f.addEventListener('submit', function(e) {
// file upload i forme sa data-full-reload šalju se normalno
if (f.enctype === 'multipart/form-data' || f.hasAttribute('data-full-reload')) return;
e.preventDefault();
// prikazujemo dugme kao zauzeto
var btn = f.querySelector('[type="submit"]');
if (btn) btn.disabled = true;
// URLSearchParams šalje kao application/x-www-form-urlencoded
// što Go-ov r.FormValue() (i CSRF middleware) može da pročita
fetch(f.action || location.href, {
method: 'POST',
body: new URLSearchParams(new FormData(f)),
redirect: 'follow'
}).then(function(res) {
var finUrl = new URL(res.url);
var isStiPath = finUrl.pathname === location.pathname;
var imaSacuvano = finUrl.search.indexOf('sacuvano') !== -1;
if (isStiPath && imaSacuvano) {
// uspeh na istoj stranici — prikaži toast, ostani
window.ntechToast('Sačuvano', 'uspeh');
if (btn) btn.disabled = false;
// odmah primeni podešavanja koja menjaju globalne atribute body-ja
var anim = f.querySelector('[name="lokalna_animacija"]');
if (anim) {
if (anim.value) document.body.dataset.animacija = anim.value;
else delete document.body.dataset.animacija;
}
var hov = f.querySelector('[name="lokalni_hover"]');
if (hov) {
if (hov.value) document.body.dataset.hover = hov.value;
else delete document.body.dataset.hover;
}
var brzina = f.querySelector('[name="lokalna_brzina_animacije"]');
if (brzina) {
if (brzina.value) document.body.dataset.brzinaAnimacije = brzina.value;
else delete document.body.dataset.brzinaAnimacije;
}
// toggle „Prikaži logo u gornjoj traci" — pokaži/sakrij logo u topbaru bez reloda
var logoToggle = f.querySelector('[name="topbar_logo_slika"]');
if (logoToggle) {
var logoImg = document.querySelector('.topbar-logo');
if (logoImg) logoImg.classList.toggle('skriven', !logoToggle.checked);
}
// posle čuvanja stilova lične pozadine ažuriraj CSS custom properties
var bgBlur = f.querySelector('[name="lokalna_pozadina_blur"]');
if (bgBlur) {
var r = document.documentElement;
var bgBlurBg = parseInt(f.querySelector('[name="lokalna_pozadina_blur_pozadine"]').value) || 0;
var bgOp = parseInt(f.querySelector('[name="lokalna_pozadina_opacity"]').value) || 0;
var bgGlass = parseInt(f.querySelector('[name="lokalna_pozadina_glass_opacity"]').value) || 0;
var bgBlurV = parseInt(bgBlur.value) || 0;
r.style.setProperty('--app-blur-bg', bgBlurBg + 'px');
r.style.setProperty('--app-blur-bg-inset', bgBlurBg > 0 ? '-20px' : '0');
r.style.setProperty('--app-overlay', bgOp + '%');
r.style.setProperty('--app-blur', bgBlurV + 'px');
r.style.setProperty('--app-glass-sb', bgGlass + '%');
r.style.setProperty('--app-glass-el', bgGlass + '%');
}
// promena teme zahteva reload (menja se ceo CSS fajl)
if (f.querySelector('[name="lokalna_tema"]')) location.reload();
} else {
// redirect na drugu stranicu ili bez sacuvano — navigiraj normalno
location.href = res.url;
}
}).catch(function() {
// mrežna greška — pošalji formu normalno
f.submit();
});
});
});
}
if (!window._ntechCsrfDodato) {
window._ntechCsrfDodato = true;
document.addEventListener('htmx:afterSettle', ntechInicijalizuj);
}
document.addEventListener('DOMContentLoaded', ntechInicijalizuj);
</script>
</body>
</html>
{{end}}