From 081e880fb6eebfd4b996ceb9db80d5fccf787ee1 Mon Sep 17 00:00:00 2001
From: Jason Streifling <jason@streifling.com>
Date: Sat, 13 Jul 2024 13:58:36 +0200
Subject: [PATCH 1/7] Change structure of code tor frontend and backend one

---
 cmd/{model => backend}/articles.go      |   2 +-
 cmd/{model => backend}/articles_tags.go |   2 +-
 cmd/{control => backend}/config.go      |   2 +-
 cmd/{model => backend}/db.go            |   2 +-
 cmd/{model => backend}/issues.go        |   2 +-
 cmd/{control => backend}/markdown.go    |   2 +-
 cmd/{control => backend}/rss.go         |   7 +-
 cmd/{control => backend}/sessions.go    |   2 +-
 cmd/{model => backend}/tags.go          |   2 +-
 cmd/{model => backend}/users.go         |   2 +-
 cmd/{view => frontend}/articles.go      | 118 +++++++++++++++++-------
 cmd/{view => frontend}/editor.go        |   9 +-
 cmd/{view => frontend}/images.go        |   6 +-
 cmd/{view => frontend}/issues.go        |   7 +-
 cmd/{view => frontend}/sessions.go      |  13 ++-
 cmd/{view => frontend}/users.go         |  39 ++++----
 cmd/main.go                             |  78 ++++++++--------
 web/templates/editor.html               |  28 +++++-
 18 files changed, 197 insertions(+), 126 deletions(-)
 rename cmd/{model => backend}/articles.go (99%)
 rename cmd/{model => backend}/articles_tags.go (99%)
 rename cmd/{control => backend}/config.go (99%)
 rename cmd/{model => backend}/db.go (99%)
 rename cmd/{model => backend}/issues.go (99%)
 rename cmd/{control => backend}/markdown.go (97%)
 rename cmd/{control => backend}/rss.go (93%)
 rename cmd/{control => backend}/sessions.go (98%)
 rename cmd/{model => backend}/tags.go (97%)
 rename cmd/{model => backend}/users.go (99%)
 rename cmd/{view => frontend}/articles.go (74%)
 rename cmd/{view => frontend}/editor.go (76%)
 rename cmd/{view => frontend}/images.go (73%)
 rename cmd/{view => frontend}/issues.go (77%)
 rename cmd/{view => frontend}/sessions.go (86%)
 rename cmd/{view => frontend}/users.go (92%)

