2026-05-16 23:03:50 +00:00
|
|
|
package app
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"net/http"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"archi_folio/internal/store"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func (s *Server) adminUpdateContent(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if err := parseAdminForm(w, r, maxFormBytes); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "main", "content form failed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
current, err := s.store.SiteContent(r.Context())
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "main", "content could not be loaded")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
content := store.SiteContent{
|
2026-05-17 12:36:50 +00:00
|
|
|
HeroTitle: formValueOr(r, "hero_title", current.HeroTitle),
|
|
|
|
|
HeroSubtitle: formValueOr(r, "hero_subtitle", current.HeroSubtitle),
|
|
|
|
|
Positioning: formValueOr(r, "positioning", current.Positioning),
|
|
|
|
|
HeroCTALabel: formValueOr(r, "hero_cta_label", current.HeroCTALabel),
|
|
|
|
|
HeroCTAURL: formValueOr(r, "hero_cta_url", current.HeroCTAURL),
|
|
|
|
|
SecondaryCTALabel: formValueOr(r, "secondary_cta_label", current.SecondaryCTALabel),
|
|
|
|
|
SecondaryCTAURL: formValueOr(r, "secondary_cta_url", current.SecondaryCTAURL),
|
|
|
|
|
IntroTitle: formValueOr(r, "intro_title", current.IntroTitle),
|
|
|
|
|
IntroText: formValueOr(r, "intro_text", current.IntroText),
|
|
|
|
|
ServiceOneTitle: formValueOr(r, "service_one_title", current.ServiceOneTitle),
|
|
|
|
|
ServiceOneText: formValueOr(r, "service_one_text", current.ServiceOneText),
|
|
|
|
|
ServiceTwoTitle: formValueOr(r, "service_two_title", current.ServiceTwoTitle),
|
|
|
|
|
ServiceTwoText: formValueOr(r, "service_two_text", current.ServiceTwoText),
|
|
|
|
|
ServiceThreeTitle: formValueOr(r, "service_three_title", current.ServiceThreeTitle),
|
|
|
|
|
ServiceThreeText: formValueOr(r, "service_three_text", current.ServiceThreeText),
|
|
|
|
|
ProcessOneTitle: formValueOr(r, "process_one_title", current.ProcessOneTitle),
|
|
|
|
|
ProcessOneText: formValueOr(r, "process_one_text", current.ProcessOneText),
|
|
|
|
|
ProcessTwoTitle: formValueOr(r, "process_two_title", current.ProcessTwoTitle),
|
|
|
|
|
ProcessTwoText: formValueOr(r, "process_two_text", current.ProcessTwoText),
|
|
|
|
|
ProcessThreeTitle: formValueOr(r, "process_three_title", current.ProcessThreeTitle),
|
|
|
|
|
ProcessThreeText: formValueOr(r, "process_three_text", current.ProcessThreeText),
|
|
|
|
|
AboutName: formValueOr(r, "about_name", current.AboutName),
|
|
|
|
|
AboutRole: formValueOr(r, "about_role", current.AboutRole),
|
|
|
|
|
AboutBio: formValueOr(r, "about_bio", current.AboutBio),
|
|
|
|
|
StudioPhilosophy: formValueOr(r, "studio_philosophy", current.StudioPhilosophy),
|
|
|
|
|
StudioApproach: formValueOr(r, "studio_approach", current.StudioApproach),
|
|
|
|
|
StudioCredentials: formValueOr(r, "studio_credentials", current.StudioCredentials),
|
|
|
|
|
ServiceArea: formValueOr(r, "service_area", current.ServiceArea),
|
|
|
|
|
Email: current.Email,
|
|
|
|
|
Phone: current.Phone,
|
|
|
|
|
Location: current.Location,
|
|
|
|
|
HeroImage: formValueOr(r, "hero_image_current", current.HeroImage),
|
|
|
|
|
AboutImage: formValueOr(r, "about_image_current", current.AboutImage),
|
2026-05-16 23:03:50 +00:00
|
|
|
}
|
|
|
|
|
if err := validateContent(content); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "main", err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if path, ok, err := s.saveUpload(r, "hero_image"); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "main", "hero image "+err.Error())
|
|
|
|
|
return
|
|
|
|
|
} else if ok {
|
|
|
|
|
content.HeroImage = path
|
|
|
|
|
}
|
|
|
|
|
if path, ok, err := s.saveUpload(r, "about_image"); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "main", "about image "+err.Error())
|
|
|
|
|
return
|
|
|
|
|
} else if ok {
|
|
|
|
|
content.AboutImage = path
|
|
|
|
|
}
|
|
|
|
|
if err := s.store.UpdateSiteContent(r.Context(), content); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "main", "content could not be saved")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
s.redirectAdmin(w, r, "main", "content saved")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) adminUpdateContactDetails(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "contact-details", "contact details form failed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
content, err := s.store.SiteContent(r.Context())
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "contact-details", "contact details could not be loaded")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
content.Email = strings.TrimSpace(r.FormValue("email"))
|
|
|
|
|
content.Phone = strings.TrimSpace(r.FormValue("phone"))
|
|
|
|
|
content.Location = strings.TrimSpace(r.FormValue("location"))
|
|
|
|
|
if err := validateContactDetails(content); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "contact-details", err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := s.store.UpdateSiteContent(r.Context(), content); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "contact-details", "contact details could not be saved")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
s.redirectAdmin(w, r, "contact-details", "contact details saved")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) adminCreateProject(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if err := parseAdminForm(w, r, maxFormBytes); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "projects", "project form failed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
cover := "/static/placeholders/project-1.svg"
|
|
|
|
|
if path, ok, err := s.saveUpload(r, "cover_image"); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "projects", "cover upload "+err.Error())
|
|
|
|
|
return
|
|
|
|
|
} else if ok {
|
|
|
|
|
cover = path
|
|
|
|
|
}
|
|
|
|
|
slug := strings.TrimSpace(r.FormValue("slug"))
|
|
|
|
|
if slug == "" {
|
|
|
|
|
slug = store.SlugFromTitle(r.FormValue("title"))
|
|
|
|
|
} else {
|
|
|
|
|
slug = store.SlugFromTitle(slug)
|
|
|
|
|
}
|
|
|
|
|
project := store.Project{
|
|
|
|
|
Slug: slug,
|
|
|
|
|
Title: strings.TrimSpace(r.FormValue("title")),
|
|
|
|
|
Location: strings.TrimSpace(r.FormValue("location")),
|
|
|
|
|
Year: strings.TrimSpace(r.FormValue("year")),
|
|
|
|
|
Category: strings.TrimSpace(r.FormValue("category")),
|
2026-05-17 12:36:50 +00:00
|
|
|
Summary: strings.TrimSpace(r.FormValue("summary")),
|
|
|
|
|
Scope: strings.TrimSpace(r.FormValue("scope")),
|
|
|
|
|
Status: strings.TrimSpace(r.FormValue("status")),
|
|
|
|
|
Position: formInt(r, "position"),
|
2026-05-16 23:03:50 +00:00
|
|
|
Description: strings.TrimSpace(r.FormValue("description")),
|
|
|
|
|
CoverImage: cover,
|
|
|
|
|
Featured: r.FormValue("featured") == "on",
|
|
|
|
|
}
|
|
|
|
|
if err := validateProject(project); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "projects", err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
id, err := s.store.CreateProject(r.Context(), project)
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "projects", "project could not be created")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
_ = s.store.AddProjectImage(r.Context(), id, cover, r.FormValue("title"))
|
|
|
|
|
s.redirectAdmin(w, r, "projects", "project created")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) adminUpdateProject(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := parseAdminForm(w, r, maxFormBytes); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "projects", "project form failed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
cover := r.FormValue("cover_image_current")
|
|
|
|
|
if path, ok, err := s.saveUpload(r, "cover_image"); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "projects", "cover upload "+err.Error())
|
|
|
|
|
return
|
|
|
|
|
} else if ok {
|
|
|
|
|
cover = path
|
|
|
|
|
}
|
|
|
|
|
slug := strings.TrimSpace(r.FormValue("slug"))
|
|
|
|
|
if slug == "" {
|
|
|
|
|
slug = store.SlugFromTitle(r.FormValue("title"))
|
|
|
|
|
} else {
|
|
|
|
|
slug = store.SlugFromTitle(slug)
|
|
|
|
|
}
|
|
|
|
|
project := store.Project{
|
|
|
|
|
ID: id,
|
|
|
|
|
Slug: slug,
|
|
|
|
|
Title: strings.TrimSpace(r.FormValue("title")),
|
|
|
|
|
Location: strings.TrimSpace(r.FormValue("location")),
|
|
|
|
|
Year: strings.TrimSpace(r.FormValue("year")),
|
|
|
|
|
Category: strings.TrimSpace(r.FormValue("category")),
|
2026-05-17 12:36:50 +00:00
|
|
|
Summary: strings.TrimSpace(r.FormValue("summary")),
|
|
|
|
|
Scope: strings.TrimSpace(r.FormValue("scope")),
|
|
|
|
|
Status: strings.TrimSpace(r.FormValue("status")),
|
|
|
|
|
Position: formInt(r, "position"),
|
2026-05-16 23:03:50 +00:00
|
|
|
Description: strings.TrimSpace(r.FormValue("description")),
|
|
|
|
|
CoverImage: cover,
|
|
|
|
|
Featured: r.FormValue("featured") == "on",
|
|
|
|
|
}
|
|
|
|
|
if err := validateProject(project); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "projects", err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
err = s.store.UpdateProject(r.Context(), project)
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "projects", "project could not be saved")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
s.redirectAdmin(w, r, "projects", "project saved")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) adminDeleteProject(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
|
|
|
|
if err == nil {
|
|
|
|
|
err = s.store.DeleteProject(r.Context(), id)
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "projects", "project could not be deleted")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
s.redirectAdmin(w, r, "projects", "project deleted")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) adminAddProjectImage(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
projectID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := parseAdminForm(w, r, maxFormBytes); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "projects", "image form failed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
path, ok, err := s.saveUpload(r, "image")
|
|
|
|
|
if err != nil || !ok {
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "projects", "image upload "+err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
s.redirectAdmin(w, r, "projects", "image upload failed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := s.store.AddProjectImage(r.Context(), projectID, path, r.FormValue("caption")); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "projects", "image could not be added")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
s.redirectAdmin(w, r, "projects", "image added")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) adminDeleteProjectImage(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
|
|
|
|
if err == nil {
|
|
|
|
|
err = s.store.DeleteProjectImage(r.Context(), id)
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "projects", "image could not be deleted")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
s.redirectAdmin(w, r, "projects", "image deleted")
|
|
|
|
|
}
|
2026-05-17 12:36:50 +00:00
|
|
|
|
|
|
|
|
func (s *Server) adminCreateService(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "services", "service form failed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
service := serviceFromForm(r, 0)
|
|
|
|
|
if err := validateService(service); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "services", err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := s.store.CreateService(r.Context(), service); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "services", "service could not be created")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
s.redirectAdmin(w, r, "services", "service created")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) adminUpdateService(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "services", "service form failed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
service := serviceFromForm(r, id)
|
|
|
|
|
if err := validateService(service); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "services", err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := s.store.UpdateService(r.Context(), service); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "services", "service could not be saved")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
s.redirectAdmin(w, r, "services", "service saved")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) adminDeleteService(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
|
|
|
|
if err == nil {
|
|
|
|
|
err = s.store.DeleteService(r.Context(), id)
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "services", "service could not be deleted")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
s.redirectAdmin(w, r, "services", "service deleted")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) adminCreateFAQ(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "services", "FAQ form failed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
faq := faqFromForm(r, 0)
|
|
|
|
|
if err := validateFAQ(faq); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "services", err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := s.store.CreateFAQ(r.Context(), faq); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "services", "FAQ could not be created")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
s.redirectAdmin(w, r, "services", "FAQ created")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) adminUpdateFAQ(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "services", "FAQ form failed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
faq := faqFromForm(r, id)
|
|
|
|
|
if err := validateFAQ(faq); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "services", err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := s.store.UpdateFAQ(r.Context(), faq); err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "services", "FAQ could not be saved")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
s.redirectAdmin(w, r, "services", "FAQ saved")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) adminDeleteFAQ(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
|
|
|
|
if err == nil {
|
|
|
|
|
err = s.store.DeleteFAQ(r.Context(), id)
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.redirectAdmin(w, r, "services", "FAQ could not be deleted")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
s.redirectAdmin(w, r, "services", "FAQ deleted")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func serviceFromForm(r *http.Request, id int64) store.Service {
|
|
|
|
|
return store.Service{
|
|
|
|
|
ID: id,
|
|
|
|
|
Title: strings.TrimSpace(r.FormValue("title")),
|
|
|
|
|
Summary: strings.TrimSpace(r.FormValue("summary")),
|
|
|
|
|
Details: strings.TrimSpace(r.FormValue("details")),
|
|
|
|
|
Position: formInt(r, "position"),
|
|
|
|
|
Active: r.FormValue("active") == "on",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func faqFromForm(r *http.Request, id int64) store.FAQ {
|
|
|
|
|
return store.FAQ{
|
|
|
|
|
ID: id,
|
|
|
|
|
Question: strings.TrimSpace(r.FormValue("question")),
|
|
|
|
|
Answer: strings.TrimSpace(r.FormValue("answer")),
|
|
|
|
|
Position: formInt(r, "position"),
|
|
|
|
|
Active: r.FormValue("active") == "on",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func formInt(r *http.Request, name string) int {
|
|
|
|
|
value, _ := strconv.Atoi(strings.TrimSpace(r.FormValue(name)))
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func formValueOr(r *http.Request, name, fallback string) string {
|
|
|
|
|
if _, ok := r.Form[name]; !ok {
|
|
|
|
|
return fallback
|
|
|
|
|
}
|
|
|
|
|
return strings.TrimSpace(r.FormValue(name))
|
|
|
|
|
}
|