Podešavanja: pozadinska slika i glass efekt za aplikaciju sa live preview sliderom

This commit is contained in:
2026-06-05 23:49:05 +02:00
parent 9af712edd3
commit 8def13e855
14 changed files with 685 additions and 15 deletions
+5
View File
@@ -142,6 +142,11 @@ func main() {
r.Get("/podesavanja", h.Podesavanja) r.Get("/podesavanja", h.Podesavanja)
r.Post("/podesavanja/sacuvaj", h.SacuvajPodesavanja) r.Post("/podesavanja/sacuvaj", h.SacuvajPodesavanja)
r.Post("/podesavanja/logo", h.OtpremiLogo) r.Post("/podesavanja/logo", h.OtpremiLogo)
r.Post("/podesavanja/login-pozadina", h.OtpremiLoginPozadinu)
r.Post("/podesavanja/login-pozadina/ukloni", h.UkloniLoginPozadinu)
r.Post("/podesavanja/app-pozadina", h.OtpremiAppPozadinu)
r.Post("/podesavanja/app-pozadina/ukloni", h.UkloniAppPozadinu)
r.Post("/podesavanja/app-pozadina/stilovi", h.SacuvajAppPozadinaStilove)
r.Get("/podesavanja/backup", h.BackupBaze) r.Get("/podesavanja/backup", h.BackupBaze)
r.Post("/podesavanja/backup/vrati", h.VratiBackup) r.Post("/podesavanja/backup/vrati", h.VratiBackup)
r.Get("/tema/{tema}", h.PromeniTemu) r.Get("/tema/{tema}", h.PromeniTemu)
+11
View File
@@ -90,5 +90,16 @@ func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]s
} }
ps.CsrfToken = middleware.CsrfToken(r.Context()) ps.CsrfToken = middleware.CsrfToken(r.Context())
ps.Flash = middleware.GetFlash(r, h.DB) ps.Flash = middleware.GetFlash(r, h.DB)
ps.AppPozadina = podesavanja["app_pozadina"]
ps.AppPozadinaOpacity = podesavanja["app_pozadina_opacity"]
if ps.AppPozadinaOpacity == "" {
ps.AppPozadinaOpacity = "50"
}
ps.AppPozadinaBlur = podesavanja["app_pozadina_blur"]
if ps.AppPozadinaBlur == "" {
ps.AppPozadinaBlur = "12"
}
return ps return ps
} }
+349 -13
View File
@@ -1,6 +1,8 @@
package handler package handler
import ( import (
"crypto/rand"
"encoding/hex"
"fmt" "fmt"
"io" "io"
"log" "log"
@@ -9,6 +11,7 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"sort" "sort"
"strconv"
"strings" "strings"
"time" "time"
@@ -22,19 +25,23 @@ import (
// PodaciPodesavanja su podaci za stranicu podešavanja // PodaciPodesavanja su podaci za stranicu podešavanja
type PodaciPodesavanja struct { type PodaciPodesavanja struct {
model.PodaciStranice model.PodaciStranice
NazivFirme string NazivFirme string
Podnazlov string Podnazlov string
Adresa string Adresa string
Telefon string Telefon string
PIB string PIB string
LogoTip string LogoTip string
LogoPutanja string LogoPutanja string
Tema string Tema string
Sacuvano bool Sacuvano bool
Verzija string Verzija string
LogoGreska string LogoGreska string
BackupVracen bool BackupVracen bool
Backupi []BackupInfo Backupi []BackupInfo
LoginPozadina string
AppPozadina string
AppPozadinaOpacity string
AppPozadinaBlur string
} }
// BackupInfo opisuje jedan backup fajl // BackupInfo opisuje jedan backup fajl
@@ -78,6 +85,18 @@ func (h *Handler) Podesavanja(w http.ResponseWriter, r *http.Request) {
Verzija: h.Verzija, Verzija: h.Verzija,
LogoGreska: r.URL.Query().Get("logo_greska"), LogoGreska: r.URL.Query().Get("logo_greska"),
Backupi: ucitajListuBackupa(), Backupi: ucitajListuBackupa(),
LoginPozadina: podesavanja["login_pozadina"],
AppPozadina: podesavanja["app_pozadina"],
AppPozadinaOpacity: func() string {
v := podesavanja["app_pozadina_opacity"]
if v == "" { return "50" }
return v
}(),
AppPozadinaBlur: func() string {
v := podesavanja["app_pozadina_blur"]
if v == "" { return "12" }
return v
}(),
} }
h.renderujTemplate(w, "podesavanja", podaci) h.renderujTemplate(w, "podesavanja", podaci)
@@ -341,6 +360,323 @@ func (h *Handler) OtpremiLogo(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/podesavanja?sacuvano=1", http.StatusSeeOther) http.Redirect(w, r, "/podesavanja?sacuvano=1", http.StatusSeeOther)
} }
// generisiImeUploada vraća slučajno hex ime (16 bajtova) sa datom ekstenzijom
func generisiImeUploada(ext string) (string, error) {
buf := make([]byte, 16)
if _, err := rand.Read(buf); err != nil {
return "", err
}
return hex.EncodeToString(buf) + ext, nil
}
// OtpremiLoginPozadinu prima multipart upload slike i čuva je kao pozadinsku sliku login stranice
func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.login_pozadina") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 5<<20+4096)
if err := r.ParseMultipartForm(5 << 20); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Fajl je prevelik (maksimum 5 MB).")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
fajl, zaglavlje, err := r.FormFile("login_pozadina")
if err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Nije odabran fajl.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
defer fajl.Close()
if zaglavlje.Size > 5<<20 {
middleware.SetFlash(w, r, h.DB, "greska", "Fajl je prevelik (maksimum 5 MB).")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
ext := strings.ToLower(filepath.Ext(zaglavlje.Filename))
dozvoljenoExt := map[string]string{
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".webp": "image/webp",
}
ocekivaniMime, ok := dozvoljenoExt[ext]
if !ok {
middleware.SetFlash(w, r, h.DB, "greska", "Dozvoljeni formati su JPG, PNG i WebP.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
// proveravamo stvarni tip sadržaja (magic bytes)
buf := make([]byte, 512)
n, _ := fajl.Read(buf)
stvarniMime := http.DetectContentType(buf[:n])
if !strings.HasPrefix(stvarniMime, ocekivaniMime) {
middleware.SetFlash(w, r, h.DB, "greska", "Sadržaj fajla ne odgovara odabranoj ekstenziji.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
if _, err := fajl.Seek(0, io.SeekStart); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri obradi fajla.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
// briše staru pozadinu sa diska ako postoji
staraPodesavanja, _ := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if stara := staraPodesavanja["login_pozadina"]; stara != "" {
// putanja u bazi je oblika /static/uploads/ime.ext?v=..., izvlačimo samo ime fajla
deoBezverzije := strings.Split(stara, "?")[0]
staroIme := filepath.Base(deoBezverzije)
os.Remove(filepath.Join("web/static/uploads", staroIme))
}
novoIme, err := generisiImeUploada(ext)
if err != nil {
log.Printf("upload login pozadine: greška pri generisanju imena: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
odrediste := filepath.Join("web/static/uploads", novoIme)
dst, err := os.Create(odrediste)
if err != nil {
log.Printf("upload login pozadine: ne mogu kreirati fajl: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
defer dst.Close()
if _, err := io.Copy(dst, fajl); err != nil {
log.Printf("upload login pozadine: greška pri kopiranju: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
putanja := fmt.Sprintf("/static/uploads/%s?v=%d", novoIme, time.Now().Unix())
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "login_pozadina", putanja); err != nil {
log.Printf("upload login pozadine: greška pri čuvanju putanje: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju podešavanja.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika je uspešno otpremljena.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
}
// UkloniLoginPozadinu briše pozadinsku sliku login stranice sa diska i iz podešavanja
func (h *Handler) UkloniLoginPozadinu(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.login_pozadina") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return
}
podesavanja, err := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if err == nil {
if stara := podesavanja["login_pozadina"]; stara != "" {
deoBezverzije := strings.Split(stara, "?")[0]
staroIme := filepath.Base(deoBezverzije)
os.Remove(filepath.Join("web/static/uploads", staroIme))
}
}
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "login_pozadina", ""); err != nil {
log.Printf("ukloni login pozadinu: greška pri čuvanju: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju slike.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika je uklonjena.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
}
// OtpremiAppPozadinu prima multipart upload slike i čuva je kao pozadinsku sliku aplikacije
func (h *Handler) OtpremiAppPozadinu(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.app_pozadina") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 5<<20+4096)
if err := r.ParseMultipartForm(5 << 20); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Fajl je prevelik (maksimum 5 MB).")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
fajl, zaglavlje, err := r.FormFile("app_pozadina")
if err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Nije odabran fajl.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
defer fajl.Close()
if zaglavlje.Size > 5<<20 {
middleware.SetFlash(w, r, h.DB, "greska", "Fajl je prevelik (maksimum 5 MB).")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
ext := strings.ToLower(filepath.Ext(zaglavlje.Filename))
dozvoljenoExt := map[string]string{
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".webp": "image/webp",
}
ocekivaniMime, ok := dozvoljenoExt[ext]
if !ok {
middleware.SetFlash(w, r, h.DB, "greska", "Dozvoljeni formati su JPG, PNG i WebP.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
buf := make([]byte, 512)
n, _ := fajl.Read(buf)
stvarniMime := http.DetectContentType(buf[:n])
if !strings.HasPrefix(stvarniMime, ocekivaniMime) {
middleware.SetFlash(w, r, h.DB, "greska", "Sadržaj fajla ne odgovara odabranoj ekstenziji.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
if _, err := fajl.Seek(0, io.SeekStart); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri obradi fajla.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
// briše staru app pozadinu sa diska ako postoji
staraPodesavanja, _ := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if stara := staraPodesavanja["app_pozadina"]; stara != "" {
deoBezverzije := strings.Split(stara, "?")[0]
staroIme := filepath.Base(deoBezverzije)
os.Remove(filepath.Join("web/static/uploads", staroIme))
}
novoIme, err := generisiImeUploada(ext)
if err != nil {
log.Printf("upload app pozadine: greška pri generisanju imena: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
odrediste := filepath.Join("web/static/uploads", novoIme)
dst, err := os.Create(odrediste)
if err != nil {
log.Printf("upload app pozadine: ne mogu kreirati fajl: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
defer dst.Close()
if _, err := io.Copy(dst, fajl); err != nil {
log.Printf("upload app pozadine: greška pri kopiranju: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
putanja := fmt.Sprintf("/static/uploads/%s?v=%d", novoIme, time.Now().Unix())
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "app_pozadina", putanja); err != nil {
log.Printf("upload app pozadine: greška pri čuvanju putanje: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju podešavanja.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika aplikacije je uspešno otpremljena.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
}
// UkloniAppPozadinu briše pozadinsku sliku aplikacije sa diska i iz podešavanja
func (h *Handler) UkloniAppPozadinu(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.app_pozadina") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return
}
podesavanja, err := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if err == nil {
if stara := podesavanja["app_pozadina"]; stara != "" {
deoBezverzije := strings.Split(stara, "?")[0]
staroIme := filepath.Base(deoBezverzije)
os.Remove(filepath.Join("web/static/uploads", staroIme))
}
}
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "app_pozadina", ""); err != nil {
log.Printf("ukloni app pozadinu: greška pri čuvanju: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju slike.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika aplikacije je uklonjena.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
}
// SacuvajAppPozadinaStilove čuva vrednosti zamućenja i prozirnosti pozadine aplikacije
func (h *Handler) SacuvajAppPozadinaStilove(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.app_pozadina") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return
}
if err := r.ParseForm(); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čitanju forme.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
blurStr := r.FormValue("blur")
opacityStr := r.FormValue("opacity")
blurVal, err := strconv.Atoi(blurStr)
if err != nil || blurVal < 0 || blurVal > 20 {
middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost zamućenja.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
opacityVal, err := strconv.Atoi(opacityStr)
if err != nil || opacityVal < 0 || opacityVal > 80 {
middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost prozirnosti.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "app_pozadina_blur", blurStr); err != nil {
log.Printf("stilovi app pozadine: greška pri čuvanju blur: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju podešavanja.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "app_pozadina_opacity", opacityStr); err != nil {
log.Printf("stilovi app pozadine: greška pri čuvanju opacity: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju podešavanja.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
return
}
middleware.SetFlash(w, r, h.DB, "uspeh", "Izgled pozadine aplikacije je sačuvan.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
}
// PromeniTemu menja aktivnu temu i vraća na prethodnu stranicu // PromeniTemu menja aktivnu temu i vraća na prethodnu stranicu
func (h *Handler) PromeniTemu(w http.ResponseWriter, r *http.Request) { func (h *Handler) PromeniTemu(w http.ResponseWriter, r *http.Request) {
tema := chi.URLParam(r, "tema") tema := chi.URLParam(r, "tema")
+11 -2
View File
@@ -9,6 +9,7 @@ import (
"time" "time"
"ntech/internal/auth" "ntech/internal/auth"
ntechsqlite "ntech/internal/db/sqlite"
"ntech/internal/middleware" "ntech/internal/middleware"
) )
@@ -29,9 +30,17 @@ func (h *Handler) PrikazPrijave(w http.ResponseWriter, r *http.Request) {
} }
greska := r.URL.Query().Get("greska") greska := r.URL.Query().Get("greska")
// login_pozadina se čita bez prijavljenog korisnika — koristimo background kontekst
loginPozadina := ""
if podesavanja, err := ntechsqlite.DohvatiSvaPodesavanja(context.Background(), h.DB); err == nil {
loginPozadina = podesavanja["login_pozadina"]
}
h.renderujStandalone(w, "prijava", map[string]any{ h.renderujStandalone(w, "prijava", map[string]any{
"Greska": greska, "Greska": greska,
"CsrfToken": middleware.CsrfToken(r.Context()), "CsrfToken": middleware.CsrfToken(r.Context()),
"LoginPozadina": loginPozadina,
}) })
} }
+5
View File
@@ -43,6 +43,8 @@ var sveAkcije = []string{
"korisnik.uloga", "korisnik.uloga",
"backup.pregled", "backup.pregled",
"backup.pokreni", "backup.pokreni",
"podesavanja.login_pozadina",
"podesavanja.app_pozadina",
} }
// SveAkcije vraća listu svih poznatih akcija — koristi se pri inicijalizaciji baze i resetu // SveAkcije vraća listu svih poznatih akcija — koristi se pri inicijalizaciji baze i resetu
@@ -100,6 +102,9 @@ func ImaDozvolu(uloga, akcija string) bool {
// backup // backup
case "backup.pregled", "backup.pokreni": case "backup.pregled", "backup.pokreni":
return true return true
// pozadinske slike
case "podesavanja.login_pozadina", "podesavanja.app_pozadina":
return true
} }
return false return false
+26
View File
@@ -88,3 +88,29 @@ func (n ServisniNalog) AvansStr() string {
} }
return fmt.Sprintf("%.2f", *n.Avans) return fmt.Sprintf("%.2f", *n.Avans)
} }
// PreostaloZaNaplatu vraća razliku konacna_cena avans, minimum 0.
// Vraća nil ako konačna cena nije uneta.
func (n ServisniNalog) PreostaloZaNaplatu() *float64 {
if n.CenaKonacna == nil {
return nil
}
avans := 0.0
if n.Avans != nil {
avans = *n.Avans
}
v := *n.CenaKonacna - avans
if v < 0 {
v = 0
}
return &v
}
// PreostaloZaNaplatuStr vraća formatirano preostalo za naplatu, ili prazan string
func (n ServisniNalog) PreostaloZaNaplatuStr() string {
v := n.PreostaloZaNaplatu()
if v == nil {
return ""
}
return fmt.Sprintf("%.2f", *v)
}
+4
View File
@@ -44,6 +44,10 @@ type PodaciStranice struct {
CsrfToken string // CSRF zaštitni token za forme CsrfToken string // CSRF zaštitni token za forme
Dozvole map[string]bool // mapa akcija → dozvoljeno/nije Dozvole map[string]bool // mapa akcija → dozvoljeno/nije
Flash *FlashPoruka // jednokratna poruka nakon redirecta Flash *FlashPoruka // jednokratna poruka nakon redirecta
// app pozadina — popunjava se iz podešavanja za sve stranice
AppPozadina string
AppPozadinaOpacity string // vrednost 0-80 (% overlay zatamnjivanja)
AppPozadinaBlur string // vrednost 0-20 (px backdrop-filter blur)
} }
// PodaciDashboarda su podaci specifični za dashboard stranicu // PodaciDashboarda su podaci specifični za dashboard stranicu
+1
View File
@@ -0,0 +1 @@
INSERT OR IGNORE INTO podesavanja (kljuc, vrednost) VALUES ('login_pozadina', '');
+3
View File
@@ -0,0 +1,3 @@
INSERT OR IGNORE INTO podesavanja (kljuc, vrednost) VALUES ('app_pozadina', '');
INSERT OR IGNORE INTO podesavanja (kljuc, vrednost) VALUES ('app_pozadina_opacity', '50');
INSERT OR IGNORE INTO podesavanja (kljuc, vrednost) VALUES ('app_pozadina_blur', '12');
+51
View File
@@ -0,0 +1,51 @@
<svg width="100%" viewBox="0 0 750 690" role="img" style="" xmlns="http://www.w3.org/2000/svg">
<title style="fill:rgb(0, 0, 0);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">NTech favicon v4</title>
<desc style="fill:rgb(0, 0, 0);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">Moderna ikonica sa NT slovima i tehnološkim akcentom</desc>
<defs>
<clipPath id="rounded-clip">
<rect width="680" height="680" rx="120"/>
</clipPath>
</defs>
<rect width="680" height="680" rx="120" style="fill:rgb(30, 27, 75);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<g clip-path="url(#rounded-clip)" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<!-- Dekorativni krugovi u pozadini -->
<circle cx="560" cy="160" r="180" fill="#2d2a6e" opacity="0.5" style="fill:rgb(45, 42, 110);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.5;font-family:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<circle cx="140" cy="520" r="120" fill="#2d2a6e" opacity="0.4" style="fill:rgb(45, 42, 110);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.4;font-family:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<!-- Horizontalna linija-akcenat -->
<rect x="80" y="335" width="520" height="3" rx="2" fill="#4f46e5" opacity="0.35" style="fill:rgb(79, 70, 229);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.35;font-family:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<!-- Slovo N -->
<path d="M80 490 L80 190 L135 190 L295 410 L295 190 L350 190 L350 490 L295 490 L135 270 L135 490 Z" fill="#6366f1" style="fill:rgb(99, 102, 241);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<path d="M80 190 L135 190 L135 255 L80 215 Z" fill="#818cf8" style="fill:rgb(129, 140, 248);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<path d="M295 430 L350 490 L295 490 Z" fill="#818cf8" style="fill:rgb(129, 140, 248);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<!-- Slovo T -->
<rect x="375" y="190" width="205" height="48" rx="6" fill="#6366f1" style="fill:rgb(99, 102, 241);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<rect x="452" y="238" width="52" height="252" rx="6" fill="#6366f1" style="fill:rgb(99, 102, 241);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<rect x="375" y="190" width="100" height="20" rx="6" fill="#818cf8" style="fill:rgb(129, 140, 248);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<!-- Tehnički detalj — tačka gore desno -->
<circle cx="590" cy="155" r="24" style="fill:rgb(99, 102, 241);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<circle cx="590" cy="155" r="13" fill="#1e1b4b" style="fill:rgb(30, 27, 75);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<circle cx="590" cy="155" r="6" style="fill:rgb(165, 180, 252);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<!-- Linija koja spaja sa T -->
<line x1="567" y1="162" x2="550" y2="190" stroke="#4f46e5" stroke-width="2.5" stroke-linecap="round" style="fill:rgb(0, 0, 0);stroke:rgb(79, 70, 229);color:rgb(251, 251, 254);stroke-width:2.5px;stroke-linecap:round;stroke-linejoin:miter;opacity:1;font-family:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<!-- Male tačke -->
<circle cx="590" cy="205" r="5" style="fill:rgb(55, 48, 163);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<circle cx="590" cy="228" r="5" style="fill:rgb(55, 48, 163);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<circle cx="590" cy="251" r="5" style="fill:rgb(55, 48, 163);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<!-- Donji akcenat -->
<rect x="80" y="518" width="130" height="4" rx="2" style="fill:rgb(99, 102, 241);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<rect x="223" y="518" width="44" height="4" rx="2" opacity="0.5" style="fill:rgb(129, 140, 248);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.5;font-family:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.4 KiB

+145
View File
@@ -85,6 +85,151 @@
</div> </div>
</form> </form>
<!-- pozadinska slika aplikacije — upload, preview i stilovi -->
<div class="kartica animiraj" style="margin-bottom:16px;"
x-data="{
blur: Number('{{.AppPozadinaBlur}}'),
opacity: Number('{{.AppPozadinaOpacity}}'),
slika: '{{.AppPozadina}}'
}">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--sb-akcent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Pozadinska slika aplikacije</span>
</div>
{{if .AppPozadina}}
<div style="margin-bottom:14px;display:flex;align-items:flex-start;gap:14px;flex-wrap:wrap;">
<img src="{{.AppPozadina}}" alt="Trenutna app pozadina"
style="width:160px;height:90px;object-fit:cover;border:0.5px solid var(--ivica);border-radius:8px;">
<form method="POST" action="/podesavanja/app-pozadina/ukloni" style="margin:0;">
<button type="submit"
style="padding:7px 14px;background:transparent;border:0.5px solid #fca5a5;border-radius:8px;color:#dc2626;font-size:12px;cursor:pointer;"
data-potvrda="Ukloniti pozadinsku sliku aplikacije?">
Ukloni sliku
</button>
</form>
</div>
{{end}}
<form method="POST" action="/podesavanja/app-pozadina" enctype="multipart/form-data">
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
<input type="file" name="app_pozadina" accept=".jpg,.jpeg,.png,.webp"
style="flex:1;min-width:200px;font-size:13px;color:var(--tekst-sporedni);">
<button type="submit"
style="padding:8px 16px;background:var(--sb-aktivan);color:var(--tekst-jak);border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;cursor:pointer;white-space:nowrap;transition:opacity 0.2s;"
onmouseover="this.style.opacity='0.8'" onmouseout="this.style.opacity='1'">
Otpremi sliku
</button>
</div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:6px;">JPG, PNG ili WebP — maksimum 5 MB</div>
</form>
<!-- live preview -->
<div style="margin-top:20px;padding-top:16px;border-top:0.5px solid var(--ivica);">
<div style="font-size:13px;font-weight:500;color:var(--tekst-glavni);margin-bottom:10px;">Preview</div>
<!-- simulacija layouta aplikacije -->
<div style="position:relative;width:100%;height:180px;border-radius:12px;overflow:hidden;"
:style="slika ? `background:url('${slika}') center/cover` : 'background:#1a2033'">
<!-- overlay -->
<div style="position:absolute;inset:0;pointer-events:none;"
:style="`background:rgba(0,0,0,${opacity}%)`"></div>
<!-- lažni sidebar -->
<div style="position:absolute;top:0;left:0;bottom:0;width:70px;display:flex;flex-direction:column;padding:10px 8px;gap:8px;"
:style="`background:rgba(255,255,255,0.08);backdrop-filter:blur(${blur}px);-webkit-backdrop-filter:blur(${blur}px);border-right:1px solid rgba(255,255,255,0.12)`">
<div style="width:38px;height:8px;background:rgba(255,255,255,0.35);border-radius:4px;"></div>
<div style="margin-top:6px;display:flex;flex-direction:column;gap:6px;">
<div style="width:30px;height:5px;background:rgba(255,255,255,0.2);border-radius:3px;"></div>
<div style="width:26px;height:5px;background:rgba(255,255,255,0.14);border-radius:3px;"></div>
<div style="width:32px;height:5px;background:rgba(255,255,255,0.14);border-radius:3px;"></div>
<div style="width:22px;height:5px;background:rgba(255,255,255,0.14);border-radius:3px;"></div>
</div>
</div>
<!-- lažni glavni sadržaj -->
<div style="position:absolute;top:0;left:70px;right:0;bottom:0;padding:7px;display:flex;flex-direction:column;gap:6px;">
<!-- lažni topbar -->
<div style="height:26px;border-radius:7px;flex-shrink:0;"
:style="`background:rgba(255,255,255,0.08);backdrop-filter:blur(${blur}px);-webkit-backdrop-filter:blur(${blur}px);border:1px solid rgba(255,255,255,0.12)`"></div>
<!-- lažne kartice -->
<div style="display:flex;gap:6px;flex:1;">
<div style="border-radius:7px;flex:1;"
:style="`background:rgba(255,255,255,0.08);backdrop-filter:blur(${blur}px);-webkit-backdrop-filter:blur(${blur}px);border:1px solid rgba(255,255,255,0.12)`"></div>
<div style="border-radius:7px;flex:1;"
:style="`background:rgba(255,255,255,0.08);backdrop-filter:blur(${blur}px);-webkit-backdrop-filter:blur(${blur}px);border:1px solid rgba(255,255,255,0.12)`"></div>
<div style="border-radius:7px;flex:1;"
:style="`background:rgba(255,255,255,0.08);backdrop-filter:blur(${blur}px);-webkit-backdrop-filter:blur(${blur}px);border:1px solid rgba(255,255,255,0.12)`"></div>
</div>
</div>
</div>
<!-- slideri -->
<div style="margin-top:14px;display:flex;flex-direction:column;gap:10px;">
<div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">
<span style="font-size:12px;color:var(--tekst-sporedni);">Zamućenje stakla</span>
<span style="font-size:12px;color:var(--tekst-glavni);font-weight:500;" x-text="blur + 'px'"></span>
</div>
<input type="range" x-model.number="blur" min="0" max="20" step="1"
style="width:100%;accent-color:var(--sb-akcent);height:4px;cursor:pointer;">
</div>
<div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">
<span style="font-size:12px;color:var(--tekst-sporedni);">Zatamnjivanje</span>
<span style="font-size:12px;color:var(--tekst-glavni);font-weight:500;" x-text="opacity + '%'"></span>
</div>
<input type="range" x-model.number="opacity" min="0" max="80" step="1"
style="width:100%;accent-color:var(--sb-akcent);height:4px;cursor:pointer;">
</div>
</div>
<!-- forma za čuvanje vrednosti slidera -->
<form method="POST" action="/podesavanja/app-pozadina/stilovi" style="margin-top:12px;">
<input type="hidden" name="blur" :value="blur">
<input type="hidden" name="opacity" :value="opacity">
<button type="submit"
style="padding:8px 20px;background:var(--sb-akcent);color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:500;cursor:pointer;transition:opacity 0.2s;"
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
Sačuvaj izgled
</button>
</form>
</div>
</div>
<!-- pozadinska slika login stranice — posebna forma jer je multipart -->
<form method="POST" action="/podesavanja/login-pozadina" enctype="multipart/form-data">
<div class="kartica animiraj" style="margin-bottom:16px;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--sb-akcent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Pozadinska slika prijave</span>
</div>
{{if .LoginPozadina}}
<div style="margin-bottom:14px;display:flex;align-items:flex-start;gap:14px;flex-wrap:wrap;">
<img src="{{.LoginPozadina}}" alt="Trenutna pozadina"
style="width:160px;height:90px;object-fit:cover;border:0.5px solid var(--ivica);border-radius:8px;">
<form method="POST" action="/podesavanja/login-pozadina/ukloni" style="margin:0;">
<button type="submit"
style="padding:7px 14px;background:transparent;border:0.5px solid #fca5a5;border-radius:8px;color:#dc2626;font-size:12px;cursor:pointer;"
onclick="return confirm('Ukloniti pozadinsku sliku?')">
Ukloni sliku
</button>
</form>
</div>
{{end}}
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
<input type="file" name="login_pozadina" accept=".jpg,.jpeg,.png,.webp"
style="flex:1;min-width:200px;font-size:13px;color:var(--tekst-sporedni);">
<button type="submit"
style="padding:8px 16px;background:var(--sb-aktivan);color:var(--tekst-jak);border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;cursor:pointer;white-space:nowrap;transition:opacity 0.2s;"
onmouseover="this.style.opacity='0.8'" onmouseout="this.style.opacity='1'">
Otpremi sliku
</button>
</div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:6px;">JPG, PNG ili WebP — maksimum 5 MB</div>
</div>
</form>
<form method="POST" action="/podesavanja/sacuvaj"> <form method="POST" action="/podesavanja/sacuvaj">
<!-- sekcija: firma --> <!-- sekcija: firma -->
+24
View File
@@ -14,7 +14,31 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 16px; padding: 16px;
position: relative;
} }
{{if .LoginPozadina}}
body::before {
content: '';
position: fixed;
inset: 0;
background-image: url('{{.LoginPozadina}}');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
z-index: 0;
}
body::after {
content: '';
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.40);
z-index: 1;
}
.kartica {
position: relative;
z-index: 2;
}
{{end}}
.kartica { .kartica {
background: #1a1d27; background: #1a1d27;
border: 0.5px solid #2d3148; border: 0.5px solid #2d3148;
@@ -148,6 +148,14 @@
{{if .Nalog.Avans}}{{.Nalog.AvansStr}} din{{else}}—{{end}} {{if .Nalog.Avans}}{{.Nalog.AvansStr}} din{{else}}—{{end}}
</div> </div>
</div> </div>
{{if .Nalog.PreostaloZaNaplatu}}
<div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Preostalo za naplatu</div>
<div style="font-size:20px;font-weight:600;color:#16a34a;">
{{.Nalog.PreostaloZaNaplatuStr}} din
</div>
</div>
{{end}}
</div> </div>
</div> </div>
@@ -2,6 +2,7 @@
<!doctype html> <!doctype html>
<html lang="sr"> <html lang="sr">
<head> <head>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{block "naslov" .}}NTech{{end}}</title> <title>{{block "naslov" .}}NTech{{end}}</title>
@@ -23,8 +24,49 @@
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
{{block "dodatni-css" .}}{{end}} {{block "dodatni-css" .}}{{end}}
{{if .AppPozadina}}
<style>
body {
background-image: url('{{.AppPozadina}}');
background-size: cover;
background-position: center;
background-attachment: fixed;
}
.app-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,{{.AppPozadinaOpacity}}%);
pointer-events: none;
z-index: 0;
}
.raspored {
position: relative;
z-index: 1;
}
.sidebar {
background: rgba(255,255,255,0.08) !important;
backdrop-filter: blur({{.AppPozadinaBlur}}px);
-webkit-backdrop-filter: blur({{.AppPozadinaBlur}}px);
border-right: 1px solid rgba(255,255,255,0.12) !important;
}
.topbar {
background: rgba(255,255,255,0.08) !important;
backdrop-filter: blur({{.AppPozadinaBlur}}px);
-webkit-backdrop-filter: blur({{.AppPozadinaBlur}}px);
border-bottom: 1px solid rgba(255,255,255,0.12) !important;
}
.kartica {
background: rgba(255,255,255,0.08) !important;
backdrop-filter: blur({{.AppPozadinaBlur}}px);
-webkit-backdrop-filter: blur({{.AppPozadinaBlur}}px);
border: 1px solid rgba(255,255,255,0.12) !important;
}
</style>
{{end}}
</head> </head>
<body> <body>
{{if .AppPozadina}}<div class="app-overlay"></div>{{end}}
<div class="sidebar-overlay" id="sidebar-overlay"></div> <div class="sidebar-overlay" id="sidebar-overlay"></div>
<div class="raspored"> <div class="raspored">
{{template "sidebar" .}} {{template "sidebar" .}}