Files
GoNtech/internal/handler/profil.go
T
Dasko f7a5d2673b Tema: slider za brzinu animacije, zamena scaleIn sa blurIn, AJAX čuvanje
- nova animacija blurIn (zamagljivanje) umesto scaleIn koji je izgledao isto kao fadeIn
- slider za brzinu animacije (0.1s–0.8s, korak 0.1) premešten u karticu animacije
- brzina i vrsta animacije čuvaju se jednim klikom, iz istog forma
- nova kolona lokalna_brzina_animacije u bazi (migracija 056)
- AJAX čuvanje profil/tema: nema reload stranice, scroll ostaje, toast notifikacija
- otpremnica vidljiva samo za status Završeno/Preuzeto; radni nalog skriven kada završeno
- toast notifikacije sa punom bojom pozadine (svetla i tamna tema)
2026-06-20 12:42:11 +02:00

527 lines
18 KiB
Go

package handler
import (
"fmt"
"io"
"log/slog"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"ntech/internal/db/sqlite"
"ntech/internal/middleware"
)
// sacuvanoReferer vraća URL za redirect posle uspešnog čuvanja:
// koristi Referer zaglavlje (samo putanja, nikad spoljni sajt) i dodaje ?sacuvano=1.
// AJAX handler u browseru detektuje sacuvano=1 i prikazuje toast bez reloada.
func sacuvanoReferer(r *http.Request, rezervna string) string {
if ref := r.Referer(); ref != "" {
if u, err := url.Parse(ref); err == nil {
putanja := u.RequestURI()
if strings.Contains(putanja, "?") {
return putanja + "&sacuvano=1"
}
return putanja + "?sacuvano=1"
}
}
return rezervna + "?sacuvano=1"
}
// ProfilTema prikazuje stranicu lične teme i pozadine
func (h *Handler) ProfilTema(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "tema.lokalno") {
http.Error(w, "Pristup odbijen.", http.StatusForbidden)
return
}
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if err != nil {
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
return
}
svezi, err := h.KorisniciRepo.DohvatiPoID(r.Context(), k.ID)
if err != nil {
http.Error(w, "Greška pri učitavanju profila", http.StatusInternalServerError)
return
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "profil-tema"
ps.NaslovStranice = "Moja tema"
podaci := podaciProfilTema{
PodaciStranice: ps,
LokalnaTema: svezi.LokalnaTema,
KoristiLokalnuTemu: svezi.KoristiLokalnuTemu,
LokalnaPozadina: svezi.LokalnaPozadina,
LokalnaPozadinaOpacity: svezi.LokalnaPozadinaOpacity,
LokalnaPozadinaBlur: svezi.LokalnaPozadinaBlur,
LokalnaPozadinaBlurPozadine: svezi.LokalnaPozadinaBlurPozadine,
LokalnaPozadinaGlassOpacity: svezi.LokalnaPozadinaGlassOpacity,
LokalnaAnimacija: svezi.LokalnaAnimacija,
LokalniHover: svezi.LokalniHover,
}
if podaci.LokalnaPozadinaOpacity == "" {
podaci.LokalnaPozadinaOpacity = "50"
}
if podaci.LokalnaPozadinaBlur == "" {
podaci.LokalnaPozadinaBlur = "12"
}
if podaci.LokalnaPozadinaBlurPozadine == "" {
podaci.LokalnaPozadinaBlurPozadine = "0"
}
if podaci.LokalnaPozadinaGlassOpacity == "" {
podaci.LokalnaPozadinaGlassOpacity = "10"
}
h.renderujTemplate(w, "profil_tema", podaci)
}
// ProfilOtpremiPozadinu prima upload lične pozadinske slike
func (h *Handler) ProfilOtpremiPozadinu(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "tema.lokalno") {
http.Error(w, "Pristup odbijen.", 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, "/profil/tema", http.StatusSeeOther)
return
}
fajl, zaglavlje, err := r.FormFile("lokalna_pozadina")
if err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Nije odabran fajl.")
http.Redirect(w, r, "/profil/tema", 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, "/profil/tema", 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, "/profil/tema", 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, "/profil/tema", 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, "/profil/tema", http.StatusSeeOther)
return
}
// briše staru ličnu pozadinu sa diska ako postoji
svezi, _ := h.KorisniciRepo.DohvatiPoID(r.Context(), k.ID)
if svezi != nil && svezi.LokalnaPozadina != "" {
deo, _, _ := strings.Cut(svezi.LokalnaPozadina, "?")
os.Remove(filepath.Join("web/static/uploads", filepath.Base(deo)))
}
// ime fajla: korisnik_{id}_pozadina.ext — deterministično, lako obrisati
novoIme := fmt.Sprintf("korisnik_%d_pozadina%s", k.ID, ext)
odrediste := filepath.Join("web/static/uploads", novoIme)
dst, err := os.Create(odrediste)
if err != nil {
slog.Error("ProfilOtpremiPozadinu: ne mogu kreirati fajl", "error", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
defer dst.Close()
if _, err := io.Copy(dst, fajl); err != nil {
slog.Error("ProfilOtpremiPozadinu: greška pri kopiranju", "error", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
putanja := fmt.Sprintf("/static/uploads/%s?v=%d", novoIme, time.Now().Unix())
opacity := "50"
blur := "12"
blurPozadine := "0"
glassOpacity := "10"
if svezi != nil {
if svezi.LokalnaPozadinaOpacity != "" {
opacity = svezi.LokalnaPozadinaOpacity
}
if svezi.LokalnaPozadinaBlur != "" {
blur = svezi.LokalnaPozadinaBlur
}
if svezi.LokalnaPozadinaBlurPozadine != "" {
blurPozadine = svezi.LokalnaPozadinaBlurPozadine
}
if svezi.LokalnaPozadinaGlassOpacity != "" {
glassOpacity = svezi.LokalnaPozadinaGlassOpacity
}
}
if err := h.KorisniciRepo.SacuvajLokalnuPozadinu(r.Context(), k.ID, putanja, opacity, blur, blurPozadine, glassOpacity); err != nil {
slog.Error("ProfilOtpremiPozadinu: greška pri čuvanju", "error", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju podešavanja.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
http.Redirect(w, r, "/profil/tema?sacuvano=1", http.StatusSeeOther)
}
// ProfilUkloniPozadinu briše ličnu pozadinsku sliku korisnika
func (h *Handler) ProfilUkloniPozadinu(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "tema.lokalno") {
http.Error(w, "Pristup odbijen.", http.StatusForbidden)
return
}
svezi, _ := h.KorisniciRepo.DohvatiPoID(r.Context(), k.ID)
if svezi != nil && svezi.LokalnaPozadina != "" {
deo, _, _ := strings.Cut(svezi.LokalnaPozadina, "?")
os.Remove(filepath.Join("web/static/uploads", filepath.Base(deo)))
}
if err := h.KorisniciRepo.SacuvajLokalnuPozadinu(r.Context(), k.ID, "", "50", "12", "0", "10"); err != nil {
slog.Error("ProfilUkloniPozadinu", "error", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju slike.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
http.Redirect(w, r, "/profil/tema?sacuvano=1", http.StatusSeeOther)
}
// ProfilSacuvajPozadinuStilove čuva opacity i blur lične pozadine
func (h *Handler) ProfilSacuvajPozadinuStilove(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "tema.lokalno") {
http.Error(w, "Pristup odbijen.", 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, "/profil/tema", http.StatusSeeOther)
return
}
svezi, _ := h.KorisniciRepo.DohvatiPoID(r.Context(), k.ID)
pozadina := ""
if svezi != nil {
pozadina = svezi.LokalnaPozadina
}
opacity := r.FormValue("lokalna_pozadina_opacity")
blur := r.FormValue("lokalna_pozadina_blur")
blurPozadineSt := r.FormValue("lokalna_pozadina_blur_pozadine")
glassOpacitySt := r.FormValue("lokalna_pozadina_glass_opacity")
if opacity == "" {
opacity = "50"
}
if blur == "" {
blur = "12"
}
if blurPozadineSt == "" {
blurPozadineSt = "0"
}
if glassOpacitySt == "" {
glassOpacitySt = "10"
}
if err := h.KorisniciRepo.SacuvajLokalnuPozadinu(r.Context(), k.ID, pozadina, opacity, blur, blurPozadineSt, glassOpacitySt); err != nil {
slog.Error("ProfilSacuvajPozadinuStilove", "error", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju podešavanja.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
http.Redirect(w, r, "/profil/tema?sacuvano=1", http.StatusSeeOther)
}
// SacuvajLokalnuTemu čuva korisnikovu lokalnu temu
func (h *Handler) SacuvajLokalnuTemu(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil {
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
return
}
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "tema.lokalno") {
http.Error(w, "Pristup odbijen", http.StatusForbidden)
return
}
if err := r.ParseForm(); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška. Pokušajte ponovo.")
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
return
}
koristi := r.FormValue("koristi_lokalnu_temu") == "1"
lokalnaTema := r.FormValue("lokalna_tema")
if lokalnaTema != "tamna" && lokalnaTema != "svetla" {
lokalnaTema = "tamna"
}
if err := h.KorisniciRepo.SacuvajLokalnuTemu(r.Context(), k.ID, lokalnaTema, koristi); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.")
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
return
}
http.Redirect(w, r, sacuvanoReferer(r, "/admin/profil"), http.StatusSeeOther)
}
// SacuvajLokalnuAnimaciju čuva korisnikovu preferencu animacije tabela
func (h *Handler) SacuvajLokalnuAnimaciju(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil {
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
return
}
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "tema.lokalno") {
http.Error(w, "Pristup odbijen", http.StatusForbidden)
return
}
if err := r.ParseForm(); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška. Pokušajte ponovo.")
http.Redirect(w, r, "/admin/profil/tema", http.StatusSeeOther)
return
}
animacija := r.FormValue("lokalna_animacija")
dozvoljene := map[string]bool{"": true, "bez": true, "fadeInUp": true, "fadeIn": true, "blurIn": true, "slideLeft": true}
if !dozvoljene[animacija] {
animacija = ""
}
brzina := r.FormValue("lokalna_brzina_animacije")
dozvoljenieBrzine := map[string]bool{"": true, "0.1": true, "0.2": true, "0.3": true, "0.4": true, "0.5": true, "0.6": true, "0.7": true, "0.8": true}
if !dozvoljenieBrzine[brzina] {
brzina = ""
}
if err := h.KorisniciRepo.SacuvajLokalnuAnimaciju(r.Context(), k.ID, animacija); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.")
http.Redirect(w, r, "/admin/profil/tema", http.StatusSeeOther)
return
}
if err := h.KorisniciRepo.SacuvajLokalnuBrzinuAnimacije(r.Context(), k.ID, brzina); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.")
http.Redirect(w, r, "/admin/profil/tema", http.StatusSeeOther)
return
}
http.Redirect(w, r, sacuvanoReferer(r, "/admin/profil/tema"), http.StatusSeeOther)
}
// SacuvajLokalniHover čuva korisnikovu preferencu hover efekta na karticama
func (h *Handler) SacuvajLokalniHover(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil {
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
return
}
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "tema.lokalno") {
http.Error(w, "Pristup odbijen", http.StatusForbidden)
return
}
if err := r.ParseForm(); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška. Pokušajte ponovo.")
http.Redirect(w, r, "/admin/profil/tema", http.StatusSeeOther)
return
}
hover := r.FormValue("lokalni_hover")
dozvoljeni := map[string]bool{"": true, "bez": true, "podizanje": true, "svetlost": true, "zoom": true, "boja": true}
if !dozvoljeni[hover] {
hover = ""
}
if err := h.KorisniciRepo.SacuvajLokalniHover(r.Context(), k.ID, hover); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.")
http.Redirect(w, r, "/admin/profil/tema", http.StatusSeeOther)
return
}
http.Redirect(w, r, sacuvanoReferer(r, "/admin/profil/tema"), http.StatusSeeOther)
}
// SacuvajLokalnuBrzinuAnimacije čuva korisnikovu preferencu brzine animacije tabela
func (h *Handler) SacuvajLokalnuBrzinuAnimacije(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil {
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
return
}
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "tema.lokalno") {
http.Error(w, "Pristup odbijen", http.StatusForbidden)
return
}
if err := r.ParseForm(); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška. Pokušajte ponovo.")
http.Redirect(w, r, "/admin/profil/tema", http.StatusSeeOther)
return
}
brzina := r.FormValue("lokalna_brzina_animacije")
dozvoljene := map[string]bool{"": true, "0.2": true, "0.4": true, "0.6": true, "0.8": true, "1.0": true, "1.2": true, "1.5": true}
if !dozvoljene[brzina] {
brzina = ""
}
if err := h.KorisniciRepo.SacuvajLokalnuBrzinuAnimacije(r.Context(), k.ID, brzina); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.")
http.Redirect(w, r, "/admin/profil/tema", http.StatusSeeOther)
return
}
http.Redirect(w, r, sacuvanoReferer(r, "/admin/profil/tema"), http.StatusSeeOther)
}
// ProfilOtpremiAvatar prima upload lične avatar slike korisnika
func (h *Handler) ProfilOtpremiAvatar(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil {
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 2<<20+4096)
if err := r.ParseMultipartForm(2 << 20); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Fajl je prevelik (maksimum 2 MB).")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
fajl, zaglavlje, err := r.FormFile("avatar")
if err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Nije odabran fajl.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
defer fajl.Close()
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, "/profil/tema", http.StatusSeeOther)
return
}
buf := make([]byte, 512)
n, err := fajl.Read(buf)
if err != nil && err != io.EOF {
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čitanju fajla.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
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, "/profil/tema", 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, "/profil/tema", http.StatusSeeOther)
return
}
// briše stari avatar sa diska
svezi, _ := h.KorisniciRepo.DohvatiPoID(r.Context(), k.ID)
if svezi != nil && svezi.AvatarPutanja != "" {
deo, _, _ := strings.Cut(svezi.AvatarPutanja, "?")
os.Remove(filepath.Join("web/static/uploads", filepath.Base(deo)))
}
novoIme := fmt.Sprintf("korisnik_%d_avatar%s", k.ID, ext)
odrediste := filepath.Join("web/static/uploads", novoIme)
dst, err := os.Create(odrediste)
if err != nil {
slog.Error("ProfilOtpremiAvatar: ne mogu kreirati fajl", "error", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
defer dst.Close()
if _, err := io.Copy(dst, fajl); err != nil {
slog.Error("ProfilOtpremiAvatar: greška pri kopiranju", "error", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
// cache-buster verzija u URL-u
v := fmt.Sprintf("%d", time.Now().Unix())
putanja := fmt.Sprintf("/static/uploads/%s?v=%s", novoIme, v)
if err := h.KorisniciRepo.SacuvajAvatar(r.Context(), k.ID, putanja); err != nil {
slog.Error("ProfilOtpremiAvatar: greška pri čuvanju u bazi", "error", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
http.Redirect(w, r, "/profil/tema?sacuvano=1", http.StatusSeeOther)
}
// ProfilUkloniAvatar briše ličnu avatar sliku korisnika
func (h *Handler) ProfilUkloniAvatar(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil {
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
return
}
svezi, _ := h.KorisniciRepo.DohvatiPoID(r.Context(), k.ID)
if svezi != nil && svezi.AvatarPutanja != "" {
deo, _, _ := strings.Cut(svezi.AvatarPutanja, "?")
os.Remove(filepath.Join("web/static/uploads", filepath.Base(deo)))
}
if err := h.KorisniciRepo.SacuvajAvatar(r.Context(), k.ID, ""); err != nil {
slog.Error("ProfilUkloniAvatar", "error", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju avatara.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
http.Redirect(w, r, "/profil/tema?sacuvano=1", http.StatusSeeOther)
}