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)
This commit is contained in:
@@ -393,6 +393,7 @@ func main() {
|
||||
r.With(doz("tema.lokalno")).Post("/profil/tema", h.SacuvajLokalnuTemu)
|
||||
r.With(doz("tema.lokalno")).Post("/profil/animacija", h.SacuvajLokalnuAnimaciju)
|
||||
r.With(doz("tema.lokalno")).Post("/profil/hover", h.SacuvajLokalniHover)
|
||||
r.With(doz("tema.lokalno")).Post("/profil/brzina-animacije", h.SacuvajLokalnuBrzinuAnimacije)
|
||||
r.Get("/profil/tema", h.ProfilTema)
|
||||
r.With(doz("tema.lokalno")).Post("/profil/pozadina", h.ProfilOtpremiPozadinu)
|
||||
r.With(doz("tema.lokalno")).Post("/profil/pozadina/ukloni", h.ProfilUkloniPozadinu)
|
||||
|
||||
@@ -156,6 +156,7 @@ type KorisniciRepository interface {
|
||||
SacuvajLokalnuPozadinu(ctx context.Context, id int64, pozadina, opacity, blur, blurPozadine, glassOpacity string) error
|
||||
SacuvajLokalnuAnimaciju(ctx context.Context, id int64, animacija string) error
|
||||
SacuvajLokalniHover(ctx context.Context, id int64, hover string) error
|
||||
SacuvajLokalnuBrzinuAnimacije(ctx context.Context, id int64, brzina string) error
|
||||
SacuvajAvatar(ctx context.Context, id int64, putanja string) error
|
||||
PostojiIjedan(ctx context.Context) (bool, error)
|
||||
Obrisi(ctx context.Context, id int64) error
|
||||
|
||||
@@ -32,6 +32,7 @@ type korisnikOpcije struct {
|
||||
avatarPutanja sql.NullString
|
||||
lokalnaAnimacija sql.NullString
|
||||
lokalniHover sql.NullString
|
||||
lokalnaBrzinaAnimacije sql.NullString
|
||||
}
|
||||
|
||||
// dodeliOpcijeKorisnika prenosi vrednosti iz korisnikOpcije na model.Korisnik
|
||||
@@ -48,6 +49,7 @@ func dodeliOpcijeKorisnika(k *model.Korisnik, o korisnikOpcije) {
|
||||
k.AvatarPutanja = o.avatarPutanja.String
|
||||
k.LokalnaAnimacija = o.lokalnaAnimacija.String
|
||||
k.LokalniHover = o.lokalniHover.String
|
||||
k.LokalnaBrzinaAnimacije = o.lokalnaBrzinaAnimacije.String
|
||||
}
|
||||
|
||||
// skeniraiKorisnika čita jedan red iz baze i popunjava model.Korisnik
|
||||
@@ -59,7 +61,7 @@ func skeniraiKorisnika(row interface{ Scan(...any) error }) (*model.Korisnik, er
|
||||
&o.lokalnaTema, &o.koristiLokalnuTemu, &o.datumKreiranja,
|
||||
&o.lokalnaPozadina, &o.lokalnaPozadinaOpacity, &o.lokalnaPozadinaBlur,
|
||||
&o.lokalnaPozadinaBlurPozadine, &o.lokalnaPozadinaGlassOpacity, &o.avatarPutanja,
|
||||
&o.lokalnaAnimacija, &o.lokalniHover,
|
||||
&o.lokalnaAnimacija, &o.lokalniHover, &o.lokalnaBrzinaAnimacije,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -104,7 +106,8 @@ func (r *sqliteKorisniciRepo) DohvatiPoImenu(ctx context.Context, korisnickoIme
|
||||
COALESCE(lokalna_pozadina, ''), COALESCE(lokalna_pozadina_opacity, '50'),
|
||||
COALESCE(lokalna_pozadina_blur, '12'), COALESCE(lokalna_pozadina_blur_pozadine, '0'),
|
||||
COALESCE(lokalna_pozadina_glass_opacity, '10'), COALESCE(avatar_putanja, ''),
|
||||
COALESCE(lokalna_animacija, ''), COALESCE(lokalni_hover, '')
|
||||
COALESCE(lokalna_animacija, ''), COALESCE(lokalni_hover, ''),
|
||||
COALESCE(lokalna_brzina_animacije, '')
|
||||
FROM korisnici WHERE korisnicko_ime = ?`, korisnickoIme)
|
||||
k, err := skeniraiKorisnika(row)
|
||||
if err != nil {
|
||||
@@ -121,7 +124,8 @@ func (r *sqliteKorisniciRepo) DohvatiPoID(ctx context.Context, id int64) (*model
|
||||
COALESCE(lokalna_pozadina, ''), COALESCE(lokalna_pozadina_opacity, '50'),
|
||||
COALESCE(lokalna_pozadina_blur, '12'), COALESCE(lokalna_pozadina_blur_pozadine, '0'),
|
||||
COALESCE(lokalna_pozadina_glass_opacity, '10'), COALESCE(avatar_putanja, ''),
|
||||
COALESCE(lokalna_animacija, ''), COALESCE(lokalni_hover, '')
|
||||
COALESCE(lokalna_animacija, ''), COALESCE(lokalni_hover, ''),
|
||||
COALESCE(lokalna_brzina_animacije, '')
|
||||
FROM korisnici WHERE id = ?`, id)
|
||||
k, err := skeniraiKorisnika(row)
|
||||
if err != nil {
|
||||
@@ -138,7 +142,8 @@ func (r *sqliteKorisniciRepo) Lista(ctx context.Context) ([]model.Korisnik, erro
|
||||
COALESCE(lokalna_pozadina, ''), COALESCE(lokalna_pozadina_opacity, '50'),
|
||||
COALESCE(lokalna_pozadina_blur, '12'), COALESCE(lokalna_pozadina_blur_pozadine, '0'),
|
||||
COALESCE(lokalna_pozadina_glass_opacity, '10'), COALESCE(avatar_putanja, ''),
|
||||
COALESCE(lokalna_animacija, ''), COALESCE(lokalni_hover, '')
|
||||
COALESCE(lokalna_animacija, ''), COALESCE(lokalni_hover, ''),
|
||||
COALESCE(lokalna_brzina_animacije, '')
|
||||
FROM korisnici ORDER BY datum_kreiranja ASC`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: korisnici.Lista: %w", err)
|
||||
@@ -153,7 +158,7 @@ func (r *sqliteKorisniciRepo) Lista(ctx context.Context) ([]model.Korisnik, erro
|
||||
&o.lokalnaTema, &o.koristiLokalnuTemu, &o.datumKreiranja,
|
||||
&o.lokalnaPozadina, &o.lokalnaPozadinaOpacity, &o.lokalnaPozadinaBlur,
|
||||
&o.lokalnaPozadinaBlurPozadine, &o.lokalnaPozadinaGlassOpacity, &o.avatarPutanja,
|
||||
&o.lokalnaAnimacija, &o.lokalniHover,
|
||||
&o.lokalnaAnimacija, &o.lokalniHover, &o.lokalnaBrzinaAnimacije,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("ntech: korisnici.Lista: %w", err)
|
||||
}
|
||||
@@ -229,6 +234,19 @@ func (r *sqliteKorisniciRepo) SacuvajLokalniHover(ctx context.Context, id int64,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *sqliteKorisniciRepo) SacuvajLokalnuBrzinuAnimacije(ctx context.Context, id int64, brzina string) error {
|
||||
var val any
|
||||
if brzina != "" {
|
||||
val = brzina
|
||||
}
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
`UPDATE korisnici SET lokalna_brzina_animacije = ? WHERE id = ?`, val, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: korisnici.SacuvajLokalnuBrzinuAnimacije: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *sqliteKorisniciRepo) AzurirajUlogu(ctx context.Context, id int64, uloga string) error {
|
||||
_, err := r.db.ExecContext(ctx, `UPDATE korisnici SET uloga = ? WHERE id = ?`, uloga, id)
|
||||
if err != nil {
|
||||
|
||||
@@ -215,6 +215,7 @@ func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]s
|
||||
if korisnik != nil {
|
||||
ps.LokalnaAnimacija = korisnik.LokalnaAnimacija
|
||||
ps.LokalniHover = korisnik.LokalniHover
|
||||
ps.LokalnaBrzinaAnimacije = korisnik.LokalnaBrzinaAnimacije
|
||||
}
|
||||
|
||||
return ps
|
||||
|
||||
+63
-31
@@ -15,6 +15,22 @@ import (
|
||||
"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())
|
||||
@@ -177,8 +193,7 @@ func (h *Handler) ProfilOtpremiPozadinu(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika je uspešno otpremljena.")
|
||||
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/profil/tema?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// ProfilUkloniPozadinu briše ličnu pozadinsku sliku korisnika
|
||||
@@ -202,8 +217,7 @@ func (h *Handler) ProfilUkloniPozadinu(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika je uklonjena.")
|
||||
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/profil/tema?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// ProfilSacuvajPozadinuStilove čuva opacity i blur lične pozadine
|
||||
@@ -250,8 +264,7 @@ func (h *Handler) ProfilSacuvajPozadinuStilove(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Podešavanja su sačuvana.")
|
||||
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/profil/tema?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// SacuvajLokalnuTemu čuva korisnikovu lokalnu temu
|
||||
@@ -284,15 +297,7 @@ func (h *Handler) SacuvajLokalnuTemu(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Tema je sačuvana.")
|
||||
// koristimo samo putanju iz Referer zaglavlja — nikad ceo URL jer može biti spoljni sajt
|
||||
if ref := r.Referer(); ref != "" {
|
||||
if u, err := url.Parse(ref); err == nil {
|
||||
http.Redirect(w, r, u.RequestURI(), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
}
|
||||
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
|
||||
http.Redirect(w, r, sacuvanoReferer(r, "/admin/profil"), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// SacuvajLokalnuAnimaciju čuva korisnikovu preferencu animacije tabela
|
||||
@@ -313,25 +318,29 @@ func (h *Handler) SacuvajLokalnuAnimaciju(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
animacija := r.FormValue("lokalna_animacija")
|
||||
dozvoljene := map[string]bool{"": true, "bez": true, "fadeInUp": true, "fadeIn": true, "scaleIn": true, "slideLeft": true}
|
||||
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
|
||||
}
|
||||
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Animacija je sačuvana.")
|
||||
if ref := r.Referer(); ref != "" {
|
||||
if u, err := url.Parse(ref); err == nil {
|
||||
http.Redirect(w, r, u.RequestURI(), http.StatusSeeOther)
|
||||
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, "/admin/profil/tema", http.StatusSeeOther)
|
||||
|
||||
http.Redirect(w, r, sacuvanoReferer(r, "/admin/profil/tema"), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// SacuvajLokalniHover čuva korisnikovu preferencu hover efekta na karticama
|
||||
@@ -363,14 +372,39 @@ func (h *Handler) SacuvajLokalniHover(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Hover efekat je sačuvan.")
|
||||
if ref := r.Referer(); ref != "" {
|
||||
if u, err := url.Parse(ref); err == nil {
|
||||
http.Redirect(w, r, u.RequestURI(), http.StatusSeeOther)
|
||||
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
|
||||
@@ -464,8 +498,7 @@ func (h *Handler) ProfilOtpremiAvatar(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Avatar je sačuvan.")
|
||||
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/profil/tema?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// ProfilUkloniAvatar briše ličnu avatar sliku korisnika
|
||||
@@ -489,6 +522,5 @@ func (h *Handler) ProfilUkloniAvatar(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Avatar je uklonjen.")
|
||||
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/profil/tema?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ type Korisnik struct {
|
||||
AvatarPutanja string
|
||||
LokalnaAnimacija string // "" | "fadeInUp" | "fadeIn" | "scaleIn" | "slideLeft"
|
||||
LokalniHover string // "" | "bez" | "podizanje" | "svetlost"
|
||||
LokalnaBrzinaAnimacije string // "" | "0.2" | "0.4" | "0.6" | "0.8" | "1.2" (sekunde)
|
||||
}
|
||||
|
||||
// Sesija predstavlja aktivnu sesiju prijavljenog korisnika
|
||||
|
||||
@@ -55,6 +55,7 @@ type PodaciStranice struct {
|
||||
AppPozadinaGlassOpacity string // vrednost 0-80 (% zatamnjivanje glass elemenata) — samo za ličnu pozadinu
|
||||
LokalnaAnimacija string // "" | "fadeInUp" | "fadeIn" | "scaleIn" | "slideLeft"
|
||||
LokalniHover string // "" | "bez" | "podizanje" | "svetlost"
|
||||
LokalnaBrzinaAnimacije string // "" | "0.2" | "0.4" | ... | "1.5" (sekunde)
|
||||
}
|
||||
|
||||
// PodaciDashboarda su podaci specifični za dashboard stranicu
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE korisnici ADD COLUMN lokalna_brzina_animacije TEXT NOT NULL DEFAULT '';
|
||||
+25
-15
@@ -5,6 +5,16 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:root { --anim-trajanje: 0.4s; }
|
||||
[data-brzina-animacije="0.1"] { --anim-trajanje: 0.1s; }
|
||||
[data-brzina-animacije="0.2"] { --anim-trajanje: 0.2s; }
|
||||
[data-brzina-animacije="0.3"] { --anim-trajanje: 0.3s; }
|
||||
[data-brzina-animacije="0.4"] { --anim-trajanje: 0.4s; }
|
||||
[data-brzina-animacije="0.5"] { --anim-trajanje: 0.5s; }
|
||||
[data-brzina-animacije="0.6"] { --anim-trajanje: 0.6s; }
|
||||
[data-brzina-animacije="0.7"] { --anim-trajanje: 0.7s; }
|
||||
[data-brzina-animacije="0.8"] { --anim-trajanje: 0.8s; }
|
||||
|
||||
html {
|
||||
background: var(--pozadina);
|
||||
}
|
||||
@@ -793,8 +803,8 @@ select {
|
||||
animation: toastIn 0.3s ease forwards;
|
||||
max-width: 340px;
|
||||
}
|
||||
.toast-greska { background: rgba(207, 87, 87, 0.12); color: var(--greska); border: 0.5px solid var(--greska); }
|
||||
.toast-uspeh { background: var(--poruka-uspeh-bg); color: var(--poruka-uspeh-boja); border: 0.5px solid var(--poruka-uspeh-boja); }
|
||||
.toast-greska { background: #fef2f2; color: #dc2626; border: 0.5px solid #fecaca; }
|
||||
.toast-uspeh { background: #f0fdf4; color: #15803d; border: 0.5px solid #86efac; }
|
||||
.toast.nestaje { animation: toastOut 0.3s ease forwards; }
|
||||
@keyframes toastIn {
|
||||
from { opacity: 0; transform: translateY(12px); }
|
||||
@@ -1011,7 +1021,7 @@ select {
|
||||
|
||||
/* animacije */
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
from { opacity: 0; transform: translateY(16px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@@ -1020,13 +1030,13 @@ select {
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from { opacity: 0; transform: scale(0.95); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
@keyframes blurIn {
|
||||
from { opacity: 0; filter: blur(8px); }
|
||||
to { opacity: 1; filter: blur(0); }
|
||||
}
|
||||
|
||||
@keyframes slideLeft {
|
||||
from { opacity: 0; transform: translateX(-12px); }
|
||||
from { opacity: 0; transform: translateX(-18px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
|
||||
@@ -1050,7 +1060,7 @@ select {
|
||||
/* backwards (ne both): drži početni frame tokom stagger-delay-a da nema treperenja,
|
||||
ali NE zaključava krajnji transform — pa .kartica:hover lift radi i na .animiraj karticama */
|
||||
.animiraj {
|
||||
animation: fadeInUp 0.2s ease backwards;
|
||||
animation: fadeInUp var(--anim-trajanje) ease backwards;
|
||||
}
|
||||
|
||||
/* kartice ne animiramo po defaultu — animacija je samo na redovima tabela */
|
||||
@@ -1066,26 +1076,26 @@ select {
|
||||
|
||||
[data-animacija="fadeInUp"] .animiraj,
|
||||
[data-animacija="fadeInUp"] .tabela tbody tr,
|
||||
[data-animacija="fadeInUp"] .kartica.animiraj { animation: fadeInUp 0.2s ease backwards; }
|
||||
[data-animacija="fadeInUp"] .kartica.animiraj { animation: fadeInUp var(--anim-trajanje) ease backwards; }
|
||||
|
||||
[data-animacija="fadeIn"] .animiraj,
|
||||
[data-animacija="fadeIn"] .tabela tbody tr,
|
||||
[data-animacija="fadeIn"] .kartica.animiraj { animation: fadeIn 0.2s ease backwards; }
|
||||
[data-animacija="fadeIn"] .kartica.animiraj { animation: fadeIn var(--anim-trajanje) ease backwards; }
|
||||
|
||||
[data-animacija="scaleIn"] .animiraj,
|
||||
[data-animacija="scaleIn"] .tabela tbody tr,
|
||||
[data-animacija="scaleIn"] .kartica.animiraj { animation: scaleIn 0.2s ease backwards; }
|
||||
[data-animacija="blurIn"] .animiraj,
|
||||
[data-animacija="blurIn"] .tabela tbody tr,
|
||||
[data-animacija="blurIn"] .kartica.animiraj { animation: blurIn var(--anim-trajanje) ease backwards; }
|
||||
|
||||
[data-animacija="slideLeft"] .animiraj,
|
||||
[data-animacija="slideLeft"] .tabela tbody tr,
|
||||
[data-animacija="slideLeft"] .kartica.animiraj { animation: slideLeft 0.2s ease backwards; }
|
||||
[data-animacija="slideLeft"] .kartica.animiraj { animation: slideLeft var(--anim-trajanje) ease backwards; }
|
||||
|
||||
/* Stepenasta (stagger) animacija redova u svim listama — JEDNO mesto za sve tabele.
|
||||
Animacija se primenjuje direktno na <tr> u .tabela, bez potrebe za .animiraj klasom.
|
||||
Mora biti u main.css: HTMX navigacija (hx-select) odbacuje <head>, pa
|
||||
stilovi iz page <style> blokova ne bi važili tokom navigacije. */
|
||||
.tabela tbody tr {
|
||||
animation: fadeInUp 0.2s ease backwards;
|
||||
animation: fadeInUp var(--anim-trajanje) ease backwards;
|
||||
}
|
||||
.tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
|
||||
.tabela tbody tr:nth-child(2) { animation-delay: 0.08s; }
|
||||
|
||||
@@ -22,3 +22,6 @@
|
||||
--poruka-uspeh-bg: rgba(93, 184, 118, 0.15);
|
||||
--poruka-uspeh-boja: #5db876;
|
||||
}
|
||||
|
||||
.toast-uspeh { background: #1a2e22; color: #5db876; border: 0.5px solid #2d5a3d; }
|
||||
.toast-greska { background: #2e1a1a; color: #e07070; border: 0.5px solid #5a2d2d; }
|
||||
|
||||
@@ -83,24 +83,23 @@
|
||||
|
||||
<form method="POST" action="/profil/animacija">
|
||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||
<input type="hidden" name="lokalna_brzina_animacije" id="brzina-hidden" value="{{if .LokalnaBrzinaAnimacije}}{{.LokalnaBrzinaAnimacije}}{{else}}0.4{{end}}">
|
||||
<div class="kolona" style="gap:16px;">
|
||||
<div style="max-width:300px;">
|
||||
<label class="polje-labela" for="anim-select">Vrsta animacije pri učitavanju</label>
|
||||
<select id="anim-select" name="lokalna_animacija" style="width:100%;" onchange="animPreview(this.value)"><!-- preview se primenjuje na wrapper, ne na body -->
|
||||
<select id="anim-select" name="lokalna_animacija" style="width:100%;" onchange="animPreview(this.value)">
|
||||
<option value="" {{if eq .LokalnaAnimacija ""}}selected{{end}}>Podrazumevano (klizanje gore)</option>
|
||||
<option value="bez" {{if eq .LokalnaAnimacija "bez"}}selected{{end}}>Bez animacije</option>
|
||||
<option value="fadeInUp" {{if eq .LokalnaAnimacija "fadeInUp"}}selected{{end}}>Klizanje gore (fadeInUp)</option>
|
||||
<option value="fadeIn" {{if eq .LokalnaAnimacija "fadeIn"}}selected{{end}}>Pojavljivanje (fadeIn)</option>
|
||||
<option value="scaleIn" {{if eq .LokalnaAnimacija "scaleIn"}}selected{{end}}>Zumiranje (scaleIn)</option>
|
||||
<option value="slideLeft" {{if eq .LokalnaAnimacija "slideLeft"}}selected{{end}}>Klizanje s leva (slideLeft)</option>
|
||||
<option value="fadeInUp" {{if eq .LokalnaAnimacija "fadeInUp"}}selected{{end}}>Klizanje gore</option>
|
||||
<option value="fadeIn" {{if eq .LokalnaAnimacija "fadeIn"}}selected{{end}}>Pojavljivanje</option>
|
||||
<option value="blurIn" {{if eq .LokalnaAnimacija "blurIn"}}selected{{end}}>Zamagljivanje</option>
|
||||
<option value="slideLeft" {{if eq .LokalnaAnimacija "slideLeft"}}selected{{end}}>Klizanje s leva</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- preview -->
|
||||
<div>
|
||||
<div class="pomocni-tekst" style="margin-bottom:8px;">Pregled:</div>
|
||||
<!-- preview wrapper nosi data-animacija atribut; isti CSS [data-animacija] .animiraj
|
||||
koji radi globalno na body radi i ovde, ali izolovano — pogađa SAMO redove unutar wrappera -->
|
||||
<div id="anim-preview-wrap" style="border:0.5px solid var(--ivica);border-radius:8px;overflow:hidden;max-width:340px;">
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
<tbody id="anim-preview-body">
|
||||
@@ -116,6 +115,20 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- slider brzine unutar iste forme -->
|
||||
<div style="max-width:340px;">
|
||||
<label class="polje-labela" for="brzina-slider">Trajanje efekta: <span id="brzina-labela">{{if .LokalnaBrzinaAnimacije}}{{.LokalnaBrzinaAnimacije}}s{{else}}0.4s{{end}}</span></label>
|
||||
<input type="range" id="brzina-slider"
|
||||
min="0.1" max="0.8" step="0.1"
|
||||
value="{{if .LokalnaBrzinaAnimacije}}{{.LokalnaBrzinaAnimacije}}{{else}}0.4{{end}}"
|
||||
style="width:100%;margin-top:8px;accent-color:var(--sb-akcent);"
|
||||
oninput="brzinaPromena(this.value)">
|
||||
<div style="display:flex;justify-content:space-between;margin-top:4px;">
|
||||
<span style="font-size:11px;color:var(--tekst-sporedni);">0.1s (brže)</span>
|
||||
<span style="font-size:11px;color:var(--tekst-sporedni);">0.8s (sporije)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit" class="btn-primarno">Sačuvaj</button>
|
||||
</div>
|
||||
@@ -376,7 +389,7 @@ var animVrednosti = {
|
||||
'bez': 'bez',
|
||||
'fadeInUp': 'fadeInUp',
|
||||
'fadeIn': 'fadeIn',
|
||||
'scaleIn': 'scaleIn',
|
||||
'blurIn': 'blurIn',
|
||||
'slideLeft': 'slideLeft'
|
||||
};
|
||||
// postavlja data-animacija na PREVIEW wrapper (ne na body), pa CSS
|
||||
@@ -385,10 +398,13 @@ function animPreview(val) {
|
||||
var wrap = document.getElementById('anim-preview-wrap');
|
||||
if (!wrap) return;
|
||||
var anim = animVrednosti[val] || 'fadeInUp';
|
||||
// restartuj animaciju: skini atribut, izazovi reflow, pa ga vrati
|
||||
// skini data-animacija i klasu animiraj sa redova da bi se animacija resetovala
|
||||
wrap.removeAttribute('data-animacija');
|
||||
void wrap.offsetHeight; // reflow da bi se animacija ponovo okinula
|
||||
var redovi = wrap.querySelectorAll('.animiraj');
|
||||
redovi.forEach(function(r) { r.classList.remove('animiraj'); });
|
||||
void wrap.offsetHeight; // reflow — bez ovoga browser spoji promene i ne restartuje
|
||||
wrap.setAttribute('data-animacija', anim);
|
||||
redovi.forEach(function(r) { r.classList.add('animiraj'); });
|
||||
}
|
||||
// "Ponovi pregled" — ponovo okida animaciju za trenutno izabrani stil
|
||||
function animPreviewPonovi() {
|
||||
@@ -399,5 +415,16 @@ function animPreviewPonovi() {
|
||||
var sel = document.getElementById('anim-select');
|
||||
if (sel) animPreview(sel.value);
|
||||
})();
|
||||
|
||||
function brzinaPromena(val) {
|
||||
var v = parseFloat(val).toFixed(1);
|
||||
document.getElementById('brzina-hidden').value = v;
|
||||
document.getElementById('brzina-labela').textContent = v + 's';
|
||||
// primeni odmah — korisnik vidi efekat pre čuvanja
|
||||
document.body.dataset.brzinaAnimacije = v;
|
||||
// ažuriraj i preview wrapper da odmah odrazi novu brzinu
|
||||
var wrap = document.getElementById('anim-preview-wrap');
|
||||
if (wrap) wrap.style.setProperty('--anim-trajanje', v + 's');
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
@@ -57,12 +57,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
||||
{{if not (or (eq .Nalog.Status "Završeno") (eq .Nalog.Status "Preuzeto"))}}
|
||||
<a href="/servis/{{.Nalog.ID}}/stampa" target="_blank" class="btn-sekundarno">
|
||||
Radni nalog
|
||||
</a>
|
||||
{{end}}
|
||||
{{if or (eq .Nalog.Status "Završeno") (eq .Nalog.Status "Preuzeto")}}
|
||||
<a href="/servis/{{.Nalog.ID}}/otpremnica" target="_blank" class="btn-sekundarno">
|
||||
Otpremnica
|
||||
</a>
|
||||
{{end}}
|
||||
<a href="/servis/izmeni/{{.Nalog.ID}}" class="btn-primarno">
|
||||
Izmeni nalog
|
||||
</a>
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
</script>
|
||||
{{end}}
|
||||
</head>
|
||||
<body{{if .LokalnaAnimacija}} data-animacija="{{.LokalnaAnimacija}}"{{end}}{{if .LokalniHover}} data-hover="{{.LokalniHover}}"{{end}}>
|
||||
<body{{if .LokalnaAnimacija}} data-animacija="{{.LokalnaAnimacija}}"{{end}}{{if .LokalniHover}} data-hover="{{.LokalniHover}}"{{end}}{{if .LokalnaBrzinaAnimacije}} data-brzina-animacije="{{.LokalnaBrzinaAnimacije}}"{{end}}>
|
||||
<div class="raspored">
|
||||
<div class="sidebar-overlay" id="sidebar-overlay"></div>
|
||||
{{template "sidebar" .}}
|
||||
@@ -307,6 +307,8 @@
|
||||
|
||||
function ntechInicijalizuj() {
|
||||
ntechKonvertujPoruke();
|
||||
|
||||
|
||||
var m = document.querySelector('meta[name="csrf-token"]');
|
||||
if (m && m.content) {
|
||||
document.querySelectorAll('form[method="POST"],form[method="post"]').forEach(function(f) {
|
||||
@@ -323,14 +325,64 @@
|
||||
e.preventDefault();
|
||||
window._ntechPotvrdi(el.getAttribute('data-potvrda')).then(function(ok) {
|
||||
if (!ok) return;
|
||||
// dugme unutar forme — submit forme
|
||||
var forma = el.closest('form');
|
||||
if (forma) { forma.submit(); return; }
|
||||
// link
|
||||
if (el.href) { window.location.href = el.href; }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// AJAX submit — forme koje serveru vraćaju ?sacuvano= ostaju na stranici (scroll se ne gubi)
|
||||
document.querySelectorAll('form[method="POST"],form[method="post"]').forEach(function(f) {
|
||||
if (f._ajaxSave) return;
|
||||
f._ajaxSave = true;
|
||||
f.addEventListener('submit', function(e) {
|
||||
// file upload i forme sa data-full-reload šalju se normalno
|
||||
if (f.enctype === 'multipart/form-data' || f.dataset.fullReload) return;
|
||||
e.preventDefault();
|
||||
// prikazujemo dugme kao zauzeto
|
||||
var btn = f.querySelector('[type="submit"]');
|
||||
if (btn) btn.disabled = true;
|
||||
// URLSearchParams šalje kao application/x-www-form-urlencoded
|
||||
// što Go-ov r.FormValue() (i CSRF middleware) može da pročita
|
||||
fetch(f.action || location.href, {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams(new FormData(f)),
|
||||
redirect: 'follow'
|
||||
}).then(function(res) {
|
||||
var finUrl = new URL(res.url);
|
||||
if (finUrl.search.indexOf('sacuvano') !== -1) {
|
||||
// uspeh — prikaži toast, ostani na stranici
|
||||
window.ntechToast('Sačuvano', 'uspeh');
|
||||
if (btn) btn.disabled = false;
|
||||
// odmah primeni podešavanja koja menjaju globalne atribute body-ja
|
||||
var anim = f.querySelector('[name="lokalna_animacija"]');
|
||||
if (anim) {
|
||||
if (anim.value) document.body.dataset.animacija = anim.value;
|
||||
else delete document.body.dataset.animacija;
|
||||
}
|
||||
var hov = f.querySelector('[name="lokalni_hover"]');
|
||||
if (hov) {
|
||||
if (hov.value) document.body.dataset.hover = hov.value;
|
||||
else delete document.body.dataset.hover;
|
||||
}
|
||||
var brzina = f.querySelector('[name="lokalna_brzina_animacije"]');
|
||||
if (brzina) {
|
||||
if (brzina.value) document.body.dataset.brzinaAnimacije = brzina.value;
|
||||
else delete document.body.dataset.brzinaAnimacije;
|
||||
}
|
||||
// promena teme zahteva reload (menja se ceo CSS fajl)
|
||||
if (f.querySelector('[name="lokalna_tema"]')) location.reload();
|
||||
} else {
|
||||
// greška ili redirect na drugu stranicu — navigiraj normalno
|
||||
location.href = res.url;
|
||||
}
|
||||
}).catch(function() {
|
||||
// mrežna greška — pošalji formu normalno
|
||||
f.submit();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
if (!window._ntechCsrfDodato) {
|
||||
window._ntechCsrfDodato = true;
|
||||
|
||||
Reference in New Issue
Block a user