diff --git a/cmd/model/articles.go b/cmd/backend/articles.go
similarity index 99%
rename from cmd/model/articles.go
rename to cmd/backend/articles.go
index 4d2a162..41a32d3 100644
--- a/cmd/model/articles.go
+++ b/cmd/backend/articles.go
@@ -1,4 +1,4 @@
-package model
+package backend
 
 import (
 	"context"
diff --git a/cmd/model/articles_tags.go b/cmd/backend/articles_tags.go
similarity index 99%
rename from cmd/model/articles_tags.go
rename to cmd/backend/articles_tags.go
index b44de77..72e7273 100644
--- a/cmd/model/articles_tags.go
+++ b/cmd/backend/articles_tags.go
@@ -1,4 +1,4 @@
-package model
+package backend
 
 import (
 	"fmt"
diff --git a/cmd/control/config.go b/cmd/backend/config.go
similarity index 99%
rename from cmd/control/config.go
rename to cmd/backend/config.go
index e5ab394..02f9ffc 100644
--- a/cmd/control/config.go
+++ b/cmd/backend/config.go
@@ -1,4 +1,4 @@
-package control
+package backend
 
 import (
 	"flag"
diff --git a/cmd/model/db.go b/cmd/backend/db.go
similarity index 99%
rename from cmd/model/db.go
rename to cmd/backend/db.go
index 6fcade8..cc7e67c 100644
--- a/cmd/model/db.go
+++ b/cmd/backend/db.go
@@ -1,4 +1,4 @@
-package model
+package backend
 
 import (
 	"bufio"
diff --git a/cmd/model/issues.go b/cmd/backend/issues.go
similarity index 99%
rename from cmd/model/issues.go
rename to cmd/backend/issues.go
index feec9a0..8b53b9e 100644
--- a/cmd/model/issues.go
+++ b/cmd/backend/issues.go
@@ -1,4 +1,4 @@
-package model
+package backend
 
 import (
 	"context"
diff --git a/cmd/control/markdown.go b/cmd/backend/markdown.go
similarity index 97%
rename from cmd/control/markdown.go
rename to cmd/backend/markdown.go
index 861a265..59346c4 100644
--- a/cmd/control/markdown.go
+++ b/cmd/backend/markdown.go
@@ -1,4 +1,4 @@
-package control
+package backend
 
 import (
 	"bytes"
diff --git a/cmd/control/rss.go b/cmd/backend/rss.go
similarity index 93%
rename from cmd/control/rss.go
rename to cmd/backend/rss.go
index 170c359..c68395e 100644
--- a/cmd/control/rss.go
+++ b/cmd/backend/rss.go
@@ -1,4 +1,4 @@
-package control
+package backend
 
 import (
 	"fmt"
@@ -7,10 +7,9 @@ import (
 	"time"
 
 	"git.streifling.com/jason/rss"
-	"streifling.com/jason/cpolis/cmd/model"
 )
 
-func GetChannel(db *model.DB, title, link, description string) (*rss.Channel, error) {
+func GetChannel(db *DB, title, link, description string) (*rss.Channel, error) {
 	channel := &rss.Channel{
 		Title:       title,
 		Link:        link,
@@ -51,7 +50,7 @@ func GetChannel(db *model.DB, title, link, description string) (*rss.Channel, er
 	return channel, nil
 }
 
-func GenerateRSS(db *model.DB, title, link, desc string) (*string, error) {
+func GenerateRSS(db *DB, title, link, desc string) (*string, error) {
 	channel := &rss.Channel{
 		Title:       title,
 		Link:        link,
diff --git a/cmd/control/sessions.go b/cmd/backend/sessions.go
similarity index 98%
rename from cmd/control/sessions.go
rename to cmd/backend/sessions.go
index ae5ec16..971fc7f 100644
--- a/cmd/control/sessions.go
+++ b/cmd/backend/sessions.go
@@ -1,4 +1,4 @@
-package control
+package backend
 
 import (
 	"crypto/rand"
diff --git a/cmd/model/tags.go b/cmd/backend/tags.go
similarity index 97%
rename from cmd/model/tags.go
rename to cmd/backend/tags.go
index 88e5a37..8a56d45 100644
--- a/cmd/model/tags.go
+++ b/cmd/backend/tags.go
@@ -1,4 +1,4 @@
-package model
+package backend
 
 import "fmt"
 
diff --git a/cmd/model/users.go b/cmd/backend/users.go
similarity index 99%
rename from cmd/model/users.go
rename to cmd/backend/users.go
index 08102db..36dcef1 100644
--- a/cmd/model/users.go
+++ b/cmd/backend/users.go
@@ -1,4 +1,4 @@
-package model
+package backend
 
 import (
 	"context"
diff --git a/cmd/view/articles.go b/cmd/frontend/articles.go
similarity index 74%
rename from cmd/view/articles.go
rename to cmd/frontend/articles.go
index 6faaae9..924a0ea 100644
--- a/cmd/view/articles.go
+++ b/cmd/frontend/articles.go
@@ -1,4 +1,4 @@
-package view
+package frontend
 
 import (
 	"fmt"
@@ -13,11 +13,10 @@ import (
 	"time"
 
 	"github.com/google/uuid"
-	"streifling.com/jason/cpolis/cmd/control"
-	"streifling.com/jason/cpolis/cmd/model"
+	"streifling.com/jason/cpolis/cmd/backend"
 )
 
-func ShowHub(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
+func ShowHub(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		session, err := s.Get(r, "cookie")
 		if err != nil {
@@ -31,7 +30,7 @@ func ShowHub(c *control.Config, db *model.DB, s *control.CookieStore) http.Handl
 	}
 }
 
-func WriteArticle(c *control.Config, db *model.DB) http.HandlerFunc {
+func WriteArticle(c *backend.Config, db *backend.DB) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		tags, err := db.GetTagList()
 		if err != nil {
@@ -45,7 +44,7 @@ func WriteArticle(c *control.Config, db *model.DB) http.HandlerFunc {
 	}
 }
 
-func SubmitArticle(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
+func SubmitArticle(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		session, err := s.Get(r, "cookie")
 		if err != nil {
@@ -54,7 +53,7 @@ func SubmitArticle(c *control.Config, db *model.DB, s *control.CookieStore) http
 			template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
 		}
 
-		article := &model.Article{
+		article := &backend.Article{
 			Title:       r.PostFormValue("article-title"),
 			Description: r.PostFormValue("article-description"),
 			Content:     r.PostFormValue("article-content"),
@@ -93,7 +92,7 @@ func SubmitArticle(c *control.Config, db *model.DB, s *control.CookieStore) http
 	}
 }
 
-func ResubmitArticle(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
+func ResubmitArticle(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 		if err != nil {
@@ -107,10 +106,10 @@ func ResubmitArticle(c *control.Config, db *model.DB, s *control.CookieStore) ht
 		content := r.PostFormValue("article-content")
 
 		if err = db.UpdateAttributes(
-			&model.Attribute{Table: "articles", ID: id, AttName: "title", Value: title},
-			&model.Attribute{Table: "articles", ID: id, AttName: "description", Value: description},
-			&model.Attribute{Table: "articles", ID: id, AttName: "content", Value: content},
-			&model.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false},
+			&backend.Attribute{Table: "articles", ID: id, AttName: "title", Value: title},
+			&backend.Attribute{Table: "articles", ID: id, AttName: "description", Value: description},
+			&backend.Attribute{Table: "articles", ID: id, AttName: "content", Value: content},
+			&backend.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false},
 		); err != nil {
 			log.Println(err)
 			http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -147,7 +146,7 @@ func ResubmitArticle(c *control.Config, db *model.DB, s *control.CookieStore) ht
 	}
 }
 
-func ShowUnpublishedArticles(c *control.Config, db *model.DB) http.HandlerFunc {
+func ShowUnpublishedArticles(c *backend.Config, db *backend.DB) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		unpublishedArticles, err := db.GetCertainArticles(false, false)
 		if err != nil {
@@ -162,11 +161,11 @@ func ShowUnpublishedArticles(c *control.Config, db *model.DB) http.HandlerFunc {
 	}
 }
 
-func ShowRejectedArticles(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
+func ShowRejectedArticles(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		type htmlData struct {
 			MyIDs            map[int64]bool
-			RejectedArticles []*model.Article
+			RejectedArticles []*backend.Article
 		}
 		data := new(htmlData)
 
@@ -197,13 +196,13 @@ func ShowRejectedArticles(c *control.Config, db *model.DB, s *control.CookieStor
 	}
 }
 
-func ReviewUnpublishedArticle(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
+func ReviewUnpublishedArticle(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		type htmlData struct {
 			Title       string
 			Description string
 			Content     template.HTML
-			Tags        []*model.Tag
+			Tags        []*backend.Tag
 			ID          int64
 		}
 
@@ -224,21 +223,21 @@ func ReviewUnpublishedArticle(c *control.Config, db *model.DB, s *control.Cookie
 			return
 		}
 
-		data.Title, err = control.ConvertToPlain(article.Title)
+		data.Title, err = backend.ConvertToPlain(article.Title)
 		if err != nil {
 			log.Println(err)
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
 		}
 
-		data.Description, err = control.ConvertToPlain(article.Description)
+		data.Description, err = backend.ConvertToPlain(article.Description)
 		if err != nil {
 			log.Println(err)
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
 		}
 
-		content, err := control.ConvertToHTML(article.Content)
+		content, err := backend.ConvertToHTML(article.Content)
 		if err != nil {
 			log.Println(err)
 			http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -259,12 +258,12 @@ func ReviewUnpublishedArticle(c *control.Config, db *model.DB, s *control.Cookie
 	}
 }
 
-func ReviewRejectedArticle(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
+func ReviewRejectedArticle(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		type htmlData struct {
 			Selected map[int64]bool
-			Article  *model.Article
-			Tags     []*model.Tag
+			Article  *backend.Article
+			Tags     []*backend.Tag
 		}
 		data := new(htmlData)
 
@@ -306,7 +305,7 @@ func ReviewRejectedArticle(c *control.Config, db *model.DB, s *control.CookieSto
 	}
 }
 
-func PublishArticle(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
+func PublishArticle(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 		if err != nil {
@@ -329,22 +328,22 @@ func PublishArticle(c *control.Config, db *model.DB, s *control.CookieStore) htt
 		}
 
 		if err = db.UpdateAttributes(
-			&model.Attribute{Table: "articles", ID: id, AttName: "published", Value: true},
-			&model.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false},
-			&model.Attribute{Table: "articles", ID: id, AttName: "created", Value: time.Now().Format("2006-01-02 15:04:05")},
+			&backend.Attribute{Table: "articles", ID: id, AttName: "published", Value: true},
+			&backend.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false},
+			&backend.Attribute{Table: "articles", ID: id, AttName: "created", Value: time.Now().Format("2006-01-02 15:04:05")},
 		); err != nil {
 			log.Println(err)
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
 		}
 
-		feed, err := control.GenerateRSS(db, c.Title, c.Link, c.Description)
+		feed, err := backend.GenerateRSS(db, c.Title, c.Link, c.Description)
 		if err != nil {
 			log.Println(err)
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
 		}
-		if err = control.SaveRSS(c.RSSFile, feed); err != nil {
+		if err = backend.SaveRSS(c.RSSFile, feed); err != nil {
 			log.Println(err)
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
@@ -356,7 +355,7 @@ func PublishArticle(c *control.Config, db *model.DB, s *control.CookieStore) htt
 	}
 }
 
-func RejectArticle(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
+func RejectArticle(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 		if err != nil {
@@ -373,7 +372,7 @@ func RejectArticle(c *control.Config, db *model.DB, s *control.CookieStore) http
 		}
 
 		if err = db.UpdateAttributes(
-			&model.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: true},
+			&backend.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: true},
 		); err != nil {
 			log.Println(err)
 			http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -386,7 +385,7 @@ func RejectArticle(c *control.Config, db *model.DB, s *control.CookieStore) http
 	}
 }
 
-func ShowCurrentArticles(c *control.Config, db *model.DB) http.HandlerFunc {
+func ShowCurrentArticles(c *backend.Config, db *backend.DB) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		articles, err := db.GetCurrentIssueArticles()
 		if err != nil {
@@ -400,7 +399,7 @@ func ShowCurrentArticles(c *control.Config, db *model.DB) http.HandlerFunc {
 	}
 }
 
-func UploadImage(c *control.Config) http.HandlerFunc {
+func UploadImage(c *backend.Config) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		file, header, err := r.FormFile("article-image")
 		if err != nil {
@@ -440,3 +439,56 @@ func UploadImage(c *control.Config) http.HandlerFunc {
 		template.Must(tmpl, err).ExecuteTemplate(w, "editor-images", imgMD)
 	}
 }
+
+func PreviewArticle(c *backend.Config, s *backend.CookieStore) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		type htmlData struct {
+			Title       string
+			Description string
+			Content     template.HTML
+		}
+
+		var err error
+		data := new(htmlData)
+
+		data.Title, err = backend.ConvertToPlain(r.PostFormValue("article-title"))
+		if err != nil {
+			log.Println(err)
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+
+		data.Description, err = backend.ConvertToPlain(r.PostFormValue("article-description"))
+		if err != nil {
+			log.Println(err)
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+
+		content, err := backend.ConvertToHTML(r.PostFormValue("article-content"))
+		if err != nil {
+			log.Println(err)
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		data.Content = template.HTML(content)
+
+		session, err := s.Get(r, "cookie")
+		if err != nil {
+			tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
+			msg := "Session nicht mehr gültig. Bitte erneut anmelden."
+			template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
+		}
+
+		session.Values["article"] = data
+		if err = session.Save(r, w); err != nil {
+			log.Println(err)
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+
+		tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html")
+		tmpl = template.Must(tmpl, err)
+		tmpl.ExecuteTemplate(w, "preview", data)
+	}
+}
diff --git a/cmd/view/editor.go b/cmd/frontend/editor.go
similarity index 76%
rename from cmd/view/editor.go
rename to cmd/frontend/editor.go
index cf76960..dea25cf 100644
--- a/cmd/view/editor.go
+++ b/cmd/frontend/editor.go
@@ -1,21 +1,20 @@
-package view
+package frontend
 
 import (
 	"html/template"
 	"net/http"
 
-	"streifling.com/jason/cpolis/cmd/control"
-	"streifling.com/jason/cpolis/cmd/model"
+	"streifling.com/jason/cpolis/cmd/backend"
 )
 
-func CreateTag(c *control.Config) http.HandlerFunc {
+func CreateTag(c *backend.Config) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-tag.html")
 		template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil)
 	}
 }
 
-func AddTag(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
+func AddTag(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		db.AddTag(r.PostFormValue("tag"))
 
diff --git a/cmd/view/images.go b/cmd/frontend/images.go
similarity index 73%
rename from cmd/view/images.go
rename to cmd/frontend/images.go
index 91bb2f5..2e381b6 100644
--- a/cmd/view/images.go
+++ b/cmd/frontend/images.go
@@ -1,14 +1,14 @@
-package view
+package frontend
 
 import (
 	"log"
 	"net/http"
 	"path/filepath"
 
-	"streifling.com/jason/cpolis/cmd/control"
+	"streifling.com/jason/cpolis/cmd/backend"
 )
 
-func ServeImage(c *control.Config, s *control.CookieStore) http.HandlerFunc {
+func ServeImage(c *backend.Config, s *backend.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		absFilepath, err := filepath.Abs(c.PicsDir)
 		if err != nil {
diff --git a/cmd/view/issues.go b/cmd/frontend/issues.go
similarity index 77%
rename from cmd/view/issues.go
rename to cmd/frontend/issues.go
index cf5f369..ad5e3da 100644
--- a/cmd/view/issues.go
+++ b/cmd/frontend/issues.go
@@ -1,15 +1,14 @@
-package view
+package frontend
 
 import (
 	"html/template"
 	"log"
 	"net/http"
 
-	"streifling.com/jason/cpolis/cmd/control"
-	"streifling.com/jason/cpolis/cmd/model"
+	"streifling.com/jason/cpolis/cmd/backend"
 )
 
-func PublishLatestIssue(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
+func PublishLatestIssue(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		if err := db.PublishLatestIssue(); err != nil {
 			log.Println(err)
diff --git a/cmd/view/sessions.go b/cmd/frontend/sessions.go
similarity index 86%
rename from cmd/view/sessions.go
rename to cmd/frontend/sessions.go
index 3d6163d..23b214a 100644
--- a/cmd/view/sessions.go
+++ b/cmd/frontend/sessions.go
@@ -1,4 +1,4 @@
-package view
+package frontend
 
 import (
 	"fmt"
@@ -6,11 +6,10 @@ import (
 	"log"
 	"net/http"
 
-	"streifling.com/jason/cpolis/cmd/control"
-	"streifling.com/jason/cpolis/cmd/model"
+	"streifling.com/jason/cpolis/cmd/backend"
 )
 
-func saveSession(w http.ResponseWriter, r *http.Request, s *control.CookieStore, u *model.User) error {
+func saveSession(w http.ResponseWriter, r *http.Request, s *backend.CookieStore, u *backend.User) error {
 	session, err := s.Get(r, "cookie")
 	if err != nil {
 		return fmt.Errorf("error getting session: %v", err)
@@ -27,7 +26,7 @@ func saveSession(w http.ResponseWriter, r *http.Request, s *control.CookieStore,
 	return nil
 }
 
-func HomePage(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
+func HomePage(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		numRows, err := db.CountEntries("users")
 		if err != nil {
@@ -54,7 +53,7 @@ func HomePage(c *control.Config, db *model.DB, s *control.CookieStore) http.Hand
 	}
 }
 
-func Login(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
+func Login(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		userName := r.PostFormValue("username")
 		password := r.PostFormValue("password")
@@ -89,7 +88,7 @@ func Login(c *control.Config, db *model.DB, s *control.CookieStore) http.Handler
 	}
 }
 
-func Logout(c *control.Config, s *control.CookieStore) http.HandlerFunc {
+func Logout(c *backend.Config, s *backend.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		session, err := s.Get(r, "cookie")
 		if err != nil {
diff --git a/cmd/view/users.go b/cmd/frontend/users.go
similarity index 92%
rename from cmd/view/users.go
rename to cmd/frontend/users.go
index f650c0e..1131b2e 100644
--- a/cmd/view/users.go
+++ b/cmd/frontend/users.go
@@ -1,4 +1,4 @@
-package view
+package frontend
 
 import (
 	"fmt"
@@ -7,16 +7,15 @@ import (
 	"net/http"
 	"strconv"
 
-	"streifling.com/jason/cpolis/cmd/control"
-	"streifling.com/jason/cpolis/cmd/model"
+	"streifling.com/jason/cpolis/cmd/backend"
 )
 
 type UserData struct {
-	*model.User
+	*backend.User
 	Msg string
 }
 
-func checkUserStrings(user *model.User) (string, int, bool) {
+func checkUserStrings(user *backend.User) (string, int, bool) {
 	userLen := 15
 	nameLen := 50
 
@@ -31,14 +30,14 @@ func checkUserStrings(user *model.User) (string, int, bool) {
 	}
 }
 
-func CreateUser(c *control.Config) http.HandlerFunc {
+func CreateUser(c *backend.Config) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
 		template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil)
 	}
 }
 
-func AddUser(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
+func AddUser(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		role, err := strconv.Atoi(r.PostFormValue("role"))
 		if err != nil {
@@ -48,7 +47,7 @@ func AddUser(c *control.Config, db *model.DB, s *control.CookieStore) http.Handl
 		}
 
 		htmlData := UserData{
-			User: &model.User{
+			User: &backend.User{
 				UserName:  r.PostFormValue("username"),
 				FirstName: r.PostFormValue("first-name"),
 				LastName:  r.PostFormValue("last-name"),
@@ -108,7 +107,7 @@ func AddUser(c *control.Config, db *model.DB, s *control.CookieStore) http.Handl
 	}
 }
 
-func EditSelf(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
+func EditSelf(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		session, err := s.Get(r, "cookie")
 		if err != nil {
@@ -129,7 +128,7 @@ func EditSelf(c *control.Config, db *model.DB, s *control.CookieStore) http.Hand
 	}
 }
 
-func UpdateSelf(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
+func UpdateSelf(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		session, err := s.Get(r, "cookie")
 		if err != nil {
@@ -139,7 +138,7 @@ func UpdateSelf(c *control.Config, db *model.DB, s *control.CookieStore) http.Ha
 		}
 
 		userData := UserData{
-			User: &model.User{
+			User: &backend.User{
 				ID:        session.Values["id"].(int64),
 				UserName:  r.PostFormValue("username"),
 				FirstName: r.PostFormValue("first-name"),
@@ -199,16 +198,16 @@ func UpdateSelf(c *control.Config, db *model.DB, s *control.CookieStore) http.Ha
 	}
 }
 
-func AddFirstUser(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
+func AddFirstUser(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		var err error
 
 		htmlData := UserData{
-			User: &model.User{
+			User: &backend.User{
 				UserName:  r.PostFormValue("username"),
 				FirstName: r.PostFormValue("first-name"),
 				LastName:  r.PostFormValue("last-name"),
-				Role:      model.Admin,
+				Role:      backend.Admin,
 			},
 		}
 		pass := r.PostFormValue("password")
@@ -274,11 +273,11 @@ func AddFirstUser(c *control.Config, db *model.DB, s *control.CookieStore) http.
 	}
 }
 
-func ShowAllUsers(c *control.Config, db *model.DB, s *control.CookieStore, action string) http.HandlerFunc {
+func ShowAllUsers(c *backend.Config, db *backend.DB, s *backend.CookieStore, action string) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		var err error
 		type htmlData struct {
-			Users  map[int64]*model.User
+			Users  map[int64]*backend.User
 			Action string
 		}
 
@@ -303,7 +302,7 @@ func ShowAllUsers(c *control.Config, db *model.DB, s *control.CookieStore, actio
 	}
 }
 
-func EditUser(c *control.Config, db *model.DB) http.HandlerFunc {
+func EditUser(c *backend.Config, db *backend.DB) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 		if err != nil {
@@ -324,7 +323,7 @@ func EditUser(c *control.Config, db *model.DB) http.HandlerFunc {
 	}
 }
 
-func UpdateUser(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
+func UpdateUser(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 		if err != nil {
@@ -341,7 +340,7 @@ func UpdateUser(c *control.Config, db *model.DB, s *control.CookieStore) http.Ha
 		}
 
 		userData := UserData{
-			User: &model.User{
+			User: &backend.User{
 				ID:        id,
 				UserName:  r.PostFormValue("username"),
 				FirstName: r.PostFormValue("first-name"),
@@ -408,7 +407,7 @@ func UpdateUser(c *control.Config, db *model.DB, s *control.CookieStore) http.Ha
 	}
 }
 
-func DeleteUser(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
+func DeleteUser(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 		if err != nil {
diff --git a/cmd/main.go b/cmd/main.go
index 75b1713..b5e52ba 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -6,17 +6,16 @@ import (
 	"net/http"
 	"os"
 
-	"streifling.com/jason/cpolis/cmd/control"
-	"streifling.com/jason/cpolis/cmd/model"
-	"streifling.com/jason/cpolis/cmd/view"
+	"streifling.com/jason/cpolis/cmd/backend"
+	"streifling.com/jason/cpolis/cmd/frontend"
 )
 
 func init() {
-	gob.Register(model.User{})
+	gob.Register(backend.User{})
 }
 
 func main() {
-	config, err := control.HandleConfig()
+	config, err := backend.HandleConfig()
 	if err != nil {
 		log.Fatalln(err)
 	}
@@ -28,59 +27,60 @@ func main() {
 	defer logFile.Close()
 	log.SetOutput(logFile)
 
-	db, err := model.OpenDB(config.DBName)
+	db, err := backend.OpenDB(config.DBName)
 	if err != nil {
 		log.Fatalln(err)
 	}
 	defer db.Close()
 
-	key, err := control.LoadKey(config.KeyFile)
+	key, err := backend.LoadKey(config.KeyFile)
 	if err != nil {
-		key, err = control.NewKey()
+		key, err = backend.NewKey()
 		if err != nil {
 			log.Fatalln(err)
 		}
-		control.SaveKey(key, config.KeyFile)
+		backend.SaveKey(key, config.KeyFile)
 	}
-	store := control.NewCookieStore(key)
+	store := backend.NewCookieStore(key)
 
 	mux := http.NewServeMux()
 	mux.Handle("/web/static/", http.StripPrefix("/web/static/",
 		http.FileServer(http.Dir(config.WebDir+"/static/"))))
-	mux.HandleFunc("/", view.HomePage(config, db, store))
+	mux.HandleFunc("/", frontend.HomePage(config, db, store))
 
-	mux.HandleFunc("GET /create-tag", view.CreateTag(config))
-	mux.HandleFunc("GET /create-user", view.CreateUser(config))
-	mux.HandleFunc("GET /edit-self", view.EditSelf(config, db, store))
-	mux.HandleFunc("GET /edit-user/{id}", view.EditUser(config, db))
-	mux.HandleFunc("GET /delete-user/{id}", view.DeleteUser(config, db, store))
-	mux.HandleFunc("GET /hub", view.ShowHub(config, db, store))
-	mux.HandleFunc("GET /logout", view.Logout(config, store))
-	mux.HandleFunc("GET /pics/{pic}", view.ServeImage(config, store))
-	mux.HandleFunc("GET /publish-article/{id}", view.PublishArticle(config, db, store))
-	mux.HandleFunc("GET /publish-issue", view.PublishLatestIssue(config, db, store))
-	mux.HandleFunc("GET /reject-article/{id}", view.RejectArticle(config, db, store))
-	mux.HandleFunc("GET /rejected-articles", view.ShowRejectedArticles(config, db, store))
-	mux.HandleFunc("GET /review-rejected-article/{id}", view.ReviewRejectedArticle(config, db, store))
-	mux.HandleFunc("GET /review-unpublished-article/{id}", view.ReviewUnpublishedArticle(config, db, store))
+	mux.HandleFunc("GET /create-tag", frontend.CreateTag(config))
+	mux.HandleFunc("GET /create-user", frontend.CreateUser(config))
+	mux.HandleFunc("GET /edit-self", frontend.EditSelf(config, db, store))
+	mux.HandleFunc("GET /edit-user/{id}", frontend.EditUser(config, db))
+	mux.HandleFunc("GET /delete-user/{id}", frontend.DeleteUser(config, db, store))
+	mux.HandleFunc("GET /hub", frontend.ShowHub(config, db, store))
+	mux.HandleFunc("GET /logout", frontend.Logout(config, store))
+	mux.HandleFunc("GET /pics/{pic}", frontend.ServeImage(config, store))
+	mux.HandleFunc("GET /publish-article/{id}", frontend.PublishArticle(config, db, store))
+	mux.HandleFunc("GET /publish-issue", frontend.PublishLatestIssue(config, db, store))
+	mux.HandleFunc("GET /reject-article/{id}", frontend.RejectArticle(config, db, store))
+	mux.HandleFunc("GET /rejected-articles", frontend.ShowRejectedArticles(config, db, store))
+	mux.HandleFunc("GET /review-rejected-article/{id}", frontend.ReviewRejectedArticle(config, db, store))
+	mux.HandleFunc("GET /review-unpublished-article/{id}", frontend.ReviewUnpublishedArticle(config, db, store))
 	mux.HandleFunc("GET /rss", func(w http.ResponseWriter, r *http.Request) {
 		http.ServeFile(w, r, config.RSSFile)
 	})
-	mux.HandleFunc("GET /show-all-users-edit", view.ShowAllUsers(config, db, store, "edit-user"))
-	mux.HandleFunc("GET /show-all-users-delete", view.ShowAllUsers(config, db, store, "delete-user"))
-	mux.HandleFunc("GET /this-issue", view.ShowCurrentArticles(config, db))
-	mux.HandleFunc("GET /unpublished-articles", view.ShowUnpublishedArticles(config, db))
-	mux.HandleFunc("GET /write-article", view.WriteArticle(config, db))
+	mux.HandleFunc("GET /show-all-users-edit", frontend.ShowAllUsers(config, db, store, "edit-user"))
+	mux.HandleFunc("GET /show-all-users-delete", frontend.ShowAllUsers(config, db, store, "delete-user"))
+	mux.HandleFunc("GET /this-issue", frontend.ShowCurrentArticles(config, db))
+	mux.HandleFunc("GET /unpublished-articles", frontend.ShowUnpublishedArticles(config, db))
+	mux.HandleFunc("GET /write-article", frontend.WriteArticle(config, db))
 
-	mux.HandleFunc("POST /add-first-user", view.AddFirstUser(config, db, store))
-	mux.HandleFunc("POST /add-tag", view.AddTag(config, db, store))
-	mux.HandleFunc("POST /add-user", view.AddUser(config, db, store))
-	mux.HandleFunc("POST /login", view.Login(config, db, store))
-	mux.HandleFunc("POST /resubmit-article/{id}", view.ResubmitArticle(config, db, store))
-	mux.HandleFunc("POST /submit-article", view.SubmitArticle(config, db, store))
-	mux.HandleFunc("POST /update-self", view.UpdateSelf(config, db, store))
-	mux.HandleFunc("POST /update-user/{id}", view.UpdateUser(config, db, store))
-	mux.HandleFunc("POST /upload-image", view.UploadImage(config))
+	mux.HandleFunc("POST /add-first-user", frontend.AddFirstUser(config, db, store))
+	mux.HandleFunc("POST /add-tag", frontend.AddTag(config, db, store))
+	mux.HandleFunc("POST /add-user", frontend.AddUser(config, db, store))
+	mux.HandleFunc("POST /login", frontend.Login(config, db, store))
+	mux.HandleFunc("POST /preview-article", frontend.PreviewArticle(config, store))
+	mux.HandleFunc("POST /resubmit-article/{id}", frontend.ResubmitArticle(config, db, store))
+	mux.HandleFunc("POST /submit-article", frontend.SubmitArticle(config, db, store))
+	mux.HandleFunc("POST /update-self", frontend.UpdateSelf(config, db, store))
+	mux.HandleFunc("POST /update-user/{id}", frontend.UpdateUser(config, db, store))
+	mux.HandleFunc("POST /upload-image", frontend.UploadImage(config))
 
 	log.Fatalln(http.ListenAndServe(config.Port, mux))
 }
diff --git a/web/templates/editor.html b/web/templates/editor.html
index 95b65a3..b197a62 100644
--- a/web/templates/editor.html
+++ b/web/templates/editor.html
@@ -1,7 +1,12 @@
 {{define "page-content"}}
 <h2>Editor</h2>
 
-<form>
+<form id="edit-area">
+    <div class="btn-area">
+        <button class="btn" disabled hx-get="/hub" hx-target="#edit-area">Schreiben</button>
+        <button class="btn" hx-post="/preview-article" hx-target="#edit-area">Vorschau</button>
+    </div>
+
     <div class="flex flex-col gap-y-1">
         <label for="article-title">Titel</label>
         <input name="article-title" type="text" />
@@ -55,7 +60,6 @@
 
         document.body.removeChild(textarea);
     }
-
 </script>
 {{end}}
 
@@ -68,3 +72,23 @@
 </div>
 {{end}}
 {{end}}
+
+
+{{define "preview"}}
+<span>Titel</span>
+<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
+    {{.Title}}
+</div>
+
+<span>Beschreibung</span>
+<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
+    {{.Description}}
+</div>
+
+<span>Artikel</span>
+<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
+    <div class="prose">
+        {{.Content}}
+    </div>
+</div>
+{{end}}

From b2db128aa9402c7133239e219c6b268b49940fed Mon Sep 17 00:00:00 2001
From: Jason Streifling <jason@streifling.com>
Date: Sat, 13 Jul 2024 14:09:11 +0200
Subject: [PATCH 2/7] Shorten lines by referencing frontend as f and backend as
 b

---
 cmd/frontend/articles.go | 70 +++++++++++++++++-----------------
 cmd/frontend/editor.go   |  6 +--
 cmd/frontend/images.go   |  4 +-
 cmd/frontend/issues.go   |  4 +-
 cmd/frontend/sessions.go | 10 ++---
 cmd/frontend/users.go    | 36 +++++++++---------
 cmd/main.go              | 82 ++++++++++++++++++++--------------------
 7 files changed, 105 insertions(+), 107 deletions(-)

diff --git a/cmd/frontend/articles.go b/cmd/frontend/articles.go
index 924a0ea..42b842f 100644
--- a/cmd/frontend/articles.go
+++ b/cmd/frontend/articles.go
@@ -13,10 +13,10 @@ import (
 	"time"
 
 	"github.com/google/uuid"
-	"streifling.com/jason/cpolis/cmd/backend"
+	b "streifling.com/jason/cpolis/cmd/backend"
 )
 
-func ShowHub(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
+func ShowHub(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		session, err := s.Get(r, "cookie")
 		if err != nil {
@@ -30,7 +30,7 @@ func ShowHub(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.Han
 	}
 }
 
-func WriteArticle(c *backend.Config, db *backend.DB) http.HandlerFunc {
+func WriteArticle(c *b.Config, db *b.DB) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		tags, err := db.GetTagList()
 		if err != nil {
@@ -44,7 +44,7 @@ func WriteArticle(c *backend.Config, db *backend.DB) http.HandlerFunc {
 	}
 }
 
-func SubmitArticle(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
+func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		session, err := s.Get(r, "cookie")
 		if err != nil {
@@ -53,7 +53,7 @@ func SubmitArticle(c *backend.Config, db *backend.DB, s *backend.CookieStore) ht
 			template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
 		}
 
-		article := &backend.Article{
+		article := &b.Article{
 			Title:       r.PostFormValue("article-title"),
 			Description: r.PostFormValue("article-description"),
 			Content:     r.PostFormValue("article-content"),
@@ -92,7 +92,7 @@ func SubmitArticle(c *backend.Config, db *backend.DB, s *backend.CookieStore) ht
 	}
 }
 
-func ResubmitArticle(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
+func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 		if err != nil {
@@ -106,10 +106,10 @@ func ResubmitArticle(c *backend.Config, db *backend.DB, s *backend.CookieStore)
 		content := r.PostFormValue("article-content")
 
 		if err = db.UpdateAttributes(
-			&backend.Attribute{Table: "articles", ID: id, AttName: "title", Value: title},
-			&backend.Attribute{Table: "articles", ID: id, AttName: "description", Value: description},
-			&backend.Attribute{Table: "articles", ID: id, AttName: "content", Value: content},
-			&backend.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false},
+			&b.Attribute{Table: "articles", ID: id, AttName: "title", Value: title},
+			&b.Attribute{Table: "articles", ID: id, AttName: "description", Value: description},
+			&b.Attribute{Table: "articles", ID: id, AttName: "content", Value: content},
+			&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false},
 		); err != nil {
 			log.Println(err)
 			http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -146,7 +146,7 @@ func ResubmitArticle(c *backend.Config, db *backend.DB, s *backend.CookieStore)
 	}
 }
 
-func ShowUnpublishedArticles(c *backend.Config, db *backend.DB) http.HandlerFunc {
+func ShowUnpublishedArticles(c *b.Config, db *b.DB) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		unpublishedArticles, err := db.GetCertainArticles(false, false)
 		if err != nil {
@@ -161,11 +161,11 @@ func ShowUnpublishedArticles(c *backend.Config, db *backend.DB) http.HandlerFunc
 	}
 }
 
-func ShowRejectedArticles(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
+func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		type htmlData struct {
 			MyIDs            map[int64]bool
-			RejectedArticles []*backend.Article
+			RejectedArticles []*b.Article
 		}
 		data := new(htmlData)
 
@@ -196,13 +196,13 @@ func ShowRejectedArticles(c *backend.Config, db *backend.DB, s *backend.CookieSt
 	}
 }
 
-func ReviewUnpublishedArticle(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
+func ReviewUnpublishedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		type htmlData struct {
 			Title       string
 			Description string
 			Content     template.HTML
-			Tags        []*backend.Tag
+			Tags        []*b.Tag
 			ID          int64
 		}
 
@@ -223,21 +223,21 @@ func ReviewUnpublishedArticle(c *backend.Config, db *backend.DB, s *backend.Cook
 			return
 		}
 
-		data.Title, err = backend.ConvertToPlain(article.Title)
+		data.Title, err = b.ConvertToPlain(article.Title)
 		if err != nil {
 			log.Println(err)
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
 		}
 
-		data.Description, err = backend.ConvertToPlain(article.Description)
+		data.Description, err = b.ConvertToPlain(article.Description)
 		if err != nil {
 			log.Println(err)
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
 		}
 
-		content, err := backend.ConvertToHTML(article.Content)
+		content, err := b.ConvertToHTML(article.Content)
 		if err != nil {
 			log.Println(err)
 			http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -258,12 +258,12 @@ func ReviewUnpublishedArticle(c *backend.Config, db *backend.DB, s *backend.Cook
 	}
 }
 
-func ReviewRejectedArticle(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
+func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		type htmlData struct {
 			Selected map[int64]bool
-			Article  *backend.Article
-			Tags     []*backend.Tag
+			Article  *b.Article
+			Tags     []*b.Tag
 		}
 		data := new(htmlData)
 
@@ -305,7 +305,7 @@ func ReviewRejectedArticle(c *backend.Config, db *backend.DB, s *backend.CookieS
 	}
 }
 
-func PublishArticle(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
+func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 		if err != nil {
@@ -328,22 +328,22 @@ func PublishArticle(c *backend.Config, db *backend.DB, s *backend.CookieStore) h
 		}
 
 		if err = db.UpdateAttributes(
-			&backend.Attribute{Table: "articles", ID: id, AttName: "published", Value: true},
-			&backend.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false},
-			&backend.Attribute{Table: "articles", ID: id, AttName: "created", Value: time.Now().Format("2006-01-02 15:04:05")},
+			&b.Attribute{Table: "articles", ID: id, AttName: "published", Value: true},
+			&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false},
+			&b.Attribute{Table: "articles", ID: id, AttName: "created", Value: time.Now().Format("2006-01-02 15:04:05")},
 		); err != nil {
 			log.Println(err)
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
 		}
 
-		feed, err := backend.GenerateRSS(db, c.Title, c.Link, c.Description)
+		feed, err := b.GenerateRSS(db, c.Title, c.Link, c.Description)
 		if err != nil {
 			log.Println(err)
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
 		}
-		if err = backend.SaveRSS(c.RSSFile, feed); err != nil {
+		if err = b.SaveRSS(c.RSSFile, feed); err != nil {
 			log.Println(err)
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
@@ -355,7 +355,7 @@ func PublishArticle(c *backend.Config, db *backend.DB, s *backend.CookieStore) h
 	}
 }
 
-func RejectArticle(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
+func RejectArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 		if err != nil {
@@ -372,7 +372,7 @@ func RejectArticle(c *backend.Config, db *backend.DB, s *backend.CookieStore) ht
 		}
 
 		if err = db.UpdateAttributes(
-			&backend.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: true},
+			&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: true},
 		); err != nil {
 			log.Println(err)
 			http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -385,7 +385,7 @@ func RejectArticle(c *backend.Config, db *backend.DB, s *backend.CookieStore) ht
 	}
 }
 
-func ShowCurrentArticles(c *backend.Config, db *backend.DB) http.HandlerFunc {
+func ShowCurrentArticles(c *b.Config, db *b.DB) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		articles, err := db.GetCurrentIssueArticles()
 		if err != nil {
@@ -399,7 +399,7 @@ func ShowCurrentArticles(c *backend.Config, db *backend.DB) http.HandlerFunc {
 	}
 }
 
-func UploadImage(c *backend.Config) http.HandlerFunc {
+func UploadImage(c *b.Config) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		file, header, err := r.FormFile("article-image")
 		if err != nil {
@@ -440,7 +440,7 @@ func UploadImage(c *backend.Config) http.HandlerFunc {
 	}
 }
 
-func PreviewArticle(c *backend.Config, s *backend.CookieStore) http.HandlerFunc {
+func PreviewArticle(c *b.Config, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		type htmlData struct {
 			Title       string
@@ -451,21 +451,21 @@ func PreviewArticle(c *backend.Config, s *backend.CookieStore) http.HandlerFunc
 		var err error
 		data := new(htmlData)
 
-		data.Title, err = backend.ConvertToPlain(r.PostFormValue("article-title"))
+		data.Title, err = b.ConvertToPlain(r.PostFormValue("article-title"))
 		if err != nil {
 			log.Println(err)
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
 		}
 
-		data.Description, err = backend.ConvertToPlain(r.PostFormValue("article-description"))
+		data.Description, err = b.ConvertToPlain(r.PostFormValue("article-description"))
 		if err != nil {
 			log.Println(err)
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
 		}
 
-		content, err := backend.ConvertToHTML(r.PostFormValue("article-content"))
+		content, err := b.ConvertToHTML(r.PostFormValue("article-content"))
 		if err != nil {
 			log.Println(err)
 			http.Error(w, err.Error(), http.StatusInternalServerError)
diff --git a/cmd/frontend/editor.go b/cmd/frontend/editor.go
index dea25cf..737c784 100644
--- a/cmd/frontend/editor.go
+++ b/cmd/frontend/editor.go
@@ -4,17 +4,17 @@ import (
 	"html/template"
 	"net/http"
 
-	"streifling.com/jason/cpolis/cmd/backend"
+	b "streifling.com/jason/cpolis/cmd/backend"
 )
 
-func CreateTag(c *backend.Config) http.HandlerFunc {
+func CreateTag(c *b.Config) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-tag.html")
 		template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil)
 	}
 }
 
-func AddTag(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
+func AddTag(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		db.AddTag(r.PostFormValue("tag"))
 
diff --git a/cmd/frontend/images.go b/cmd/frontend/images.go
index 2e381b6..7b77e5d 100644
--- a/cmd/frontend/images.go
+++ b/cmd/frontend/images.go
@@ -5,10 +5,10 @@ import (
 	"net/http"
 	"path/filepath"
 
-	"streifling.com/jason/cpolis/cmd/backend"
+	b "streifling.com/jason/cpolis/cmd/backend"
 )
 
-func ServeImage(c *backend.Config, s *backend.CookieStore) http.HandlerFunc {
+func ServeImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		absFilepath, err := filepath.Abs(c.PicsDir)
 		if err != nil {
diff --git a/cmd/frontend/issues.go b/cmd/frontend/issues.go
index ad5e3da..043ca95 100644
--- a/cmd/frontend/issues.go
+++ b/cmd/frontend/issues.go
@@ -5,10 +5,10 @@ import (
 	"log"
 	"net/http"
 
-	"streifling.com/jason/cpolis/cmd/backend"
+	b "streifling.com/jason/cpolis/cmd/backend"
 )
 
-func PublishLatestIssue(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
+func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		if err := db.PublishLatestIssue(); err != nil {
 			log.Println(err)
diff --git a/cmd/frontend/sessions.go b/cmd/frontend/sessions.go
index 23b214a..15245b1 100644
--- a/cmd/frontend/sessions.go
+++ b/cmd/frontend/sessions.go
@@ -6,10 +6,10 @@ import (
 	"log"
 	"net/http"
 
-	"streifling.com/jason/cpolis/cmd/backend"
+	b "streifling.com/jason/cpolis/cmd/backend"
 )
 
-func saveSession(w http.ResponseWriter, r *http.Request, s *backend.CookieStore, u *backend.User) error {
+func saveSession(w http.ResponseWriter, r *http.Request, s *b.CookieStore, u *b.User) error {
 	session, err := s.Get(r, "cookie")
 	if err != nil {
 		return fmt.Errorf("error getting session: %v", err)
@@ -26,7 +26,7 @@ func saveSession(w http.ResponseWriter, r *http.Request, s *backend.CookieStore,
 	return nil
 }
 
-func HomePage(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
+func HomePage(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		numRows, err := db.CountEntries("users")
 		if err != nil {
@@ -53,7 +53,7 @@ func HomePage(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.Ha
 	}
 }
 
-func Login(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
+func Login(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		userName := r.PostFormValue("username")
 		password := r.PostFormValue("password")
@@ -88,7 +88,7 @@ func Login(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.Handl
 	}
 }
 
-func Logout(c *backend.Config, s *backend.CookieStore) http.HandlerFunc {
+func Logout(c *b.Config, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		session, err := s.Get(r, "cookie")
 		if err != nil {
diff --git a/cmd/frontend/users.go b/cmd/frontend/users.go
index 1131b2e..5b9b804 100644
--- a/cmd/frontend/users.go
+++ b/cmd/frontend/users.go
@@ -7,15 +7,15 @@ import (
 	"net/http"
 	"strconv"
 
-	"streifling.com/jason/cpolis/cmd/backend"
+	b "streifling.com/jason/cpolis/cmd/backend"
 )
 
 type UserData struct {
-	*backend.User
+	*b.User
 	Msg string
 }
 
-func checkUserStrings(user *backend.User) (string, int, bool) {
+func checkUserStrings(user *b.User) (string, int, bool) {
 	userLen := 15
 	nameLen := 50
 
@@ -30,14 +30,14 @@ func checkUserStrings(user *backend.User) (string, int, bool) {
 	}
 }
 
-func CreateUser(c *backend.Config) http.HandlerFunc {
+func CreateUser(c *b.Config) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
 		template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil)
 	}
 }
 
-func AddUser(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
+func AddUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		role, err := strconv.Atoi(r.PostFormValue("role"))
 		if err != nil {
@@ -47,7 +47,7 @@ func AddUser(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.Han
 		}
 
 		htmlData := UserData{
-			User: &backend.User{
+			User: &b.User{
 				UserName:  r.PostFormValue("username"),
 				FirstName: r.PostFormValue("first-name"),
 				LastName:  r.PostFormValue("last-name"),
@@ -107,7 +107,7 @@ func AddUser(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.Han
 	}
 }
 
-func EditSelf(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
+func EditSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		session, err := s.Get(r, "cookie")
 		if err != nil {
@@ -128,7 +128,7 @@ func EditSelf(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.Ha
 	}
 }
 
-func UpdateSelf(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
+func UpdateSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		session, err := s.Get(r, "cookie")
 		if err != nil {
@@ -138,7 +138,7 @@ func UpdateSelf(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.
 		}
 
 		userData := UserData{
-			User: &backend.User{
+			User: &b.User{
 				ID:        session.Values["id"].(int64),
 				UserName:  r.PostFormValue("username"),
 				FirstName: r.PostFormValue("first-name"),
@@ -198,16 +198,16 @@ func UpdateSelf(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.
 	}
 }
 
-func AddFirstUser(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
+func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		var err error
 
 		htmlData := UserData{
-			User: &backend.User{
+			User: &b.User{
 				UserName:  r.PostFormValue("username"),
 				FirstName: r.PostFormValue("first-name"),
 				LastName:  r.PostFormValue("last-name"),
-				Role:      backend.Admin,
+				Role:      b.Admin,
 			},
 		}
 		pass := r.PostFormValue("password")
@@ -273,11 +273,11 @@ func AddFirstUser(c *backend.Config, db *backend.DB, s *backend.CookieStore) htt
 	}
 }
 
-func ShowAllUsers(c *backend.Config, db *backend.DB, s *backend.CookieStore, action string) http.HandlerFunc {
+func ShowAllUsers(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		var err error
 		type htmlData struct {
-			Users  map[int64]*backend.User
+			Users  map[int64]*b.User
 			Action string
 		}
 
@@ -302,7 +302,7 @@ func ShowAllUsers(c *backend.Config, db *backend.DB, s *backend.CookieStore, act
 	}
 }
 
-func EditUser(c *backend.Config, db *backend.DB) http.HandlerFunc {
+func EditUser(c *b.Config, db *b.DB) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 		if err != nil {
@@ -323,7 +323,7 @@ func EditUser(c *backend.Config, db *backend.DB) http.HandlerFunc {
 	}
 }
 
-func UpdateUser(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
+func UpdateUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 		if err != nil {
@@ -340,7 +340,7 @@ func UpdateUser(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.
 		}
 
 		userData := UserData{
-			User: &backend.User{
+			User: &b.User{
 				ID:        id,
 				UserName:  r.PostFormValue("username"),
 				FirstName: r.PostFormValue("first-name"),
@@ -407,7 +407,7 @@ func UpdateUser(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.
 	}
 }
 
-func DeleteUser(c *backend.Config, db *backend.DB, s *backend.CookieStore) http.HandlerFunc {
+func DeleteUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 		if err != nil {
diff --git a/cmd/main.go b/cmd/main.go
index b5e52ba..be4a78b 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -6,16 +6,16 @@ import (
 	"net/http"
 	"os"
 
-	"streifling.com/jason/cpolis/cmd/backend"
-	"streifling.com/jason/cpolis/cmd/frontend"
+	b "streifling.com/jason/cpolis/cmd/backend"
+	f "streifling.com/jason/cpolis/cmd/frontend"
 )
 
 func init() {
-	gob.Register(backend.User{})
+	gob.Register(b.User{})
 }
 
 func main() {
-	config, err := backend.HandleConfig()
+	config, err := b.HandleConfig()
 	if err != nil {
 		log.Fatalln(err)
 	}
@@ -27,60 +27,58 @@ func main() {
 	defer logFile.Close()
 	log.SetOutput(logFile)
 
-	db, err := backend.OpenDB(config.DBName)
+	db, err := b.OpenDB(config.DBName)
 	if err != nil {
 		log.Fatalln(err)
 	}
 	defer db.Close()
 
-	key, err := backend.LoadKey(config.KeyFile)
+	key, err := b.LoadKey(config.KeyFile)
 	if err != nil {
-		key, err = backend.NewKey()
+		key, err = b.NewKey()
 		if err != nil {
 			log.Fatalln(err)
 		}
-		backend.SaveKey(key, config.KeyFile)
+		b.SaveKey(key, config.KeyFile)
 	}
-	store := backend.NewCookieStore(key)
+	store := b.NewCookieStore(key)
 
 	mux := http.NewServeMux()
 	mux.Handle("/web/static/", http.StripPrefix("/web/static/",
 		http.FileServer(http.Dir(config.WebDir+"/static/"))))
-	mux.HandleFunc("/", frontend.HomePage(config, db, store))
+	mux.HandleFunc("/", f.HomePage(config, db, store))
 
-	mux.HandleFunc("GET /create-tag", frontend.CreateTag(config))
-	mux.HandleFunc("GET /create-user", frontend.CreateUser(config))
-	mux.HandleFunc("GET /edit-self", frontend.EditSelf(config, db, store))
-	mux.HandleFunc("GET /edit-user/{id}", frontend.EditUser(config, db))
-	mux.HandleFunc("GET /delete-user/{id}", frontend.DeleteUser(config, db, store))
-	mux.HandleFunc("GET /hub", frontend.ShowHub(config, db, store))
-	mux.HandleFunc("GET /logout", frontend.Logout(config, store))
-	mux.HandleFunc("GET /pics/{pic}", frontend.ServeImage(config, store))
-	mux.HandleFunc("GET /publish-article/{id}", frontend.PublishArticle(config, db, store))
-	mux.HandleFunc("GET /publish-issue", frontend.PublishLatestIssue(config, db, store))
-	mux.HandleFunc("GET /reject-article/{id}", frontend.RejectArticle(config, db, store))
-	mux.HandleFunc("GET /rejected-articles", frontend.ShowRejectedArticles(config, db, store))
-	mux.HandleFunc("GET /review-rejected-article/{id}", frontend.ReviewRejectedArticle(config, db, store))
-	mux.HandleFunc("GET /review-unpublished-article/{id}", frontend.ReviewUnpublishedArticle(config, db, store))
-	mux.HandleFunc("GET /rss", func(w http.ResponseWriter, r *http.Request) {
-		http.ServeFile(w, r, config.RSSFile)
-	})
-	mux.HandleFunc("GET /show-all-users-edit", frontend.ShowAllUsers(config, db, store, "edit-user"))
-	mux.HandleFunc("GET /show-all-users-delete", frontend.ShowAllUsers(config, db, store, "delete-user"))
-	mux.HandleFunc("GET /this-issue", frontend.ShowCurrentArticles(config, db))
-	mux.HandleFunc("GET /unpublished-articles", frontend.ShowUnpublishedArticles(config, db))
-	mux.HandleFunc("GET /write-article", frontend.WriteArticle(config, db))
+	mux.HandleFunc("GET /create-tag", f.CreateTag(config))
+	mux.HandleFunc("GET /create-user", f.CreateUser(config))
+	mux.HandleFunc("GET /edit-self", f.EditSelf(config, db, store))
+	mux.HandleFunc("GET /edit-user/{id}", f.EditUser(config, db))
+	mux.HandleFunc("GET /delete-user/{id}", f.DeleteUser(config, db, store))
+	mux.HandleFunc("GET /hub", f.ShowHub(config, db, store))
+	mux.HandleFunc("GET /logout", f.Logout(config, store))
+	mux.HandleFunc("GET /pics/{pic}", f.ServeImage(config, store))
+	mux.HandleFunc("GET /publish-article/{id}", f.PublishArticle(config, db, store))
+	mux.HandleFunc("GET /publish-issue", f.PublishLatestIssue(config, db, store))
+	mux.HandleFunc("GET /reject-article/{id}", f.RejectArticle(config, db, store))
+	mux.HandleFunc("GET /rejected-articles", f.ShowRejectedArticles(config, db, store))
+	mux.HandleFunc("GET /review-rejected-article/{id}", f.ReviewRejectedArticle(config, db, store))
+	mux.HandleFunc("GET /review-unpublished-article/{id}", f.ReviewUnpublishedArticle(config, db, store))
+	mux.HandleFunc("GET /rss", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, config.RSSFile) })
+	mux.HandleFunc("GET /show-all-users-edit", f.ShowAllUsers(config, db, store, "edit-user"))
+	mux.HandleFunc("GET /show-all-users-delete", f.ShowAllUsers(config, db, store, "delete-user"))
+	mux.HandleFunc("GET /this-issue", f.ShowCurrentArticles(config, db))
+	mux.HandleFunc("GET /unpublished-articles", f.ShowUnpublishedArticles(config, db))
+	mux.HandleFunc("GET /write-article", f.WriteArticle(config, db))
 
-	mux.HandleFunc("POST /add-first-user", frontend.AddFirstUser(config, db, store))
-	mux.HandleFunc("POST /add-tag", frontend.AddTag(config, db, store))
-	mux.HandleFunc("POST /add-user", frontend.AddUser(config, db, store))
-	mux.HandleFunc("POST /login", frontend.Login(config, db, store))
-	mux.HandleFunc("POST /preview-article", frontend.PreviewArticle(config, store))
-	mux.HandleFunc("POST /resubmit-article/{id}", frontend.ResubmitArticle(config, db, store))
-	mux.HandleFunc("POST /submit-article", frontend.SubmitArticle(config, db, store))
-	mux.HandleFunc("POST /update-self", frontend.UpdateSelf(config, db, store))
-	mux.HandleFunc("POST /update-user/{id}", frontend.UpdateUser(config, db, store))
-	mux.HandleFunc("POST /upload-image", frontend.UploadImage(config))
+	mux.HandleFunc("POST /add-first-user", f.AddFirstUser(config, db, store))
+	mux.HandleFunc("POST /add-tag", f.AddTag(config, db, store))
+	mux.HandleFunc("POST /add-user", f.AddUser(config, db, store))
+	mux.HandleFunc("POST /login", f.Login(config, db, store))
+	mux.HandleFunc("POST /preview-article", f.PreviewArticle(config, store))
+	mux.HandleFunc("POST /resubmit-article/{id}", f.ResubmitArticle(config, db, store))
+	mux.HandleFunc("POST /submit-article", f.SubmitArticle(config, db, store))
+	mux.HandleFunc("POST /update-self", f.UpdateSelf(config, db, store))
+	mux.HandleFunc("POST /update-user/{id}", f.UpdateUser(config, db, store))
+	mux.HandleFunc("POST /upload-image", f.UploadImage(config))
 
 	log.Fatalln(http.ListenAndServe(config.Port, mux))
 }

From 084b101e314c4af843bf0928593e084e6d877bdb Mon Sep 17 00:00:00 2001
From: Jason Streifling <jason@streifling.com>
Date: Sat, 13 Jul 2024 14:17:40 +0200
Subject: [PATCH 3/7] Register f.ArticlePreviewHtmlData in init()

---
 cmd/frontend/articles.go | 14 +++++++-------
 cmd/main.go              |  1 +
 2 files changed, 8 insertions(+), 7 deletions(-)

diff --git a/cmd/frontend/articles.go b/cmd/frontend/articles.go
index 42b842f..130016d 100644
--- a/cmd/frontend/articles.go
+++ b/cmd/frontend/articles.go
@@ -16,6 +16,12 @@ import (
 	b "streifling.com/jason/cpolis/cmd/backend"
 )
 
+type ArticlePreviewHtmlData struct {
+	Title       string
+	Description string
+	Content     template.HTML
+}
+
 func ShowHub(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		session, err := s.Get(r, "cookie")
@@ -442,14 +448,8 @@ func UploadImage(c *b.Config) http.HandlerFunc {
 
 func PreviewArticle(c *b.Config, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
-		type htmlData struct {
-			Title       string
-			Description string
-			Content     template.HTML
-		}
-
 		var err error
-		data := new(htmlData)
+		data := new(ArticlePreviewHtmlData)
 
 		data.Title, err = b.ConvertToPlain(r.PostFormValue("article-title"))
 		if err != nil {
diff --git a/cmd/main.go b/cmd/main.go
index be4a78b..8e8c667 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -12,6 +12,7 @@ import (
 
 func init() {
 	gob.Register(b.User{})
+	gob.Register(f.ArticlePreviewHtmlData{})
 }
 
 func main() {

From ef1914ee5cc90012f4350aea10a22bafc299759c Mon Sep 17 00:00:00 2001
From: Jason Streifling <jason@streifling.com>
Date: Wed, 17 Jul 2024 23:25:57 +0200
Subject: [PATCH 4/7] Implemented article preview

---
 cmd/frontend/articles.go  | 56 ++++++++++++++++++++++++++++++++-------
 cmd/main.go               |  4 +--
 web/templates/editor.html | 54 ++++++++++++++++++-------------------
 3 files changed, 75 insertions(+), 39 deletions(-)

diff --git a/cmd/frontend/articles.go b/cmd/frontend/articles.go
index 130016d..2c0ada6 100644
--- a/cmd/frontend/articles.go
+++ b/cmd/frontend/articles.go
@@ -16,10 +16,18 @@ import (
 	b "streifling.com/jason/cpolis/cmd/backend"
 )
 
-type ArticlePreviewHtmlData struct {
+const (
+	EditMode = iota
+	PreviewMode
+)
+
+type EditorHTMLData struct {
 	Title       string
 	Description string
-	Content     template.HTML
+	Content     string
+	HTMLContent template.HTML
+	Tags        []*b.Tag
+	Mode        int
 }
 
 func ShowHub(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
@@ -31,14 +39,36 @@ func ShowHub(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 			template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
 		}
 
+		session.Values["article"] = nil
+		if err = session.Save(r, w); err != nil {
+			log.Println(err)
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+
 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
 		template.Must(tmpl, err).ExecuteTemplate(w, "page-content", session.Values["role"].(int))
 	}
 }
 
-func WriteArticle(c *b.Config, db *b.DB) http.HandlerFunc {
+func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
-		tags, err := db.GetTagList()
+		session, err := s.Get(r, "cookie")
+		if err != nil {
+			tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
+			msg := "Session nicht mehr gültig. Bitte erneut anmelden."
+			template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
+		}
+
+		var data EditorHTMLData
+		if session.Values["article"] == nil {
+			data = EditorHTMLData{}
+		} else {
+			data = session.Values["article"].(EditorHTMLData)
+		}
+		data.Mode = EditMode
+
+		data.Tags, err = db.GetTagList()
 		if err != nil {
 			log.Println(err)
 			http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -46,7 +76,7 @@ func WriteArticle(c *b.Config, db *b.DB) http.HandlerFunc {
 		}
 
 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html")
-		template.Must(tmpl, err).ExecuteTemplate(w, "page-content", tags)
+		template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data)
 	}
 }
 
@@ -59,6 +89,13 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 			template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
 		}
 
+		session.Values["article"] = nil
+		if err = session.Save(r, w); err != nil {
+			log.Println(err)
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+
 		article := &b.Article{
 			Title:       r.PostFormValue("article-title"),
 			Description: r.PostFormValue("article-description"),
@@ -449,7 +486,7 @@ func UploadImage(c *b.Config) http.HandlerFunc {
 func PreviewArticle(c *b.Config, s *b.CookieStore) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		var err error
-		data := new(ArticlePreviewHtmlData)
+		data := EditorHTMLData{Mode: PreviewMode}
 
 		data.Title, err = b.ConvertToPlain(r.PostFormValue("article-title"))
 		if err != nil {
@@ -465,13 +502,14 @@ func PreviewArticle(c *b.Config, s *b.CookieStore) http.HandlerFunc {
 			return
 		}
 
-		content, err := b.ConvertToHTML(r.PostFormValue("article-content"))
+		data.Content = r.PostFormValue("article-content")
+		content, err := b.ConvertToHTML(data.Content)
 		if err != nil {
 			log.Println(err)
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
 		}
-		data.Content = template.HTML(content)
+		data.HTMLContent = template.HTML(content)
 
 		session, err := s.Get(r, "cookie")
 		if err != nil {
@@ -489,6 +527,6 @@ func PreviewArticle(c *b.Config, s *b.CookieStore) http.HandlerFunc {
 
 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html")
 		tmpl = template.Must(tmpl, err)
-		tmpl.ExecuteTemplate(w, "preview", data)
+		tmpl.ExecuteTemplate(w, "page-content", data)
 	}
 }
diff --git a/cmd/main.go b/cmd/main.go
index 8e8c667..220e945 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -12,7 +12,7 @@ import (
 
 func init() {
 	gob.Register(b.User{})
-	gob.Register(f.ArticlePreviewHtmlData{})
+	gob.Register(f.EditorHTMLData{})
 }
 
 func main() {
@@ -68,7 +68,7 @@ func main() {
 	mux.HandleFunc("GET /show-all-users-delete", f.ShowAllUsers(config, db, store, "delete-user"))
 	mux.HandleFunc("GET /this-issue", f.ShowCurrentArticles(config, db))
 	mux.HandleFunc("GET /unpublished-articles", f.ShowUnpublishedArticles(config, db))
-	mux.HandleFunc("GET /write-article", f.WriteArticle(config, db))
+	mux.HandleFunc("GET /write-article", f.WriteArticle(config, db, store))
 
 	mux.HandleFunc("POST /add-first-user", f.AddFirstUser(config, db, store))
 	mux.HandleFunc("POST /add-tag", f.AddTag(config, db, store))
diff --git a/web/templates/editor.html b/web/templates/editor.html
index b197a62..35b095a 100644
--- a/web/templates/editor.html
+++ b/web/templates/editor.html
@@ -2,28 +2,26 @@
 <h2>Editor</h2>
 
 <form id="edit-area">
-    <div class="btn-area">
-        <button class="btn" disabled hx-get="/hub" hx-target="#edit-area">Schreiben</button>
-        <button class="btn" hx-post="/preview-article" hx-target="#edit-area">Vorschau</button>
-    </div>
+    {{if eq .Mode 0}}
+    <button class="btn" hx-post="/preview-article" hx-target="#page-content">Vorschau</button>
 
     <div class="flex flex-col gap-y-1">
         <label for="article-title">Titel</label>
-        <input name="article-title" type="text" />
+        <input name="article-title" type="text" value="{{.Title}}" />
     </div>
     <div class="flex flex-col">
         <label for="article-description">Beschreibung</label>
-        <textarea name="article-description"></textarea>
+        <textarea name="article-description">{{.Description}}</textarea>
     </div>
     <div class="flex flex-col">
         <label for="article-content">Artikel</label>
-        <textarea name="article-content"></textarea>
+        <textarea name="article-content">{{.Content}}</textarea>
     </div>
 
     <div>
         <span>Tags</span>
         <div class="flex flex-wrap gap-x-4">
-            {{range .}}
+            {{range .Tags}}
             <div>
                 <input id="{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" />
                 <label for="{{.Name}}">{{.Name}}</label>
@@ -41,6 +39,26 @@
         <input class="action-btn" type="submit" value="Senden" hx-post="/submit-article" hx-target="#page-content" />
         <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
     </div>
+    {{else}}
+    <button class="btn" hx-get="/write-article" hx-target="#page-content">Schreiben</button>
+
+    <span>Titel</span>
+    <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
+        {{.Title}}
+    </div>
+
+    <span>Beschreibung</span>
+    <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
+        {{.Description}}
+    </div>
+
+    <span>Artikel</span>
+    <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
+        <div class="prose">
+            {{.HTMLContent}}
+        </div>
+    </div>
+    {{end}}
 </form>
 
 <script>
@@ -72,23 +90,3 @@
 </div>
 {{end}}
 {{end}}
-
-
-{{define "preview"}}
-<span>Titel</span>
-<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
-    {{.Title}}
-</div>
-
-<span>Beschreibung</span>
-<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
-    {{.Description}}
-</div>
-
-<span>Artikel</span>
-<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
-    <div class="prose">
-        {{.Content}}
-    </div>
-</div>
-{{end}}

From 203a1ed14760251f4d51e91fce0687a7270abd07 Mon Sep 17 00:00:00 2001
From: Jason Streifling <jason@streifling.com>
Date: Thu, 8 Aug 2024 21:09:38 +0200
Subject: [PATCH 5/7] Implemented EasyMDE

---
 cmd/frontend/articles.go          | 15 ++++--
 web/templates/editor.html         | 79 +++++++++++--------------------
 web/templates/index.html          |  4 +-
 web/templates/rework-article.html | 55 ++++++++++-----------
 4 files changed, 67 insertions(+), 86 deletions(-)

diff --git a/cmd/frontend/articles.go b/cmd/frontend/articles.go
index 2c0ada6..7f95a04 100644
--- a/cmd/frontend/articles.go
+++ b/cmd/frontend/articles.go
@@ -1,9 +1,11 @@
 package frontend
 
 import (
+	"encoding/json"
 	"fmt"
 	"html/template"
 	"io"
+	"io/fs"
 	"log"
 	"net/http"
 	"os"
@@ -462,6 +464,12 @@ func UploadImage(c *b.Config) http.HandlerFunc {
 			return
 		}
 
+		if err = os.MkdirAll(fmt.Sprint(c.PicsDir, "/"), fs.FileMode(0755)); err != nil {
+			log.Println(err)
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+
 		img, err := os.Create(absFilepath)
 		if err != nil {
 			log.Println(err)
@@ -476,10 +484,9 @@ func UploadImage(c *b.Config) http.HandlerFunc {
 			return
 		}
 
-		alt := strings.Join(nameStrings[0:len(nameStrings)-1], " ")
-		imgMD := fmt.Sprint("![", alt, "](", c.Domain, "/pics/", filename, ")")
-		tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html")
-		template.Must(tmpl, err).ExecuteTemplate(w, "editor-images", imgMD)
+		url := fmt.Sprint(c.Domain, "/pics/", filename)
+		w.Header().Set("Content-Type", "application/json")
+		json.NewEncoder(w).Encode(url)
 	}
 }
 
diff --git a/web/templates/editor.html b/web/templates/editor.html
index 35b095a..2dccc55 100644
--- a/web/templates/editor.html
+++ b/web/templates/editor.html
@@ -2,9 +2,6 @@
 <h2>Editor</h2>
 
 <form id="edit-area">
-    {{if eq .Mode 0}}
-    <button class="btn" hx-post="/preview-article" hx-target="#page-content">Vorschau</button>
-
     <div class="flex flex-col gap-y-1">
         <label for="article-title">Titel</label>
         <input name="article-title" type="text" value="{{.Title}}" />
@@ -15,8 +12,9 @@
     </div>
     <div class="flex flex-col">
         <label for="article-content">Artikel</label>
-        <textarea name="article-content">{{.Content}}</textarea>
+        <textarea id="easyMDE">{{.Content}}</textarea>
     </div>
+    <input id="article-content" name="article-content" type="hidden" />
 
     <div>
         <span>Tags</span>
@@ -30,63 +28,40 @@
         </div>
     </div>
 
-    <div id="editor-images">
-        <input class="mb-2" name="article-image" type="file" hx-encoding="multipart/form-data" hx-post="/upload-image"
-            hx-swap="beforeend" hx-target="#editor-images" />
-    </div>
-
     <div class="btn-area">
         <input class="action-btn" type="submit" value="Senden" hx-post="/submit-article" hx-target="#page-content" />
         <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
     </div>
-    {{else}}
-    <button class="btn" hx-get="/write-article" hx-target="#page-content">Schreiben</button>
-
-    <span>Titel</span>
-    <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
-        {{.Title}}
-    </div>
-
-    <span>Beschreibung</span>
-    <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
-        {{.Description}}
-    </div>
-
-    <span>Artikel</span>
-    <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
-        <div class="prose">
-            {{.HTMLContent}}
-        </div>
-    </div>
-    {{end}}
 </form>
 
 <script>
-    function copyToClipboard(text) {
-        event.preventDefault(); // Get-Request verhindern
+    var easyMDE = new EasyMDE({
+        element: document.getElementById('easyMDE'),
+        hideIcons: ['image'],
+        imageTexts: {sbInit: ''},
+        showIcons: ["code", "table", "upload-image"],
+        uploadImage: true,
 
-        var textarea = document.createElement("textarea");
-        textarea.textContent = text;
-        document.body.appendChild(textarea);
+        imageUploadFunction: function (file, onSuccess, onError) {
+            var formData = new FormData();
+            formData.append('article-image', file);
 
-        textarea.select();
-        try {
-            document.execCommand('copy');
-        } catch (err) {
-            console.warn('Fehler beim Kopieren', err);
-        }
+            fetch('/upload-image', {
+                method: 'POST',
+                body: formData
+            })
+                .then(response => response.json())
+                .then(data => {
+                    onSuccess(data);
+                })
+                .catch(error => {
+                    onError(error);
+                });
+        },
+    });
 
-        document.body.removeChild(textarea);
-    }
+    easyMDE.codemirror.on("change", () => {
+        document.getElementById('article-content').value = easyMDE.value();
+    });
 </script>
 {{end}}
-
-{{define "editor-images"}}
-{{if gt (len .) 0}}
-<div class="border px-2 py-1 rounded-md flex gap-4 justify-between">
-    <div class="self-center">{{.}}</div>
-    <button class="bg-slate-50 border my-2 px-3 py-2 rounded-md w-32 hover:bg-slate-100"
-        onclick="copyToClipboard('{{.}}')">Kopieren</button>
-</div>
-{{end}}
-{{end}}
diff --git a/web/templates/index.html b/web/templates/index.html
index 95b36b0..bce30fe 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -6,6 +6,7 @@
     <meta name="viewport" content="width=device-width, initial-scale=1">
     <title>Orient Editor</title>
     <link href="/web/static/css/style.css" rel="stylesheet">
+    <link rel="stylesheet" href="https://unpkg.com/easymde/dist/easymde.min.css">
 </head>
 
 <body class="flex flex-col justify-between min-h-screen bg-slate-50">
@@ -25,7 +26,8 @@
         </p>
     </footer>
 
-    <script src="/web/static/js/htmx.min.js"></script>
+    <script src="https://unpkg.com/htmx.org@2.0.1"></script>
+    <script src="https://unpkg.com/easymde/dist/easymde.min.js"></script>
 </body>
 
 </html>
diff --git a/web/templates/rework-article.html b/web/templates/rework-article.html
index 2abab9f..a833587 100644
--- a/web/templates/rework-article.html
+++ b/web/templates/rework-article.html
@@ -14,6 +14,7 @@
         <label for="article-content">Artikel</label>
         <textarea name="article-content" placeholder="Artikel">{{.Article.Content}}</textarea>
     </div>
+    <input id="article-content" name="article-content" type="hidden" />
 
     <div>
         <span>Tags</span>
@@ -28,11 +29,6 @@
         </div>
     </div>
 
-    <div id="editor-images">
-        <input class="mb-2" name="article-image" type="file" hx-encoding="multipart/form-data" hx-post="/upload-image"
-            hx-swap="beforeend" hx-target="#editor-images" />
-    </div>
-
     <div class="btn-area">
         <input class="action-btn" type="submit" value="Senden" hx-post="/resubmit-article/{{.Article.ID}}"
             hx-target="#page-content" />
@@ -41,32 +37,33 @@
 </form>
 
 <script>
-    function copyToClipboard(text) {
-        event.preventDefault(); // Get-Request verhindern
+    var easyMDE = new EasyMDE({
+        element: document.getElementById('easyMDE'),
+        hideIcons: ['image'],
+        imageTexts: {sbInit: ''},
+        showIcons: ["code", "table", "upload-image"],
+        uploadImage: true,
 
-        var textarea = document.createElement("textarea");
-        textarea.textContent = text;
-        document.body.appendChild(textarea);
+        imageUploadFunction: function (file, onSuccess, onError) {
+            var formData = new FormData();
+            formData.append('article-image', file);
 
-        textarea.select();
-        try {
-            document.execCommand('copy');
-        } catch (err) {
-            console.warn('Fehler beim Kopieren', err);
-        }
-
-        document.body.removeChild(textarea);
-    }
+            fetch('/upload-image', {
+                method: 'POST',
+                body: formData
+            })
+                .then(response => response.json())
+                .then(data => {
+                    onSuccess(data);
+                })
+                .catch(error => {
+                    onError(error);
+                });
+        },
+    });
 
+    easyMDE.codemirror.on("change", () => {
+        document.getElementById('article-content').value = easyMDE.value();
+    });
 </script>
 {{end}}
-
-{{define "editor-images"}}
-{{if gt (len .) 0}}
-<div class="border px-2 py-1 rounded-md flex gap-4 justify-between">
-    <div class="self-center">{{.}}</div>
-    <button class="bg-slate-50 border my-2 px-3 py-2 rounded-md w-32 hover:bg-slate-100"
-        onclick="copyToClipboard('{{.}}')">Kopieren</button>
-</div>
-{{end}}
-{{end}}

From f9e16c7c36a6b0eba5cfa72dac0bfbb9ec067537 Mon Sep 17 00:00:00 2001
From: Jonathan <jonathan.thomczik@gmx.de>
Date: Mon, 10 Feb 2025 23:58:39 +0100
Subject: [PATCH 6/7] make headers id great again

---
 cmd/backend/markdown.go | 25 ++++++++++++++++++++++---
 1 file changed, 22 insertions(+), 3 deletions(-)

diff --git a/cmd/backend/markdown.go b/cmd/backend/markdown.go
index 59346c4..0ed1b8b 100644
--- a/cmd/backend/markdown.go
+++ b/cmd/backend/markdown.go
@@ -6,19 +6,38 @@ import (
 
 	"github.com/microcosm-cc/bluemonday"
 	"github.com/yuin/goldmark"
+	"github.com/yuin/goldmark/extension"
+	"github.com/yuin/goldmark/parser"
+	"github.com/yuin/goldmark/renderer/html"
 )
 
 func ConvertToHTML(md string) (string, error) {
 	var buf bytes.Buffer
 
-	if err := goldmark.Convert([]byte(md), &buf); err != nil {
+	// Goldmark-Instanz mit GFM und aktivierter Attribute-Unterstützung initialisieren.
+	gm := goldmark.New(
+		goldmark.WithExtensions(
+			extension.GFM,
+		),
+		goldmark.WithParserOptions(
+			parser.WithAttribute(),
+		),
+		goldmark.WithRendererOptions(
+			html.WithUnsafe(), // Falls du HTML-Inhalte erlauben möchtest
+		),
+	)
+
+	// Markdown in HTML konvertieren.
+	if err := gm.Convert([]byte(md), &buf); err != nil {
 		return "", fmt.Errorf("error converting markdown to html: %v", err)
 	}
 
+	// Bluemonday-Policy anpassen, sodass id-Attribute auf h1-h6 erlaubt sind.
 	p := bluemonday.UGCPolicy()
-	html := p.Sanitize(buf.String())
+	p.AllowAttrs("id").OnElements("h1", "h2", "h3", "h4", "h5", "h6")
+	htmlOutput := p.Sanitize(buf.String())
 
-	return html, nil
+	return htmlOutput, nil
 }
 
 func ConvertToPlain(md string) (string, error) {

From 547c9a55673bdd51ec68aa971ad095032167fe87 Mon Sep 17 00:00:00 2001
From: Jonathan <jonathan.thomczik@gmx.de>
Date: Thu, 13 Feb 2025 17:29:01 +0100
Subject: [PATCH 7/7] revert variable name

---
 cmd/backend/markdown.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/cmd/backend/markdown.go b/cmd/backend/markdown.go
index 0ed1b8b..1cd0d83 100644
--- a/cmd/backend/markdown.go
+++ b/cmd/backend/markdown.go
@@ -35,9 +35,9 @@ func ConvertToHTML(md string) (string, error) {
 	// Bluemonday-Policy anpassen, sodass id-Attribute auf h1-h6 erlaubt sind.
 	p := bluemonday.UGCPolicy()
 	p.AllowAttrs("id").OnElements("h1", "h2", "h3", "h4", "h5", "h6")
-	htmlOutput := p.Sanitize(buf.String())
+	html := p.Sanitize(buf.String())
 
-	return htmlOutput, nil
+	return html, nil
 }
 
 func ConvertToPlain(md string) (string, error) {