2024-03-28 10:21:04 +03:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2024-05-18 16:15:20 +03:00
|
|
|
"embed"
|
2024-03-28 10:21:04 +03:00
|
|
|
"errors"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"html/template"
|
|
|
|
"log/slog"
|
|
|
|
"net/http"
|
2024-05-08 16:49:34 +03:00
|
|
|
"net/url"
|
2024-03-28 10:21:04 +03:00
|
|
|
"os"
|
2024-03-30 14:22:03 +03:00
|
|
|
"strings"
|
2024-03-28 10:21:04 +03:00
|
|
|
"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-28 10:21:04 +03:00
|
|
|
}
|
|
|
|
|
2024-03-30 13:51:29 +03:00
|
|
|
type Entry struct {
|
|
|
|
Title string
|
|
|
|
Content string
|
|
|
|
Link string
|
2024-03-28 10:21:04 +03:00
|
|
|
}
|
|
|
|
|
2024-04-23 14:13:45 +03:00
|
|
|
type formatEntries func([]string) []Entry
|
|
|
|
|
2024-06-17 00:54:55 +03:00
|
|
|
// Public contains the static files e.g. CSS, JS.
|
2024-06-02 12:27:36 +03:00
|
|
|
//
|
2024-05-18 16:15:20 +03:00
|
|
|
//go:embed public
|
|
|
|
var Public embed.FS
|
|
|
|
|
2024-06-17 00:54:55 +03:00
|
|
|
// Pages contains the HTML templates used by the app.
|
2024-06-02 12:27:36 +03:00
|
|
|
//
|
2024-05-18 16:15:20 +03:00
|
|
|
//go:embed pages
|
|
|
|
var Pages embed.FS
|
|
|
|
|
2024-06-17 00:54:55 +03:00
|
|
|
// EmbeddedPage returns contents of a file in Pages while "handling" potential errors.
|
2024-06-02 12:27:36 +03:00
|
|
|
func EmbeddedPage(name string) []byte {
|
2024-05-18 16:15:20 +03:00
|
|
|
data, err := Pages.ReadFile(name)
|
|
|
|
if err != nil {
|
2024-06-02 12:27:36 +03:00
|
|
|
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
|
|
|
|
2024-10-07 16:04:39 +03:00
|
|
|
var template404 = template.Must(template.New("404").Funcs(templateFuncs).ParseFS(Pages, "pages/error/404.html"))
|
2024-06-17 00:54:55 +03:00
|
|
|
// NotFound returns a user-friendly 404 error page.
|
2024-03-28 10:21:04 +03:00
|
|
|
func NotFound(w http.ResponseWriter, r *http.Request) {
|
|
|
|
w.WriteHeader(404)
|
2024-10-07 16:04:39 +03:00
|
|
|
|
|
|
|
err := template404.Execute(w, nil)
|
|
|
|
if err != nil {
|
|
|
|
slog.Error("error rendering error 404 page", "error", err)
|
|
|
|
InternalError(w, r)
|
|
|
|
return
|
|
|
|
}
|
2024-03-28 10:21:04 +03:00
|
|
|
}
|
|
|
|
|
2024-10-07 16:04:39 +03:00
|
|
|
var template500 = template.Must(template.New("500").Funcs(templateFuncs).ParseFS(Pages, "pages/error/500.html"))
|
2024-06-17 00:54:55 +03:00
|
|
|
// InternalError returns a user-friendly 500 error page.
|
2024-03-28 10:21:04 +03:00
|
|
|
func InternalError(w http.ResponseWriter, r *http.Request) {
|
|
|
|
w.WriteHeader(500)
|
2024-10-07 16:04:39 +03:00
|
|
|
|
|
|
|
err := template500.Execute(w, nil)
|
|
|
|
if err != nil { // Well this is awkward
|
|
|
|
slog.Error("error rendering error 500 page", "error", err)
|
|
|
|
HandleWrite(w.Write([]byte("500. Something went *very* wrong.")))
|
|
|
|
return
|
|
|
|
}
|
2024-03-28 10:21:04 +03:00
|
|
|
}
|
|
|
|
|
2024-06-17 00:54:55 +03:00
|
|
|
// GetEntries handles showing 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)
|
2024-03-28 10:21:04 +03:00
|
|
|
if err != nil {
|
2024-04-23 14:13:45 +03:00
|
|
|
slog.Error("error reading file list", "directory", dir, "error", err)
|
2024-03-28 10:21:04 +03:00
|
|
|
InternalError(w, r)
|
|
|
|
return
|
|
|
|
}
|
2024-04-23 14:13:45 +03:00
|
|
|
var filesFormatted = format(filesList)
|
2024-03-28 10:21:04 +03:00
|
|
|
|
2024-05-03 15:40:40 +03:00
|
|
|
err = listTemplate.ExecuteTemplate(w, "base", EntryList{Title: title, Description: description, Entries: filesFormatted})
|
2024-03-28 10:21:04 +03:00
|
|
|
if err != nil {
|
2024-03-30 14:22:03 +03:00
|
|
|
slog.Error("error executing template", "error", err)
|
2024-03-28 10:21:04 +03:00
|
|
|
InternalError(w, r)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-17 00:54:55 +03:00
|
|
|
// GetDays calls GetEntries for previous days' entries.
|
2024-04-23 14:13:45 +03:00
|
|
|
func GetDays(w http.ResponseWriter, r *http.Request) {
|
2024-05-18 16:15:20 +03:00
|
|
|
description := template.HTML(
|
2024-06-17 00:54:55 +03:00
|
|
|
"<a href=\"#footer\">" + template.HTMLEscapeString(TranslatableText("prompt.days")) + "</a>")
|
2024-05-18 16:15:20 +03:00
|
|
|
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
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-06-17 00:54:55 +03:00
|
|
|
// GetNotes calls GetEntries for all notes.
|
2024-04-23 14:13:45 +03:00
|
|
|
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(
|
2024-06-17 00:54:55 +03:00
|
|
|
"<a href=\"#\" onclick='newNote(\"" + template.HTMLEscapeString(TranslatableText("prompt.notes")) + "\")'>" + template.HTMLEscapeString(TranslatableText("button.notes")) + "</a>" +
|
2024-05-08 16:49:34 +03:00
|
|
|
" <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 {
|
2024-06-17 00:54:55 +03:00
|
|
|
// titleString := strings.Replace(v, "-", " ", -1) // This would be cool, but what if I need a hyphen?
|
|
|
|
filesFormatted = append(filesFormatted, Entry{Title: v, Link: "notes/" + v})
|
2024-04-23 14:13:45 +03:00
|
|
|
}
|
|
|
|
return filesFormatted
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-06-17 00:54:55 +03:00
|
|
|
// GetEntry handles showing a single file, editable or otherwise.
|
2024-04-30 23:35:00 +03:00
|
|
|
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-03-28 10:21:04 +03:00
|
|
|
}
|
2024-04-30 23:35:00 +03:00
|
|
|
|
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)})
|
|
|
|
}
|
2024-03-28 10:21:04 +03:00
|
|
|
if err != nil {
|
|
|
|
InternalError(w, r)
|
|
|
|
return
|
|
|
|
}
|
2024-04-30 23:35:00 +03:00
|
|
|
}
|
|
|
|
|
2024-06-17 00:54:55 +03:00
|
|
|
// PostEntry saves value of "text" HTML form component to a file and redirects back to Referer if present.
|
|
|
|
func PostEntry(filename string, w http.ResponseWriter, r *http.Request) {
|
|
|
|
err := SaveFile(filename, []byte(r.FormValue("text")))
|
|
|
|
if err != nil {
|
|
|
|
slog.Error("error saving file", "error", err, "file", filename)
|
|
|
|
}
|
|
|
|
if r.Referer() != "" {
|
2024-10-08 14:49:49 +03:00
|
|
|
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusFound)
|
2024-06-17 00:54:55 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetDay calls GetEntry for a day entry.
|
2024-04-30 23:35:00 +03:00
|
|
|
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
|
2024-10-08 14:49:49 +03:00
|
|
|
http.Redirect(w, r, "/", http.StatusFound)
|
2024-04-30 23:35:00 +03:00
|
|
|
return
|
|
|
|
}
|
2024-03-28 10:21:04 +03:00
|
|
|
|
2024-04-30 23:35:00 +03:00
|
|
|
title := dayString
|
2024-03-28 10:21:04 +03:00
|
|
|
t, err := time.Parse(time.DateOnly, dayString)
|
|
|
|
if err == nil { // This is low priority so silently fail
|
2024-04-30 23:35:00 +03:00
|
|
|
title = t.Format("02 Jan 2006")
|
2024-03-28 10:21:04 +03:00
|
|
|
}
|
|
|
|
|
2024-05-09 23:44:37 +03:00
|
|
|
GetEntry(w, r, title, DataFile("day/"+dayString), false)
|
2024-03-28 10:21:04 +03:00
|
|
|
}
|
2024-03-30 14:22:03 +03:00
|
|
|
|
2024-06-17 00:54:55 +03:00
|
|
|
// GetNote calls GetEntry for a note.
|
2024-03-30 14:22:03 +03:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2024-06-17 00:54:55 +03:00
|
|
|
// PostNote calls PostEntry for a note.
|
2024-03-30 14:22:03 +03:00
|
|
|
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-06-17 00:54:55 +03:00
|
|
|
PostEntry(DataFile("notes/"+noteString), w, r)
|
2024-05-09 23:44:37 +03:00
|
|
|
}
|