diff --git a/assets.go b/assets.go new file mode 100644 index 0000000..2927ada --- /dev/null +++ b/assets.go @@ -0,0 +1,12 @@ +package assets + +import "embed" + +//go:embed migrations +var MigracijeFS embed.FS + +//go:embed web/static/css +var StaticFS embed.FS + +//go:embed web/templates +var TemplatesFS embed.FS diff --git a/cmd/ntech/main.go b/cmd/ntech/main.go index 1771678..5ff6896 100644 --- a/cmd/ntech/main.go +++ b/cmd/ntech/main.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "io/fs" "log" "net/http" "os" @@ -11,6 +12,7 @@ import ( "sort" "time" + "ntech" "ntech/internal/auth" "ntech/internal/config" "ntech/internal/db/sqlite" @@ -30,7 +32,7 @@ func main() { auth.InitAuthLog() if config.JelPrvoPokretanje() { - config.PokreniSetup() + config.PokreniSetup(assets.TemplatesFS) return } @@ -50,7 +52,7 @@ func main() { } defer db.Close() - if err := sqlite.PokreniMigracije(db, "migrations"); err != nil { + if err := sqlite.PokreniMigracije(db, assets.MigracijeFS); err != nil { log.Fatalf("Greška pri migracijama: %v", err) } log.Println("Migracije uspešno izvršene") @@ -75,7 +77,7 @@ func main() { h.Verzija = Verzija if os.Getenv("NTECH_ENV") == "production" { - kes, err := handler.KreirajKes() + kes, err := handler.KreirajKes(assets.TemplatesFS) if err != nil { log.Fatalf("Greška pri kreiranju keša šablona: %v", err) } @@ -88,8 +90,11 @@ func main() { r.Use(ntechmw.CsrfMiddleware) r.Use(middleware.Compress(5)) - // statični fajlovi - r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static")))) + // uploads se servira sa diska (runtime fajlovi) + r.Handle("/static/uploads/*", http.StripPrefix("/static/uploads/", http.FileServer(http.Dir("web/static/uploads")))) + // ostali statični fajlovi (CSS) iz embed FS-a + staticSub, _ := fs.Sub(assets.StaticFS, "web/static") + r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.FS(staticSub)))) // javne rute (bez autentifikacije) r.Get("/prijava", h.PrikazPrijave) diff --git a/internal/config/setup.go b/internal/config/setup.go index 0a3232e..f1e281d 100644 --- a/internal/config/setup.go +++ b/internal/config/setup.go @@ -3,9 +3,9 @@ package config import ( "encoding/json" "fmt" + "io/fs" "log" "net/http" - "os" "strings" "github.com/go-chi/chi/v5" @@ -13,7 +13,7 @@ import ( ) // PokreniSetup pokreće WebView prozor za prvo podešavanje -func PokreniSetup() { +func PokreniSetup(fsys fs.FS) { port := NadjiSlobodanPort() if port == 0 { log.Fatal("Greška: nije pronađen nijedan slobodan port") @@ -30,7 +30,7 @@ func PokreniSetup() { http.Error(w, "Greška", http.StatusInternalServerError) return } - sadrzaj, err := os.ReadFile("web/templates/setup/index.html") + sadrzaj, err := fs.ReadFile(fsys, "web/templates/setup/index.html") if err != nil { http.Error(w, "Greška", http.StatusInternalServerError) return diff --git a/internal/db/sqlite/migracije.go b/internal/db/sqlite/migracije.go index 4532f02..e7b352f 100644 --- a/internal/db/sqlite/migracije.go +++ b/internal/db/sqlite/migracije.go @@ -3,8 +3,8 @@ package sqlite import ( "database/sql" "fmt" - "os" - "path/filepath" + "io/fs" + "path" "sort" _ "modernc.org/sqlite" @@ -32,8 +32,8 @@ func OtvoriDB(putanja string) (*sql.DB, error) { return db, nil } -// PokreniMigracije izvršava sve SQL fajlove iz foldera koji još nisu izvršeni -func PokreniMigracije(db *sql.DB, folder string) error { +// PokreniMigracije izvršava sve SQL fajlove iz fs.FS koji još nisu izvršeni +func PokreniMigracije(db *sql.DB, fsys fs.FS) error { // kreiramo tabelu za praćenje migracija ako ne postoji _, err := db.Exec(` CREATE TABLE IF NOT EXISTS migracije ( @@ -46,7 +46,7 @@ func PokreniMigracije(db *sql.DB, folder string) error { } // čitamo sve .sql fajlove iz foldera - unosi, err := os.ReadDir(folder) + unosi, err := fs.ReadDir(fsys, "migrations") if err != nil { return fmt.Errorf("ntech: PokreniMigracije: čitanje foldera: %w", err) } @@ -57,7 +57,7 @@ func PokreniMigracije(db *sql.DB, folder string) error { }) for _, unos := range unosi { - if filepath.Ext(unos.Name()) != ".sql" { + if path.Ext(unos.Name()) != ".sql" { continue } @@ -74,8 +74,8 @@ func PokreniMigracije(db *sql.DB, folder string) error { } // čitamo sadržaj SQL fajla - putanja := filepath.Join(folder, naziv) - sadrzaj, err := os.ReadFile(putanja) + putanja := "migrations/" + naziv + sadrzaj, err := fs.ReadFile(fsys, putanja) if err != nil { return fmt.Errorf("ntech: PokreniMigracije: čitanje %s: %w", naziv, err) } diff --git a/internal/handler/handler.go b/internal/handler/handler.go index e874056..bacbb46 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -3,12 +3,13 @@ package handler import ( "database/sql" "html/template" + "io/fs" + "net/http" "ntech/internal/db" "ntech/internal/db/sqlite" "ntech/internal/middleware" "ntech/internal/model" - "net/http" ) // Handler drži zavisnosti koje su potrebne svim handlerima @@ -26,8 +27,9 @@ type Handler struct { PodsetniciFRepo db.PodsetnikRepository PokusajiRepo db.PokusajiPrijaveRepository LoginIstorijsaRepo db.LoginIstorijsaRepository - Verzija string - Templates map[string]*template.Template + Verzija string + Templates map[string]*template.Template + TemplatesFS fs.FS } // Novi kreira novi Handler sa datom bazom diff --git a/internal/handler/kes.go b/internal/handler/kes.go index bedd579..ea52ee1 100644 --- a/internal/handler/kes.go +++ b/internal/handler/kes.go @@ -3,6 +3,7 @@ package handler import ( "fmt" "html/template" + "io/fs" "log" "net/http" ) @@ -34,15 +35,15 @@ var standaloneIme = []string{ "prijava", "setup", "totp_provera", "prodaja_stampa", } -// KreirajKes parsuje sve šablone i vraća ih keširane u mapi -func KreirajKes() (map[string]*template.Template, error) { +// KreirajKes parsuje sve šablone iz fsys i vraća ih keširane u mapi +func KreirajKes(fsys fs.FS) (map[string]*template.Template, error) { kes := make(map[string]*template.Template) for _, ime := range saSidebar { fajlovi := make([]string, len(bazniSabloni), len(bazniSabloni)+1) copy(fajlovi, bazniSabloni) fajlovi = append(fajlovi, "web/templates/stranice/"+ime+".html") - t, err := template.ParseFiles(fajlovi...) + t, err := template.ParseFS(fsys, fajlovi...) if err != nil { return nil, fmt.Errorf("kes: %s: %w", ime, err) } @@ -50,7 +51,7 @@ func KreirajKes() (map[string]*template.Template, error) { } for _, ime := range standaloneIme { - t, err := template.ParseFiles("web/templates/stranice/" + ime + ".html") + t, err := template.ParseFS(fsys, "web/templates/stranice/"+ime+".html") if err != nil { return nil, fmt.Errorf("kes: %s: %w", ime, err) } diff --git a/web/static/css/main.css b/web/static/css/main.css index 4735b29..f8f302f 100644 --- a/web/static/css/main.css +++ b/web/static/css/main.css @@ -31,6 +31,7 @@ body { .sidebar.skupljen { width: 60px; + overflow: hidden; } /* vrh sidebara — logo zona */ @@ -131,7 +132,7 @@ body { white-space: nowrap; position: relative; text-decoration: none; - transition: background 0.2s, color 0.2s; + transition: background 0.2s, color 0.2s, transform 0.15s ease; } .nav-stavka:hover { @@ -139,6 +140,14 @@ body { color: var(--tekst-jak); } +.sidebar:not(.skupljen) .nav-stavka:hover { + transform: scale(1.03); +} + +.sidebar.skupljen .nav-stavka:hover svg { + transform: scale(1.15); +} + .nav-stavka.aktivan { background: var(--sb-aktivan); color: var(--tekst-jak); @@ -159,6 +168,7 @@ body { flex-shrink: 0; width: 20px; height: 20px; + transition: transform 0.15s ease; } .nav-stavka span { @@ -437,6 +447,29 @@ select { } } +/* animacije */ +@keyframes fadeInUp { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes slideDown { + from { opacity: 0; transform: translateY(-10px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 20% { transform: translateX(-6px); } + 40% { transform: translateX(6px); } + 60% { transform: translateX(-4px); } + 80% { transform: translateX(4px); } +} + +.animiraj { + animation: fadeInUp 0.4s ease both; +} + /* gornja traka magacina — responsive */ .magacin-traka { display: flex; diff --git a/web/templates/stranice/admin_korisnici.html b/web/templates/stranice/admin_korisnici.html index c5e1807..a7e708e 100644 --- a/web/templates/stranice/admin_korisnici.html +++ b/web/templates/stranice/admin_korisnici.html @@ -8,17 +8,8 @@ from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } - @keyframes fadeInUp { - from { opacity: 0; transform: translateY(8px); } - to { opacity: 1; transform: translateY(0); } - } - .poruka-animacija { animation: slideDown 0.3s ease forwards; } - .korisnici-tabela tbody tr { - animation: fadeInUp 0.25s ease forwards; - opacity: 0; - } .korisnici-tabela tbody tr:nth-child(1) { animation-delay: 0.04s; } .korisnici-tabela tbody tr:nth-child(2) { animation-delay: 0.08s; } .korisnici-tabela tbody tr:nth-child(3) { animation-delay: 0.12s; } @@ -31,8 +22,6 @@ .korisnici-tabela tbody tr:nth-child(10) { animation-delay: 0.40s; } .nova-forma-kartica { - animation: fadeInUp 0.25s ease forwards; - opacity: 0; animation-delay: 0.30s; } @@ -52,7 +41,7 @@ {{end}} -