Compare commits

..

32 Commits

Author SHA1 Message Date
783d59805b Fixed small bug 2024-04-07 19:33:51 +02:00
b5f0fe8985 Added title, link and description to cli args. 2024-04-07 19:29:35 +02:00
d9bf79d5f8 Add space between first and last name 2024-04-07 19:01:21 +02:00
f98ab149a2 Added headings to all templates 2024-04-07 18:57:03 +02:00
822ca2b8ab Use UUID as filename and strip alt off of extension 2024-04-07 18:43:09 +02:00
af65180893 Reflect new cli arg in .air.toml 2024-04-07 11:36:34 +02:00
5615210be5 Add cli arg for domain 2024-04-07 11:32:38 +02:00
b88fb1643c Fixed bug 2024-04-07 11:12:07 +02:00
92189a4a51 Pictures are now handeled correctly 2024-04-07 10:58:07 +02:00
8dc8f02504 Changed rss package to tagged version 2024-04-04 17:17:55 +02:00
e3ce1d7b55 Simply provide RSS feed when GET request is received 2024-04-04 17:13:42 +02:00
532bc6490a Added XML encoding 2024-04-04 17:09:29 +02:00
84fa828b38 Provide RSS feed as file when pressing the button or typing the URL 2024-04-03 21:05:12 +02:00
a3c53b1b20 Changed URL patterns to be more specific 2024-04-03 20:24:54 +02:00
ca70fa6d4d Applied changes also to rework-article.html 2024-04-03 19:52:16 +02:00
972b8cac19 Corrected vertical gap size for tags when wrapping onto the next line 2024-04-03 19:51:27 +02:00
d0605660f7 Made tags wrap onto the next line when overflowing parent container 2024-04-03 19:48:42 +02:00
5d2d841aba Changed tag length to 50 characters 2024-04-03 19:47:27 +02:00
d62c5a4078 Changed visual layout for to-be-published articles 2024-04-03 18:12:28 +02:00
803c5bbdbd Slightly changed button color and changed body height to be min-100vh 2024-04-03 04:50:25 +02:00
c74bdeba72 Only show logout button in hub 2024-04-02 21:35:34 +02:00
717f1c813b Add setup script for DB 2024-04-02 19:38:16 +02:00
52797760bb Also, handle first user differently under the hood 2024-04-02 19:37:53 +02:00
8711ba0629 Handle first user differently from the rest 2024-04-01 19:26:18 +02:00
ed51d28c65 Corrected back button class for unpublished articles 2024-04-01 15:58:36 +02:00
7e7de28b14 Streamlined selection of rejected and unpublished articles 2024-04-01 15:42:51 +02:00
0139f7ab9a Use ID in path rather than an invisible input when publishing, rejecting or resubmitting an article 2024-04-01 15:30:24 +02:00
7fc115bcc3 Refined look of rejected and unpublished articles 2024-04-01 14:38:31 +02:00
ae90f693f6 no more style.css 2024-04-01 14:27:42 +02:00
a730e11b4a Styled with tailwind css 2024-04-01 14:22:59 +02:00
959e1e96b3 Fix typo 2024-03-31 05:00:57 +02:00
68b052625f Fixed bug with specifying port 2024-03-30 10:22:51 +01:00
31 changed files with 733 additions and 337 deletions

54
.air.toml Normal file
View File

@ -0,0 +1,54 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = [
"-desc 'Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität'",
"-domain localhost:8080",
"-key tmp/key.gob",
"-link https://distrikt-ni-st.de",
"-log tmp/cpolis.log",
"-pics tmp/pics",
"-rss tmp/orientexpress_alle.rss",
"-title 'Freimaurer Distrikt Niedersachsen und Sachsen-Anhalt'",
"-web web"
]
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ./cmd/main.go"
delay = 0
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html", "css"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true

1
.gitignore vendored
View File

@ -23,3 +23,4 @@ go.work
# Custom stuff
tmp/
style.css

View File

