hibiscus/routes.go

272 lines
8.2 KiB
Go
Raw Normal View History

package main
import (
2024-05-18 16:15:20 +03:00
"embed"
"errors"
"github.com/go-chi/chi/v5"
"html/template"
"log/slog"
"net/http"
2024-05-08 16:49:34 +03:00
"net/url"
"os"
2024-03-30 14:22:03 +03:00
"strings"
"time"
)
2024-03-30 13:51:29 +03:00
type EntryList struct {
2024-04-23 14:13:45 +03:00
Title string
2024-05-08 16:49:34 +03:00
Description template.HTML
2024-04-23 14:13:45 +03:00
Entries []Entry
}
2024-03-30 13:51:29 +03:00
type Entry struct {
Title string
Content string
Link string
}
2024-04-23 14:13:45 +03:00
type formatEntries func([]string) []Entry
// Public contains the static files e.g. CSS, JS
//
2024-05-18 16:15:20 +03:00
//go:embed public
var Public embed.FS
// Pages contains the HTML templates used by the app
//
2024-05-18 16:15:20 +03:00
//go:embed pages
var Pages embed.FS
// EmbeddedPage returns contents of a file in Pages while "handling" potential errors
func EmbeddedPage(name string) []byte {
2024-05-18 16:15:20 +03:00
data, err := Pages.ReadFile(name)
if err != nil {
slog.Error("error reading embedded file", "err", err)
2024-05-18 16:15:20 +03:00
}
return data
}
2024-05-07 13:27:11 +03:00
var templateFuncs = map[string]interface{}{
"translatableText": TranslatableText,
2024-05-08 15:56:11 +03:00
"info": func() AppInfo { return Info },
"config": func() Config { return Cfg },
}
2024-05-18 16:15:20 +03:00
var editTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFS(Pages, "pages/base.html", "pages/edit.html"))
var viewTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFS(Pages, "pages/base.html", "pages/entry.html"))
var listTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFS(Pages, "pages/base.html", "pages/list.html"))
2024-05-03 15:40:40 +03:00
// NotFound returns a user-friendly 404 error page
func NotFound(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404)
HandleWrite(w.Write(EmbeddedPage("pages/error/404.html")))
}
// InternalError returns a user-friendly 500 error page
func InternalError(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(500)
HandleWrite(w.Write(EmbeddedPage("pages/error/500.html")))
}
2024-03-30 14:22:03 +03:00
// GetToday renders HTML page for today's entry
func GetToday(w http.ResponseWriter, r *http.Request) {
day, err := ReadToday()
if err != nil {
if errors.Is(err, os.ErrNotExist) {
day = []byte("")
} else {
slog.Error("error reading today's file", "error", err)
InternalError(w, r)
return
}
}
2024-05-03 15:40:40 +03:00
err = editTemplate.ExecuteTemplate(w, "base", Entry{Title: TranslatableText("title.today"), Content: string(day)})
if err != nil {
2024-05-03 15:40:40 +03:00
slog.Error("error executing template", "error", err)
InternalError(w, r)
return
}
}
2024-03-28 10:42:52 +03:00
// PostToday saves today's entry from form and redirects back to GET
func PostToday(w http.ResponseWriter, r *http.Request) {
2024-03-30 13:51:29 +03:00
err := SaveToday([]byte(r.FormValue("text")))
if err != nil {
slog.Error("error saving today's file", "error", err)
}
http.Redirect(w, r, r.Header.Get("Referer"), 302)
}
2024-04-23 14:13:45 +03:00
// GetEntries is a generic HTML renderer for a list
2024-05-08 16:49:34 +03:00
func GetEntries(w http.ResponseWriter, r *http.Request, title string, description template.HTML, dir string, format formatEntries) {
2024-04-23 14:13:45 +03:00
filesList, err := ListFiles(dir)
if err != nil {
2024-04-23 14:13:45 +03:00
slog.Error("error reading file list", "directory", dir, "error", err)
InternalError(w, r)
return
}
2024-04-23 14:13:45 +03:00
var filesFormatted = format(filesList)
2024-05-03 15:40:40 +03:00
err = listTemplate.ExecuteTemplate(w, "base", EntryList{Title: title, Description: description, Entries: filesFormatted})
if err != nil {
2024-03-30 14:22:03 +03:00
slog.Error("error executing template", "error", err)
InternalError(w, r)
return
}
}
2024-04-23 14:13:45 +03:00
// GetDays renders HTML list of previous days' entries
func GetDays(w http.ResponseWriter, r *http.Request) {
2024-05-18 16:15:20 +03:00
description := template.HTML(
"<a href=\"#footer\">" + TranslatableText("prompt.days") + "</a>")
GetEntries(w, r, TranslatableText("title.days"), description, "day", func(files []string) []Entry {
2024-04-23 14:13:45 +03:00
var filesFormatted []Entry
for i := range files {
v := files[len(files)-1-i] // This is suboptimal, but reverse order is better here
dayString := v
t, err := time.Parse(time.DateOnly, v)
if err == nil {
dayString = t.Format("02 Jan 2006")
}
2024-05-05 13:06:20 +03:00
// Fancy text for today and tomorrow
// This looks bad, but strings.Title is deprecated, and I'm not importing a golang.org/x package for this...
2024-05-07 13:27:11 +03:00
// (chances we ever run into tomorrow are really low)
2024-05-05 13:06:20 +03:00
if v == TodayDate() {
2024-05-03 15:40:40 +03:00
dayString = TranslatableText("link.today")
dayString = strings.ToTitle(string([]rune(dayString)[0])) + string([]rune(dayString)[1:])
2024-05-09 23:44:37 +03:00
} else if v > TodayDate() {
2024-05-05 13:06:20 +03:00
dayString = TranslatableText("link.tomorrow")
dayString = strings.ToTitle(string([]rune(dayString)[0])) + string([]rune(dayString)[1:])
2024-04-23 14:13:45 +03:00
}
filesFormatted = append(filesFormatted, Entry{Title: dayString, Link: "day/" + v})
}
return filesFormatted
})
}
// GetNotes renders HTML list of all notes
func GetNotes(w http.ResponseWriter, r *http.Request) {
2024-05-08 16:49:34 +03:00
// This is suboptimal, but will do...
description := template.HTML(
"<a href=\"#\" onclick='newNote(\"" + TranslatableText("prompt.notes") + "\")'>" + TranslatableText("button.notes") + "</a>" +
" <noscript>(" + template.HTMLEscapeString(TranslatableText("noscript.notes")) + ")</noscript>")
GetEntries(w, r, TranslatableText("title.notes"), description, "notes", func(files []string) []Entry {
2024-04-23 14:13:45 +03:00
var filesFormatted []Entry
for _, v := range files {
titleString := strings.Replace(v, "-", " ", -1) // FIXME: what if I need a hyphen?
filesFormatted = append(filesFormatted, Entry{Title: titleString, Link: "notes/" + v})
}
return filesFormatted
})
}
2024-05-07 13:27:11 +03:00
// GetEntry handles showing a single file, editable or otherwise
func GetEntry(w http.ResponseWriter, r *http.Request, title string, filename string, editable bool) {
entry, err := ReadFile(filename)
if err != nil {
if editable && errors.Is(err, os.ErrNotExist) {
entry = []byte("")
} else {
slog.Error("error reading entry file", "error", err, "file", filename)
InternalError(w, r)
return
}
}
2024-05-03 15:40:40 +03:00
if editable {
err = editTemplate.ExecuteTemplate(w, "base", Entry{Title: title, Content: string(entry)})
} else {
err = viewTemplate.ExecuteTemplate(w, "base", Entry{Title: title, Content: string(entry)})
}
if err != nil {
InternalError(w, r)
return
}
}
// GetDay renders HTML page for a specific day entry
func GetDay(w http.ResponseWriter, r *http.Request) {
dayString := chi.URLParam(r, "day")
if dayString == "" {
w.WriteHeader(http.StatusBadRequest)
HandleWrite(w.Write([]byte("day not specified")))
return
}
2024-05-05 13:06:20 +03:00
if dayString == TodayDate() { // Today can still be edited
http.Redirect(w, r, "/", 302)
return
}
title := dayString
t, err := time.Parse(time.DateOnly, dayString)
if err == nil { // This is low priority so silently fail
title = t.Format("02 Jan 2006")
}
2024-05-09 23:44:37 +03:00
GetEntry(w, r, title, DataFile("day/"+dayString), false)
}
2024-03-30 14:22:03 +03:00
// GetNote renders HTML page for a note
func GetNote(w http.ResponseWriter, r *http.Request) {
noteString := chi.URLParam(r, "note")
if noteString == "" {
w.WriteHeader(http.StatusBadRequest)
HandleWrite(w.Write([]byte("note not specified")))
return
}
2024-05-08 16:49:34 +03:00
// Handle non-latin note names
if decodedNote, err := url.QueryUnescape(noteString); err == nil {
noteString = decodedNote
}
2024-03-30 14:22:03 +03:00
2024-05-09 23:44:37 +03:00
GetEntry(w, r, noteString, DataFile("notes/"+noteString), true)
2024-03-30 14:22:03 +03:00
}
// PostNote saves a note form and redirects back to GET
func PostNote(w http.ResponseWriter, r *http.Request) {
noteString := chi.URLParam(r, "note")
if noteString == "" {
w.WriteHeader(http.StatusBadRequest)
HandleWrite(w.Write([]byte("note not specified")))
return
}
2024-05-09 23:44:37 +03:00
err := SaveFile(DataFile("notes/"+noteString), []byte(r.FormValue("text")))
2024-03-30 14:22:03 +03:00
if err != nil {
slog.Error("error saving a note", "note", noteString, "error", err)
}
http.Redirect(w, r, r.Header.Get("Referer"), 302)
}
2024-05-07 13:27:11 +03:00
// GetReadme calls GetEntry for readme.txt
func GetReadme(w http.ResponseWriter, r *http.Request) {
2024-05-09 23:44:37 +03:00
GetEntry(w, r, "readme.txt", DataFile("readme"), true)
2024-05-07 13:27:11 +03:00
}
// PostReadme saves contents of readme.txt file
func PostReadme(w http.ResponseWriter, r *http.Request) {
2024-05-09 23:44:37 +03:00
err := SaveFile(DataFile("readme"), []byte(r.FormValue("text")))
2024-05-07 13:27:11 +03:00
if err != nil {
slog.Error("error saving readme", "error", err)
}
http.Redirect(w, r, r.Header.Get("Referer"), 302)
}
2024-05-09 23:44:37 +03:00
// GetConfig calls GetEntry for Cfg
func GetConfig(w http.ResponseWriter, r *http.Request) {
GetEntry(w, r, "config.txt", ConfigFile, true)
}
// PostConfig saves new Cfg
func PostConfig(w http.ResponseWriter, r *http.Request) {
err := SaveFile(ConfigFile, []byte(r.FormValue("text")))
if err != nil {
slog.Error("error saving config", "error", err)
}
err = Cfg.Reload()
if err != nil {
slog.Error("error reloading config", "error", err)
}
http.Redirect(w, r, r.Header.Get("Referer"), 302)
}