Lots of changes to the website

This commit is contained in:
Blake Ridgway
2026-03-27 07:57:13 -05:00
parent 617624c179
commit 7e7480ecf9
33 changed files with 1539 additions and 184 deletions

View File

@@ -1,7 +1,6 @@
package handler
import (
"encoding/json"
"fmt"
"html/template"
"io"
@@ -12,6 +11,7 @@ import (
"time"
"ridgwaysystems.org/website/internal/blog"
"ridgwaysystems.org/website/internal/changelog"
"ridgwaysystems.org/website/internal/newsletter"
"ridgwaysystems.org/website/internal/status"
)
@@ -55,6 +55,26 @@ func (h *Handler) AdminRouter(w http.ResponseWriter, r *http.Request) {
h.requireAuth(h.adminStatusGet)(w, r)
}
case path == "/admin/changelog":
h.requireAuth(h.adminChangelogList)(w, r)
case path == "/admin/changelog/new":
if r.Method == http.MethodPost {
h.requireAuth(h.adminChangelogNewPost)(w, r)
} else {
h.requireAuth(h.adminChangelogNewGet)(w, r)
}
case strings.HasPrefix(path, "/admin/changelog/edit/"):
if r.Method == http.MethodPost {
h.requireAuth(h.adminChangelogEditPost)(w, r)
} else {
h.requireAuth(h.adminChangelogEditGet)(w, r)
}
case strings.HasPrefix(path, "/admin/changelog/delete/"):
h.requireAuth(h.adminChangelogDelete)(w, r)
case path == "/admin/preview":
h.requireAuth(h.adminPreview)(w, r)
@@ -208,7 +228,7 @@ func (h *Handler) adminDeletePost(w http.ResponseWriter, r *http.Request) {
// --- Status editor ---
type adminStatusData struct {
JSON string
Page *status.Page
Error string
Flash string
}
@@ -219,9 +239,7 @@ func (h *Handler) adminStatusGet(w http.ResponseWriter, r *http.Request) {
h.render(w, "admin-status", adminStatusData{Error: "Could not load status.json: " + err.Error()})
return
}
raw, _ := json.MarshalIndent(p, "", " ")
flash := r.URL.Query().Get("flash")
h.render(w, "admin-status", adminStatusData{JSON: string(raw), Flash: flash})
h.render(w, "admin-status", adminStatusData{Page: p, Flash: r.URL.Query().Get("flash")})
}
func (h *Handler) adminStatusPost(w http.ResponseWriter, r *http.Request) {
@@ -229,15 +247,19 @@ func (h *Handler) adminStatusPost(w http.ResponseWriter, r *http.Request) {
h.renderErr(w, http.StatusBadRequest, "Bad form data.")
return
}
raw := r.FormValue("json")
var p status.Page
if err := json.Unmarshal([]byte(raw), &p); err != nil {
h.render(w, "admin-status", adminStatusData{JSON: raw, Error: "Invalid JSON: " + err.Error()})
p, err := status.Load(filepath.Join(h.dataDir, "status.json"))
if err != nil {
h.render(w, "admin-status", adminStatusData{Error: "Could not load status: " + err.Error()})
return
}
p.LastChecked = time.Now().UTC()
if err := status.Save(filepath.Join(h.dataDir, "status.json"), &p); err != nil {
h.render(w, "admin-status", adminStatusData{JSON: raw, Error: "Save failed: " + err.Error()})
for i := range p.Services {
if s := r.FormValue(fmt.Sprintf("status_%d", i)); s != "" {
p.Services[i].Status = s
}
p.Services[i].Note = strings.TrimSpace(r.FormValue(fmt.Sprintf("note_%d", i)))
}
if err := status.Save(filepath.Join(h.dataDir, "status.json"), p); err != nil {
h.render(w, "admin-status", adminStatusData{Page: p, Error: "Save failed: " + err.Error()})
return
}
http.Redirect(w, r, "/admin/status?flash=Saved", http.StatusSeeOther)
@@ -416,6 +438,121 @@ func (h *Handler) adminNewsletterDelete(w http.ResponseWriter, r *http.Request)
http.Redirect(w, r, "/admin/newsletter?flash=Removed", http.StatusSeeOther)
}
// --- Changelog admin ---
type adminChangelogData struct {
Log *changelog.Log
Entry *changelog.Entry
Categories []string
Error string
Flash string
IsNew bool
}
func (h *Handler) adminChangelogList(w http.ResponseWriter, r *http.Request) {
l, err := changelog.Load(filepath.Join(h.dataDir, "changelog.json"))
if err != nil {
l = &changelog.Log{}
}
h.render(w, "admin-changelog", adminChangelogData{
Log: l,
Flash: r.URL.Query().Get("flash"),
})
}
func (h *Handler) adminChangelogNewGet(w http.ResponseWriter, r *http.Request) {
h.render(w, "admin-changelog-editor", adminChangelogData{
Categories: changelog.Categories,
IsNew: true,
Entry: &changelog.Entry{Date: time.Now().Format("2006-01-02")},
})
}
func (h *Handler) adminChangelogNewPost(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
h.renderErr(w, http.StatusBadRequest, "Bad form data.")
return
}
e := changelog.Entry{
Date: r.FormValue("date"),
Title: strings.TrimSpace(r.FormValue("title")),
Description: strings.TrimSpace(r.FormValue("description")),
Category: r.FormValue("category"),
}
if e.Title == "" || e.Date == "" {
h.render(w, "admin-changelog-editor", adminChangelogData{
Entry: &e, Categories: changelog.Categories, IsNew: true,
Error: "Date and title are required.",
})
return
}
if err := changelog.Add(filepath.Join(h.dataDir, "changelog.json"), e); err != nil {
h.render(w, "admin-changelog-editor", adminChangelogData{
Entry: &e, Categories: changelog.Categories, IsNew: true,
Error: "Failed to save: " + err.Error(),
})
return
}
http.Redirect(w, r, "/admin/changelog?flash=Entry+created", http.StatusSeeOther)
}
func (h *Handler) adminChangelogEditGet(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/admin/changelog/edit/")
e, err := changelog.Get(filepath.Join(h.dataDir, "changelog.json"), id)
if err != nil {
h.renderErr(w, http.StatusNotFound, "Entry not found.")
return
}
h.render(w, "admin-changelog-editor", adminChangelogData{
Entry: e,
Categories: changelog.Categories,
IsNew: false,
})
}
func (h *Handler) adminChangelogEditPost(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/admin/changelog/edit/")
if err := r.ParseForm(); err != nil {
h.renderErr(w, http.StatusBadRequest, "Bad form data.")
return
}
e := changelog.Entry{
ID: id,
Date: r.FormValue("date"),
Title: strings.TrimSpace(r.FormValue("title")),
Description: strings.TrimSpace(r.FormValue("description")),
Category: r.FormValue("category"),
}
if e.Title == "" || e.Date == "" {
h.render(w, "admin-changelog-editor", adminChangelogData{
Entry: &e, Categories: changelog.Categories,
Error: "Date and title are required.",
})
return
}
if err := changelog.Update(filepath.Join(h.dataDir, "changelog.json"), e); err != nil {
h.render(w, "admin-changelog-editor", adminChangelogData{
Entry: &e, Categories: changelog.Categories,
Error: "Failed to save: " + err.Error(),
})
return
}
http.Redirect(w, r, "/admin/changelog?flash=Entry+saved", http.StatusSeeOther)
}
func (h *Handler) adminChangelogDelete(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
h.renderErr(w, http.StatusMethodNotAllowed, "POST required.")
return
}
id := strings.TrimPrefix(r.URL.Path, "/admin/changelog/delete/")
if err := changelog.Delete(filepath.Join(h.dataDir, "changelog.json"), id); err != nil {
h.renderErr(w, http.StatusInternalServerError, "Delete failed: "+err.Error())
return
}
http.Redirect(w, r, "/admin/changelog?flash=Entry+deleted", http.StatusSeeOther)
}
// sanitizeSlug ensures a slug is filesystem-safe.
func sanitizeSlug(s string) string {
s = strings.ToLower(strings.TrimSpace(s))