package handlers import ( "log" "net/http" "strings" "time" "technical.kiwi/website/internal/contact" "technical.kiwi/website/internal/gallery" "technical.kiwi/website/internal/mail" "technical.kiwi/website/internal/middleware" "technical.kiwi/website/templates" ) // Server holds shared state for HTTP handlers. type Server struct { ImagesDir string StaticDir string Images []gallery.Image Hero gallery.Image HasHero bool Mail *mail.Config ContactEnabled bool } // New builds a Server after loading the image gallery. func New(imagesDir, staticDir string, mailCfg *mail.Config) (*Server, error) { images, err := gallery.List(imagesDir) if err != nil { return nil, err } hero, hasHero := gallery.SelectHero(images) if hasHero { if err := gallery.EnsurePriority(imagesDir, hero.RelPath); err != nil { log.Printf("hero image: %v", err) } else { images, err = gallery.List(imagesDir) if err != nil { return nil, err } hero, hasHero = gallery.SelectHero(images) } log.Printf("hero image: %s", hero.RelPath) } srv := &Server{ ImagesDir: imagesDir, StaticDir: staticDir, Images: images, Hero: hero, HasHero: hasHero, Mail: mailCfg, ContactEnabled: mailCfg != nil, } go func() { if err := gallery.EnsureThumbnails(imagesDir); err != nil { log.Printf("gallery thumbnails: %v", err) return } log.Print("gallery thumbnails: ready") }() return srv, nil } func (s *Server) index(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-cache") templates.Home(s.Images, s.Hero, s.HasHero, s.ContactEnabled).Render(r.Context(), w) } func (s *Server) contact(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } if strings.TrimSpace(r.FormValue("website")) != "" { templates.ContactSuccess().Render(r.Context(), w) return } sub, errs := contact.Parse( r.FormValue("name"), r.FormValue("email"), r.FormValue("message"), ) if errs.Any() { templates.ContactValidationAlert(errs).Render(r.Context(), w) return } if s.Mail == nil { templates.ContactSendError().Render(r.Context(), w) return } if err := s.Mail.ContactEmail(sub.Name, sub.Email, sub.Message); err != nil { log.Printf("contact mail: %v", err) templates.ContactSendError().Render(r.Context(), w) return } templates.ContactSuccess().Render(r.Context(), w) } func (s *Server) galleryPartial(w http.ResponseWriter, r *http.Request) { album := r.URL.Query().Get("album") collection := r.URL.Query().Get("collection") filtered := s.filterImages(album, collection) templates.GalleryGrid(filtered).Render(r.Context(), w) } func (s *Server) imageModal(w http.ResponseWriter, r *http.Request) { rel := r.PathValue("path") if !gallery.SafeRelPath(rel) { http.NotFound(w, r) return } img, ok := s.findImage(rel) if !ok { http.NotFound(w, r) return } idx := s.indexOf(rel) prev, next := "", "" if idx > 0 { prev = s.Images[idx-1].RelPath } if idx >= 0 && idx < len(s.Images)-1 { next = s.Images[idx+1].RelPath } templates.ImageModal(img, prev, next).Render(r.Context(), w) } func (s *Server) filterImages(album, collection string) []gallery.Image { if album == "" && collection == "" { return s.Images } var out []gallery.Image for _, img := range s.Images { if album != "" && img.Album != album { continue } if collection != "" && img.CollectionKey != collection { continue } out = append(out, img) } return out } func (s *Server) findImage(rel string) (gallery.Image, bool) { for _, img := range s.Images { if img.RelPath == rel { return img, true } } return gallery.Image{}, false } func (s *Server) indexOf(rel string) int { for i, img := range s.Images { if img.RelPath == rel { return i } } return -1 } // RegisterRoutes attaches handlers to mux. func (s *Server) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /{$}", s.index) mux.HandleFunc("POST /contact", s.contact) mux.HandleFunc("GET /gallery", s.galleryPartial) mux.HandleFunc("GET /gallery/{path...}", s.imageModal) staticHandler := middleware.CacheStatic(s.StaticDir, 7*24*time.Hour) mux.Handle("GET /static/", http.StripPrefix("/static/", staticHandler)) mux.Handle("GET /images/", http.StripPrefix("/images/", middleware.CacheImages(s.ImagesDir))) }