Single-page site with gallery by album and event, contact form over SMTP, Docker dev/prod setup, and on-server image derivatives. Gallery photos stay local (app/images/ is gitignored). Co-authored-by: Cursor <cursoragent@cursor.com>
184 lines
4.5 KiB
Go
184 lines
4.5 KiB
Go
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)))
|
|
}
|