@ -7,12 +7,16 @@ import (
)
type CliArgs struct {
Description string
DBName string
Domain string
KeyFile string
Link string
LogFile string
Port string
PicsDir string
RSSFile string
Title string
WebDir string
}
@ -20,13 +24,17 @@ func HandleCliArgs() (*CliArgs, error) {
var err error
cliArgs := new(CliArgs)
keyFile := flag.String("key", "/var/www/cpolis/cpolis.key", "key file")
logFile := flag.String("log", "/var/log/cpolis.log", "log file")
picsDir := flag.String("pics", "/var/www/cpolis/pics", "pictures directory")
cliArgs.Port = fmt.Sprint(":", flag.Int("port", 8080, "port"))
rssFile := flag.String("rss", "/var/www/cpolis/cpolis.rss", "RSS file")
webDir := flag.String("web", "/var/www/cpolis/web", "web directory")
flag.StringVar(&cliArgs.DBName, "db", "cpolis", "DB name")
flag.StringVar(&cliArgs.Description, "desc", "Description", "Channel description")
flag.StringVar(&cliArgs.Domain, "domain", "", "domain name")
keyFile := flag.String("key", "/var/www/cpolis/cpolis.key", "key file")
flag.StringVar(&cliArgs.Link, "link", "Link", "Channel Link")
logFile := flag.String("log", "/var/log/cpolis.log", "log file")
flag.StringVar(&cliArgs.PicsDir, "pics", "pics", "pictures directory")
port := flag.Int("port", 8080, "port")
rssFile := flag.String("rss", "/var/www/cpolis/cpolis.rss", "RSS file")
flag.StringVar(&cliArgs.Title, "title", "Title", "Channel title")
webDir := flag.String("web", "/var/www/cpolis/web", "web directory")
flag.Parse()
cliArgs.KeyFile, err = filepath.Abs(*keyFile)
@ -39,11 +47,13 @@ func HandleCliArgs() (*CliArgs, error) {
return nil, fmt.Errorf("error finding absolute path for LogFile: %v", err)
}
cliArgs.PicsDir, err = filepath.Abs(*picsDir)
_, err = filepath.Abs(cliArgs.PicsDir)
if err != nil {
return nil, fmt.Errorf("error finding absolute path for PicsDir: %v", err)
}
cliArgs.Port = fmt.Sprint(":", *port)
cliArgs.RSSFile, err = filepath.Abs(*rssFile)
if err != nil {
return nil, fmt.Errorf("error finding absolute path for RSSFile: %v", err)

View File

@ -97,7 +97,7 @@ func GenerateRSS(db *model.DB, title, link, desc string) (*string, error) {
channel.Items = append(channel.Items, &rss.Item{
Title: articleTitle,
Author: user.FirstName + user.LastName,
Author: user.FirstName + " " + user.LastName,
PubDate: article.Created.Format(time.RFC1123Z),
Description: articleDescription,
Content: &rss.Content{Value: articleContent},
@ -107,7 +107,7 @@ func GenerateRSS(db *model.DB, title, link, desc string) (*string, error) {
feed := rss.NewFeed()
feed.Channels = append(feed.Channels, channel)
rss, err := feed.ToXML()
rss, err := feed.ToXML("UTF-8")
if err != nil {
return nil, fmt.Errorf("error converting RSS feed to XML: %v", err)
}

View File

@ -49,34 +49,33 @@ func main() {
http.FileServer(http.Dir(args.WebDir+"/static/"))))
mux.HandleFunc("/", view.HomePage(args, db, store))
mux.HandleFunc("GET /create-tag/", view.CreateTag(args))
mux.HandleFunc("GET /create-user/", view.CreateUser(args))
mux.HandleFunc("GET /edit-user/", view.EditUser(args, db, store))
mux.HandleFunc("GET /hub/", view.ShowHub(args, db, store))
mux.HandleFunc("GET /logout/", view.Logout(args, store))
mux.HandleFunc("GET /publish-issue/", view.PublishLatestIssue(args, db, store))
mux.HandleFunc("GET /rejected-articles/", view.ShowRejectedArticles(args, db, store))
mux.HandleFunc("GET /rss/", view.ShowRSS(args,
db,
"Freimaurer Distrikt Niedersachsen und Sachsen-Anhalt",
"https://distrikt-ni-st.de",
"Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität",
))
mux.HandleFunc("GET /this-issue/", view.ShowCurrentArticles(args, db))
mux.HandleFunc("GET /unpublished-articles/", view.ShowUnpublishedArticles(args, db))
mux.HandleFunc("GET /write-article/", view.WriteArticle(args, db))
mux.HandleFunc("GET /create-tag", view.CreateTag(args))
mux.HandleFunc("GET /create-user", view.CreateUser(args))
mux.HandleFunc("GET /edit-user", view.EditUser(args, db, store))
mux.HandleFunc("GET /hub", view.ShowHub(args, db, store))
mux.HandleFunc("GET /logout", view.Logout(args, store))
mux.HandleFunc("GET /publish-article/{id}", view.PublishArticle(args, db, store))
mux.HandleFunc("GET /publish-issue", view.PublishLatestIssue(args, db, store))
mux.HandleFunc("GET /reject-article/{id}", view.RejectArticle(args, db, store))
mux.HandleFunc("GET /rejected-articles", view.ShowRejectedArticles(args, db, store))
mux.HandleFunc("GET /review-rejected-article/{id}", view.ReviewRejectedArticle(args, db, store))
mux.HandleFunc("GET /review-unpublished-article/{id}", view.ReviewUnpublishedArticle(args, db, store))
mux.HandleFunc("GET /rss", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, args.RSSFile)
})
mux.HandleFunc("GET /pics/{pic}", view.ServeImage(args, store))
mux.HandleFunc("GET /this-issue", view.ShowCurrentArticles(args, db))
mux.HandleFunc("GET /unpublished-articles", view.ShowUnpublishedArticles(args, db))
mux.HandleFunc("GET /write-article", view.WriteArticle(args, db))
mux.HandleFunc("POST /add-tag/", view.AddTag(args, db, store))
mux.HandleFunc("POST /add-user/", view.AddUser(args, db, store))
mux.HandleFunc("POST /login/", view.Login(args, db, store))
mux.HandleFunc("POST /publish-article/", view.PublishArticle(args, db, store))
mux.HandleFunc("POST /reject-article/", view.RejectArticle(args, db, store))
mux.HandleFunc("POST /resubmit-article/", view.ResubmitArticle(args, db, store))
mux.HandleFunc("POST /review-rejected-article/", view.ReviewRejectedArticle(args, db, store))
mux.HandleFunc("POST /review-unpublished-article/", view.ReviewUnpublishedArticle(args, db, store))
mux.HandleFunc("POST /submit-article/", view.SubmitArticle(args, db, store))
mux.HandleFunc("POST /update-user/", view.UpdateUser(args, db, store))
mux.HandleFunc("POST /upload-image/", view.UploadImage(args))
mux.HandleFunc("POST /add-first-user", view.AddFirstUser(args, db, store))
mux.HandleFunc("POST /add-tag", view.AddTag(args, db, store))
mux.HandleFunc("POST /add-user", view.AddUser(args, db, store))
mux.HandleFunc("POST /login", view.Login(args, db, store))
mux.HandleFunc("POST /resubmit-article/{id}", view.ResubmitArticle(args, db, store))
mux.HandleFunc("POST /submit-article", view.SubmitArticle(args, db, store))
mux.HandleFunc("POST /update-user", view.UpdateUser(args, db, store))
mux.HandleFunc("POST /upload-image", view.UploadImage(args))
log.Fatalln(http.ListenAndServe(args.Port, mux))
}

View File

@ -1,6 +1,8 @@
package model
import (
"context"
"database/sql"
"fmt"
"log"
@ -198,3 +200,71 @@ func (db *DB) UpdateUserAttributes(id int64, user, first, last, oldPass, newPass
return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries)
}
func (db *DB) AddFirstUser(u *User, pass string) (int64, error) {
var numUsers int64
txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable}
selectQuery := "SELECT COUNT(*) FROM users"
insertQuery := `
INSERT INTO users (username, password, first_name, last_name, role)
VALUES (?, ?, ?, ?, ?)
`
for i := 0; i < TxMaxRetries; i++ {
id, err := func() (int64, error) {
tx, err := db.BeginTx(context.Background(), txOptions)
if err != nil {
return 0, fmt.Errorf("error starting transaction: %v", err)
}
if err := tx.QueryRow(selectQuery).Scan(&numUsers); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return 0, fmt.Errorf("error getting ID of %v: %v", u.UserName, err)
}
if numUsers != 0 {
if err = tx.Commit(); err != nil {
return 0, fmt.Errorf("error committing transaction: %v", err)
}
return 2, nil
}
hashedPass, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return 0, fmt.Errorf("error creating password hash: %v", err)
}
result, err := tx.Exec(insertQuery, u.UserName, string(hashedPass), u.FirstName, u.LastName, u.Role)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return 0, fmt.Errorf("error inserting new user %v into DB: %v", u.UserName, err)
}
id, err := result.LastInsertId()
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return 0, fmt.Errorf("error inserting user into DB: %v", err)
}
if err = tx.Commit(); err != nil {
return 0, fmt.Errorf("error committing transaction: %v", err)
}
return id, nil
}()
if err == nil {
return id, nil
}
log.Println(err)
wait(i)
}
return 0, fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries)
}

