Podešavanja: pozadinska slika i glass efekt za aplikaciju sa live preview sliderom
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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")
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
INSERT OR IGNORE INTO podesavanja (kljuc, vrednost) VALUES ('login_pozadina', '');
|
||||||
@@ -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');
|
||||||
@@ -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:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 8.4 KiB |
@@ -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 -->
|
||||||
|
|||||||
@@ -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" .}}
|
||||||
|
|||||||
Reference in New Issue
Block a user