View File

@ -7,9 +7,12 @@ import (
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"streifling.com/jason/cpolis/cmd/control"
"streifling.com/jason/cpolis/cmd/model"
)
@ -92,7 +95,7 @@ func SubmitArticle(c *control.CliArgs, db *model.DB, s *control.CookieStore) htt
func ResubmitArticle(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PostFormValue("article-id"), 10, 64)
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -197,26 +200,53 @@ func ShowRejectedArticles(c *control.CliArgs, db *model.DB, s *control.CookieSto
func ReviewUnpublishedArticle(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
type htmlData struct {
Article *model.Article
Title string
Description string
Content template.HTML
Tags []*model.Tag
ID int64
}
var err error
data := new(htmlData)
id, err := strconv.ParseInt(r.PostFormValue("id"), 10, 64)
data.ID, err = strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Article, err = db.GetArticle(id)
article, err := db.GetArticle(data.ID)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Tags, err = db.GetArticleTags(id)
data.Title, err = control.ConvertToPlain(article.Title)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Description, err = control.ConvertToPlain(article.Description)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
content, err := control.ConvertToHTML(article.Content)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Content = template.HTML(content)
data.Tags, err = db.GetArticleTags(data.ID)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -238,7 +268,7 @@ func ReviewRejectedArticle(c *control.CliArgs, db *model.DB, s *control.CookieSt
}
data := new(htmlData)
id, err := strconv.ParseInt(r.PostFormValue("id"), 10, 64)
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -278,7 +308,7 @@ func ReviewRejectedArticle(c *control.CliArgs, db *model.DB, s *control.CookieSt
func PublishArticle(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PostFormValue("id"), 10, 64)
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -308,12 +338,7 @@ func PublishArticle(c *control.CliArgs, db *model.DB, s *control.CookieStore) ht
return
}
feed, err := control.GenerateRSS(
db,
"Freimaurer Distrikt Niedersachsen und Sachsen-Anhalt",
"https://distrikt-ni-st.de",
"Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität",
)
feed, err := control.GenerateRSS(db, c.Title, c.Link, c.Description)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -333,7 +358,7 @@ func PublishArticle(c *control.CliArgs, db *model.DB, s *control.CookieStore) ht
func RejectArticle(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PostFormValue("id"), 10, 64)
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -385,8 +410,17 @@ func UploadImage(c *control.CliArgs) http.HandlerFunc {
}
defer file.Close()
filename := fmt.Sprint(c.PicsDir, time.Now().Format("2006-01-02_15:04:05"), "-", header.Filename)
img, err := os.Create(filename)
nameStrings := strings.Split(header.Filename, ".")
extension := "." + nameStrings[len(nameStrings)-1]
filename := fmt.Sprint(uuid.New(), extension)
absFilepath, err := filepath.Abs(fmt.Sprint(c.PicsDir, "/", filename))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
img, err := os.Create(absFilepath)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -400,7 +434,9 @@ func UploadImage(c *control.CliArgs) 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", fmt.Sprint("![", header.Filename, "](", filename, ")"))
template.Must(tmpl, err).ExecuteTemplate(w, "editor-images", imgMD)
}
}

22
cmd/view/images.go Normal file
View File

@ -0,0 +1,22 @@
package view
import (
"log"
"net/http"
"path/filepath"
"streifling.com/jason/cpolis/cmd/control"
)
func ServeImage(c *control.CliArgs, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
absFilepath, err := filepath.Abs(c.PicsDir)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.ServeFile(w, r, absFilepath+"/"+r.PathValue("pic"))
}
}

View File

@ -1,95 +0,0 @@
package view
import (
"fmt"
"html/template"
"log"
"net/http"
"time"
"git.streifling.com/jason/rss"
"streifling.com/jason/cpolis/cmd/control"
"streifling.com/jason/cpolis/cmd/model"
)
func ShowRSS(c *control.CliArgs, db *model.DB, title, link, desc string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
channel := &rss.Channel{
Title: title,
Link: link,
Description: desc,
Items: make([]*rss.Item, 0),
}
articles, err := db.GetCertainArticles(true, false)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for _, article := range articles {
tags, err := db.GetArticleTags(article.ID)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tagNames := make([]string, 0)
for _, tag := range tags {
tagNames = append(tagNames, tag.Name)
}
tagNames = append(tagNames, fmt.Sprint("Orient Express ", article.IssueID))
user, err := db.GetUser(article.AuthorID)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
articleTitle, err := control.ConvertToPlain(article.Title)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
articleDescription, err := control.ConvertToPlain(article.Description)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
articleContent, err := control.ConvertToHTML(article.Content)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
channel.Items = append(channel.Items, &rss.Item{
Title: articleTitle,
Author: user.FirstName + user.LastName,
PubDate: article.Created.Format(time.RFC1123Z),
Description: articleDescription,
Content: &rss.Content{Value: articleContent},
Categories: tagNames,
})
}
feed := rss.NewFeed()
feed.Channels = append(feed.Channels, channel)
rss, err := feed.ToXML()
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
files := []string{c.WebDir + "/templates/index.html", c.WebDir + "/templates/feed.rss"}
tmpl, err := template.ParseFiles(files...)
template.Must(tmpl, err).Execute(w, rss)
}
}

View File

@ -36,7 +36,7 @@ func HomePage(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.Han
files := []string{c.WebDir + "/templates/index.html"}
if numRows == 0 {
files = append(files, c.WebDir+"/templates/add-user.html")
files = append(files, c.WebDir+"/templates/first-user.html")
tmpl, err := template.ParseFiles(files...)
template.Must(tmpl, err).Execute(w, nil)
} else {

View File

@ -88,37 +88,13 @@ func AddUser(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.Hand
return
}
htmlData.ID, err = db.AddUser(htmlData.User, pass)
_, err = db.AddUser(htmlData.User, pass)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if htmlData.ID == 1 {
htmlData.Role = model.Admin
if err = db.UpdateAttributes(
&model.Attribute{Table: "users", ID: id, AttName: "role", Value: htmlData.Role},
); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := saveSession(w, r, s, htmlData.User); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if _, err := db.AddIssue(); 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", 0)
}
@ -214,3 +190,78 @@ func UpdateUser(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.H
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int))
}
}
func AddFirstUser(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var err error
htmlData := UserData{
User: &model.User{
UserName: r.PostFormValue("username"),
FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"),
Role: model.Admin,
},
}
pass := r.PostFormValue("password")
pass2 := r.PostFormValue("password2")
if len(htmlData.UserName) == 0 || len(htmlData.FirstName) == 0 ||
len(htmlData.LastName) == 0 || len(pass) == 0 || len(pass2) == 0 {
htmlData.Msg = "Alle Felder müssen ausgefüllt werden."
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData)
return
}
userString, stringLen, ok := checkUserStrings(htmlData.User)
if !ok {
htmlData.Msg = fmt.Sprint(userString, " ist zu lang. Maximal ",
stringLen, " Zeichen erlaubt.")
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData)
return
}
id, _ := db.GetID(htmlData.UserName)
if id != 0 {
htmlData.Msg = fmt.Sprint(htmlData.UserName,
" ist bereits vergeben. Bitte anderen Benutzernamen wählen.")
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData)
return
}
if pass != pass2 {
htmlData.Msg = "Die Passwörter stimmen nicht überein."
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData)
return
}
htmlData.ID, err = db.AddFirstUser(htmlData.User, pass)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if htmlData.ID > 1 {
errString := "error: there is already a first user"
log.Println(errString)
http.Error(w, errString, http.StatusInternalServerError)
return
}
if err := saveSession(w, r, s, htmlData.User); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if _, err := db.AddIssue(); 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", 0)
}
}

50
create_db.sql Normal file
View File

@ -0,0 +1,50 @@
DROP TABLE IF EXISTS articles_tags;
DROP TABLE IF EXISTS tags;
DROP TABLE IF EXISTS articles;
DROP TABLE IF EXISTS issues;
DROP TABLE IF EXISTS users;
CREATE TABLE users (
id INT AUTO_INCREMENT,
username VARCHAR(15) NOT NULL UNIQUE,
password VARCHAR(60) NOT NULL,
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL,
role INT NOT NULL,
PRIMARY KEY(id)
);
CREATE TABLE issues (
id INT AUTO_INCREMENT,
published BOOL NOT NULL,
PRIMARY KEY(id)
);
CREATE TABLE articles (
id INT AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
description TEXT NOT NULL,
content TEXT NOT NULL,
published BOOL NOT NULL,
rejected BOOL NOT NULL,
author_id INT NOT NULL,
issue_id INT NOT NULL,
PRIMARY KEY(id),
FOREIGN KEY(author_id) REFERENCES users(id),
FOREIGN KEY(issue_id) REFERENCES issues(id)
);
CREATE TABLE tags (
id INT AUTO_INCREMENT,
name VARCHAR(50) NOT NULL UNIQUE,
PRIMARY KEY(id)
);
CREATE TABLE articles_tags (
article_id INT,
tag_id INT,
PRIMARY KEY(article_id, tag_id),
FOREIGN KEY(article_id) REFERENCES articles(id),
FOREIGN KEY(tag_id) REFERENCES tags(id)
);

3
go.mod
View File

@ -3,8 +3,9 @@ module streifling.com/jason/cpolis
go 1.22.0
require (
git.streifling.com/jason/rss v0.0.0-20240305164907-524bf9676188
git.streifling.com/jason/rss v0.1.2
github.com/go-sql-driver/mysql v1.7.1
github.com/google/uuid v1.6.0
github.com/gorilla/sessions v1.2.2
github.com/microcosm-cc/bluemonday v1.0.26
github.com/yuin/goldmark v1.7.0

6
go.sum
View File

@ -1,11 +1,13 @@
git.streifling.com/jason/rss v0.0.0-20240305164907-524bf9676188 h1:C8M/j3f+cl5Y7YfGpU/ynb/SC/4tTYMDsyGFt3rswM8=
git.streifling.com/jason/rss v0.0.0-20240305164907-524bf9676188/go.mod h1:gpZF0nZbQSstMpyHD9DTAvlQEG7v4pjO5c7aIMWM4Jg=
git.streifling.com/jason/rss v0.1.2 h1:UB3UHJXMt5WDDh9y8n0Z6nS1XortbPXjEr7QZTdovY4=
git.streifling.com/jason/rss v0.1.2/go.mod h1:gpZF0nZbQSstMpyHD9DTAvlQEG7v4pjO5c7aIMWM4Jg=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=

10
tailwind.config.js Normal file
View File

@ -0,0 +1,10 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./web/templates/*.html"],
theme: {
extend: {}
},
plugins: [
require('@tailwindcss/typography')
],
}

41
web/static/css/input.css Normal file
View File

@ -0,0 +1,41 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
width: 800px;
@apply mx-auto text-slate-900;
}
h2 {
@apply font-bold mb-2 text-2xl;
}
form {
@apply flex flex-col gap-y-3;
}
input[type="file"] {
@apply border rounded-md w-full;
}
input[type="password"],
input[type="text"] {
@apply border h-8 rounded-md;
}
textarea {
@apply border h-32 rounded-md;
}
.btn-area {
@apply flex gap-4 mt-4;
}
.btn {
@apply bg-slate-200 border my-2 px-3 py-2 rounded-md w-full hover:bg-slate-100;
}
.action-btn {
@apply bg-slate-800 border my-2 px-3 py-2 rounded-md text-slate-50 w-full hover:bg-slate-700;
}

View File

@ -1,10 +1,12 @@
{{define "page-content"}}
<h2>Neuer Benutzer</h2>
<h2>Neuer Tag</h2>
<form>
<input required name="tag" placeholder="Tag" type="text" />
<input type="submit" value="Anlegen" hx-post="/add-tag/" hx-target="#page-content" />
<input required name="tag" placeholder="Tag eingeben" type="text" />
<div class="btn-area">
<input class="action-btn" type="submit" value="Anlegen" hx-post="/add-tag" hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
</div>
</form>
<button hx-get="/hub/" hx-target="#page-content">Abbrechen</button>
{{end}}

View File

@ -1,33 +1,55 @@
{{define "page-content"}}
<h2>Neuer Benutzer</h2>
<form>
<div class="grid grid-cols-3 gap-4">
<div>
<input required name="username" placeholder="Benutzername" type="text" value="{{.UserName}}" />
<input required name="password" placeholder="Passwort" type="password" />
<input required name="password2" placeholder="Passwort wiederholen" type="password" />
</div>
<div>
<input required name="first-name" placeholder="Vorname" type="text" value="{{.FirstName}}" />
<input required name="last-name" placeholder="Nachname" type="text" value="{{.LastName}}" />
<label for="username">Benutzername</label>
<input class="w-full" required name="username" type="text" value="{{.UserName}}" />
</div>
<div>
<label for="password">Passwort</label>
<input class="w-full" required name="password" placeholder="***" type="password" />
</div>
<div>
<label for="password2">Passwort wiederholen</label>
<input class="w-full" required name="password2" placeholder="***" type="password" />
</div>
<div>
<label for="first-name">Vorname</label>
<input class="w-full" required name="first-name" type="text" value="{{.FirstName}}" />
</div>
<div>
<label for="last-name">Nachname</label>
<input class="w-full" required name="last-name" type="text" value="{{.LastName}}" />
</div>
</div>
<div class="flex gap-4">
<div>
<input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} />
<label for="author">Autor</label>
</div>
<div>
<input required id="editor" name="role" type="radio" value="2" {{if eq .Role 2 }}checked{{end}} />
<label for="editor">Redakteur</label>
</div>
<div>
<input required id="publisher" name="role" type="radio" value="1" {{if eq .Role 1 }}checked{{end}} />
<label for="publisher">Herausgeber</label>
</div>
<div>
<input required id="admin" name="role" type="radio" value="0" {{if eq .Role 0 }}checked{{end}} />
<label for="admin">Admin</label>
</div>
</div>
<input type="submit" value="Anlegen" hx-post="/add-user/" hx-target="#page-content" />
<div class="btn-area">
<input class="action-btn" type="submit" value="Anlegen" hx-post="/add-user" hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
</div>
</form>
<button hx-get="/hub/" hx-target="#page-content">Abbrechen</button>
<script>
var msg = "{{.Msg}}";
if (msg != "") {

View File

@ -1,13 +1,17 @@
{{define "page-content"}}
<div>
<h2>Aktuelle Artikel</h2>
<div class="flex flex-col gap-4">
{{range .}}
<div>
<h1>{{.Title}}</h1>
<div class="border px-2 py-1 rounded-md">
<h1 class="font-bold text-2xl">{{.Title}}</h1>
<p>{{.Description}}</p>
</div>
{{end}}
</div>
<button hx-get="/publish-issue/" hx-target="#page-content">Ausgabe publizieren</button>
<button hx-get="/hub/" hx-target="#page-content">Abbrechen</button>
<div class="btn-area">
<button class="action-btn" hx-get="/publish-issue" hx-target="#page-content">Ausgabe publizieren</button>
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
</div>
{{end}}

View File

@ -1,19 +1,38 @@
{{define "page-content"}}
<h2>Benutzerdaten bearbeiten</h2>
<form>
<div class="grid grid-cols-3 gap-4">
<div>
<input name="username" type="text" value="{{.UserName}}" />
<input name="first-name" type="text" value="{{.FirstName}}" />
<input name="last-name" type="text" value="{{.LastName}}" />
<label for="username">Benutzername</label>
<input class="w-full" name="username" type="text" value="{{.UserName}}" />
</div>
<div>
<label for="first-name">Vorname</label>
<input class="w-full" name="first-name" type="text" value="{{.FirstName}}" />
</div>
<div>
<label for="last-name">Nachname</label>
<input class="w-full" name="last-name" type="text" value="{{.LastName}}" />
</div>
<div>
<label for="old-password">Altes Passwort</label>
<input class="w-full" name="old-password" placeholder="***" type="password" />
</div>
<div>
<label for="password">Passwort</label>
<input class="w-full" name="password" placeholder="***" type="password" />
</div>
<div>
<label for="password2">Passwort wiederholen</label>
<input class="w-full" name="password2" placeholder="***" type="password" />
</div>
</div>
<div>
<input name="old-password" placeholder="Altes Passwort" type="password" />
<input name="password" placeholder="Neues Passwort" type="password" />
<input name="password2" placeholder="Wiederholen" type="password" />
<div class="btn-area">
<input class="action-btn" type="submit" value="Aktualisieren" hx-post="/update-user"
hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
</div>
<input type="submit" value="Aktualisieren" hx-post="/update-user/" hx-target="#page-content" />
</form>
<button hx-get="/hub/" hx-target="#page-content">Abbrechen</button>
{{end}}

View File

@ -1,13 +1,23 @@
{{define "page-content"}}
<h2>Editor</h2>
<form>
<div>
<input name="article-title" placeholder="Titel" type="text" />
<textarea name="article-description" placeholder="Beschreibung"></textarea>
<textarea name="article-content" placeholder="Artikel"></textarea>
<div class="flex flex-col gap-y-1">
<label for="article-title">Titel</label>
<input name="article-title" type="text" />
</div>
<div class="flex flex-col">
<label for="article-description">Beschreibung</label>
<textarea name="article-description"></textarea>
</div>
<div class="flex flex-col">
<label for="article-content">Artikel</label>
<textarea name="article-content"></textarea>
</div>
<div>
<span>Tags</span>
<div class="flex flex-wrap gap-x-4">
{{range .}}
<div>
<input id="{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" />
@ -15,17 +25,19 @@
</div>
{{end}}
</div>
</div>
<div id="editor-images">
<input name="article-image" type="file" hx-encoding="multipart/form-data" hx-post="/upload-image/"
<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>
<input type="submit" value="Senden" hx-post="/submit-article/" hx-target="#page-content" />
<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>
</form>
<button hx-get="/hub/" hx-target="#page-content">Abbrechen</button>
<script>
function copyToClipboard(text) {
event.preventDefault(); // Get-Request verhindern
@ -49,9 +61,10 @@
{{define "editor-images"}}
{{if gt (len .) 0}}
<div>
{{.}}
<button onclick="copyToClipboard('{{.}}')">Kopieren</button>
<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}}

View File

@ -1,3 +0,0 @@
{{define "page-content"}}
{{.}}
{{end}}

View File

@ -0,0 +1,39 @@
{{define "page-content"}}
<h2>Erster Benutzer (Administrator)</h2>
<form>
<div class="grid grid-cols-3 gap-4">
<div>
<label for="username">Benutzername</label>
<input class="w-full" required name="username" type="text" value="{{.UserName}}" />
</div>
<div>
<label for="password">Passwort</label>
<input class="w-full" required name="password" placeholder="***" type="password" />
</div>
<div>
<label for="password2">Passwort wiederholen</label>
<input class="w-full" required name="password2" placeholder="***" type="password" />
</div>
<div>
<label for="first-name">Vorname</label>
<input class="w-full" required name="first-name" type="text" value="{{.FirstName}}" />
</div>
<div>
<label for="last-name">Nachname</label>
<input class="w-full" required name="last-name" type="text" value="{{.LastName}}" />
</div>
</div>
<div class="btn-area">
<input class="action-btn" type="submit" value="Anlegen" hx-post="/add-first-user" hx-target="#page-content" />
</div>
</form>
<script>
var msg = "{{.Msg}}";
if (msg != "") {
alert(msg);
}
</script>
{{end}}

View File

@ -1,25 +1,45 @@
{{define "page-content"}}
<h2>Hub</h2>
<div>
<button hx-get="/write-article/" hx-target="#page-content">Artikel schreiben</button>
<button hx-get="/rejected-articles/" hx-target="#page-content">Abgelehnte Artikel</button>
<button hx-get="/rss/" hx-target="#page-content">RSS Feed</button>
<button hx-get="/edit-user/" hx-target="#page-content">Benutzer bearbeiten</button>
</div>
{{if lt . 3}}
<div>
<button hx-get="/unpublished-articles/" hx-target="#page-content">Unveröffentlichte Artikel</button>
<button hx-get="/create-tag/" hx-target="#page-content">Neuer Tag</button>
<div class="flex flex-col gap-4">
<button class="btn" hx-get="/logout" hx-target="#page-content">Abmelden</button>
<div class="mb-3">
<h2>Autor</h2>
<div class="grid grid-cols-2 gap-x-4 gap-y-2">
<button class="btn" hx-get="/write-article" hx-target="#page-content">Artikel schreiben</button>
<button class="btn" hx-get="/rejected-articles" hx-target="#page-content">Abgelehnte Artikel</button>
<a class="btn text-center" href="/rss">RSS Feed</a>
<button class="btn" hx-get="/edit-user" hx-target="#page-content">Benutzer bearbeiten</button>
</div>
</div>
{{if lt . 3}}
<div class="mb-3">
<h2>Redakteur</h2>
<div class="grid grid-cols-2 gap-4">
<button class="btn" hx-get="/unpublished-articles" hx-target="#page-content">
Unveröffentlichte Artikel
</button>
<button class="btn" hx-get="/create-tag" hx-target="#page-content">Neuer Tag</button>
</div>
</div>
{{end}}
{{if lt . 2}}
<div class="mb-3">
<h2>Herausgeber</h2>
<div class="grid grid-cols-2 gap-4">
<button class="btn" hx-get="/this-issue" hx-target="#page-content">Diese Ausgabe</button>
</div>
</div>
{{end}}
{{if eq . 0}}
<div class="mb-3">
<h2>Administrator</h2>
<div class="grid grid-cols-2 gap-4">
<button class="btn" hx-get="/create-user" hx-target="#page-content">Benutzer hinzufügen</button>
</div>
</div>
{{end}}
</div>
{{end}}
{{if lt . 2}}
<div>
<button hx-get="/this-issue/" hx-target="#page-content">Diese Ausgabe</button>
</div>
{{end}}
{{if eq . 0}}
<div>
<button hx-get="/create-user/" hx-target="#page-content">Benutzer hinzufügen</button>
</div>
{{end}}
{{end}}

View File

@ -5,26 +5,27 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Orient Editor</title>
<link href="web/static/css/style.css" rel="stylesheet">
<link href="/web/static/css/style.css" rel="stylesheet">
</head>
<body>
<header>
<h1>Orient Editor</h1>
<button hx-get="logout" hx-target="#page-content">Abmelden</button>
<body class="flex flex-col justify-between min-h-screen bg-slate-50">
<header class="my-8">
<h1 class="font-bold text-4xl text-center">Orient Editor</h1>
</header>
<main>
<main class="mx-4">
<div id="page-content">
{{template "page-content" .}}
</div>
<script src="web/static/js/htmx.min.js"></script>
</main>
<footer>
<p>&copy; 2024 Jason Streifling. Alle Rechte vorbehalten.</p>
<footer class="my-8">
<p class="text-center text-gray-500 dark:text-gray-400">
&copy; 2024 Jason Streifling. Alle Rechte vorbehalten.
</p>
</footer>
<script src="/web/static/js/htmx.min.js"></script>
</body>
</html>

View File

@ -1,11 +1,12 @@
{{define "page-content"}}
<h2>Anmeldung</h2>
<form>
<div>
<input name="username" placeholder="Benutzername" type="text" />
<input name="password" placeholder="Passwort" type="password" />
<div class="btn-area">
<input class="w-full" name="username" placeholder="Benutzername" type="text" />
<input class="w-full" name="password" placeholder="Passwort" type="password" />
</div>
<input type="submit" value="Anmelden" hx-post="/login/" hx-target="#page-content" />
<input class="action-btn" type="submit" value="Anmelden" hx-post="/login" hx-target="#page-content" />
</form>
{{end}}

View File

@ -1,18 +1,16 @@
{{define "page-content"}}
<form>
<div>
<h2>Abgelehnte Artikel</h2>
<div class="flex flex-col gap-4">
{{range .RejectedArticles}}
<div>
{{if index $.MyIDs .ID}}
<input required id="{{.ID}}" name="id" type="radio" value="{{.ID}}" />
<label for="{{.ID}}">{{.Title}}</label>
<button class="btn" hx-get="/review-rejected-article/{{.ID}}" hx-target="#page-content">
<h1 class="font-bold text-2xl">{{.Title}}</h1>
<p>{{.Description}}</p>
</button>
{{end}}
</div>
{{end}}
</div>
<input type="submit" value="Auswählen" hx-post="/review-rejected-article/" hx-target="#page-content" />
</form>
<button hx-get="/hub/" hx-target="#page-content">Zurück</button>
<button class="action-btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
</div>
{{end}}

View File

@ -1,14 +1,23 @@
{{define "page-content"}}
<h2>Editor</h2>
<form>
<div>
<input name="article-title" placeholder="Titel" type="text" value="{{.Article.Title}}" />
<textarea name="article-description" placeholder="Beschreibung">{{.Article.Description}}</textarea>
<div class="flex flex-col gap-y-1">
<label for="article-title">Titel</label>
<input name="article-title" type="text" value="{{.Article.Title}}" />
</div>
<div class="flex flex-col">
<label for="article-description">Beschreibung</label>
<textarea name="article-description">{{.Article.Description}}</textarea>
</div>
<div class="flex flex-col">
<label for="article-content">Artikel</label>
<textarea name="article-content" placeholder="Artikel">{{.Article.Content}}</textarea>
<input name="article-id" type="hidden" value="{{.Article.ID}}" />
</div>
<div>
<span>Tags</span>
<div class="flex flex-wrap gap-x-4">
{{range .Tags}}
<div>
<input id="tag-{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" {{if index $.Selected
@ -17,17 +26,20 @@
</div>
{{end}}
</div>
</div>
<div id="editor-images">
<input name="article-image" type="file" hx-encoding="multipart/form-data" hx-post="/upload-image/"
<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>
<input type="submit" value="Senden" hx-post="/resubmit-article/" hx-target="#page-content" />
<div class="btn-area">
<input class="action-btn" type="submit" value="Senden" hx-post="/resubmit-article/{{.Article.ID}}"
hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
</div>
</form>
<button hx-get="/hub/" hx-target="#page-content">Zurück</button>
<script>
function copyToClipboard(text) {
event.preventDefault(); // Get-Request verhindern
@ -51,9 +63,10 @@
{{define "editor-images"}}
{{if gt (len .) 0}}
<div>
{{.}}
<button onclick="copyToClipboard('{{.}}')">Kopieren</button>
<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}}

View File

@ -1,19 +1,37 @@
{{define "page-content"}}
<form>
<h2>{{.Article.Title}}</h2>
<p>{{.Article.Description}}</p>
{{.Article.Content}}
<h2>Artikel veröffentlichen</h2>
<p>
<div>
<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>
<span>Tags</span>
<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
{{range .Tags}}
{{.Name}}
<br>
{{end}}
</p>
</div>
<input name="id" type="hidden" value="{{.Article.ID}}" />
<input type="submit" value="Veröffentlichen" hx-post="/publish-article/" hx-target="#page-content" />
<input type="submit" value="Ablehnen" hx-post="/reject-article/" hx-target="#page-content" />
</form>
<button hx-get="/hub/" hx-target="#page-content">Zurück</button>
<div class="btn-area">
<input class="action-btn" type="submit" value="Veröffentlichen" hx-get="/publish-article/{{.ID}}"
hx-target="#page-content" />
<input class="btn" type="submit" value="Ablehnen" hx-get="/reject-article/{{.ID}}" hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
</div>
</div>
{{end}}

View File

@ -1,16 +1,13 @@
{{define "page-content"}}
<form>
<div>
<h2>Unveröffentlichte Artikel</h2>
<div class="flex flex-col gap-4">
{{range .}}
<div>
<input required id="{{.ID}}" name="id" type="radio" value="{{.ID}}" />
<label for="{{.ID}}">{{.Title}}</label>
</div>
<button class="btn" hx-get="/review-unpublished-article/{{.ID}}" hx-target="#page-content">
<h1 class="font-bold text-2xl">{{.Title}}</h1>
<p>{{.Description}}</p>
</button>
{{end}}
</div>
<input type="submit" value="Auswählen" hx-post="/review-unpublished-article/" hx-target="#page-content" />
</form>
<button hx-get="/hub/" hx-target="#page-content">Zurück</button>
<button class="action-btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
</div>
{{end}}