Instead of having entire articles in the RSS feed, items now contain just a link

This commit is contained in:
Jason Streifling 2024-08-30 21:20:29 +02:00
parent 4a11e1a497
commit 3f1b18c29f
18 changed files with 349 additions and 108 deletions

View File

@ -4,6 +4,7 @@ tmp_dir = "tmp"
[build] [build]
args_bin = [ args_bin = [
"-articles tmp/articles",
"-desc 'Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität'", "-desc 'Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität'",
"-domain localhost", "-domain localhost",
"-key tmp/key.gob", "-key tmp/key.gob",

View File

@ -13,6 +13,7 @@ type Article struct {
Created time.Time Created time.Time
Description string Description string
Content string Content string
Link string
Published bool Published bool
Rejected bool Rejected bool
ID int64 ID int64
@ -26,8 +27,8 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
selectQuery := "SELECT id FROM issues WHERE published = false" selectQuery := "SELECT id FROM issues WHERE published = false"
insertQuery := ` insertQuery := `
INSERT INTO articles INSERT INTO articles
(title, description, content, published, rejected, author_id, issue_id) (title, description, content, link, published, rejected, author_id, issue_id)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
` `
for i := 0; i < TxMaxRetries; i++ { for i := 0; i < TxMaxRetries; i++ {
@ -45,7 +46,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
} }
result, err := tx.Exec(insertQuery, a.Title, a.Description, result, err := tx.Exec(insertQuery, a.Title, a.Description,
a.Content, a.Published, a.Rejected, a.AuthorID, id) a.Content, a.Link, a.Published, a.Rejected, a.AuthorID, id)
if err != nil { if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
@ -79,7 +80,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
func (db *DB) GetArticle(id int64) (*Article, error) { func (db *DB) GetArticle(id int64) (*Article, error) {
query := ` query := `
SELECT title, created, description, content, published, author_id SELECT title, created, description, content, link, published, author_id
FROM articles FROM articles
WHERE id = ? WHERE id = ?
` `
@ -90,7 +91,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
var err error var err error
if err := row.Scan(&article.Title, &created, &article.Description, if err := row.Scan(&article.Title, &created, &article.Description,
&article.Content, &article.Published, &article.AuthorID); err != nil { &article.Content, &article.Link, &article.Published, &article.AuthorID); err != nil {
return nil, fmt.Errorf("error scanning article row: %v", err) return nil, fmt.Errorf("error scanning article row: %v", err)
} }
@ -105,7 +106,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) { func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) {
query := ` query := `
SELECT id, title, created, description, content, author_id, issue_id SELECT id, title, created, description, content, link, author_id, issue_id
FROM articles FROM articles
WHERE published = ? WHERE published = ?
AND rejected = ? AND rejected = ?
@ -121,8 +122,8 @@ func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) {
var created []byte var created []byte
if err = rows.Scan(&article.ID, &article.Title, &created, if err = rows.Scan(&article.ID, &article.Title, &created,
&article.Description, &article.Content, &article.AuthorID, &article.Description, &article.Content, &article.Link,
&article.IssueID); err != nil { &article.AuthorID, &article.IssueID); err != nil {
return nil, fmt.Errorf("error scanning article row: %v", err) return nil, fmt.Errorf("error scanning article row: %v", err)
} }
@ -143,7 +144,7 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) {
txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable} txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable}
issueQuery := "SELECT id FROM issues WHERE published = false" issueQuery := "SELECT id FROM issues WHERE published = false"
articlesQuery := ` articlesQuery := `
SELECT id, title, created, description, content, author_id SELECT id, title, created, description, content, link, author_id
FROM articles FROM articles
WHERE issue_id = ? AND published = true WHERE issue_id = ? AND published = true
` `
@ -177,7 +178,7 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) {
var created []byte var created []byte
if err = rows.Scan(&article.ID, &article.Title, &created, if err = rows.Scan(&article.ID, &article.Title, &created,
&article.Description, &article.Content, &article.AuthorID); err != nil { &article.Description, &article.Content, &article.Link, &article.AuthorID); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
} }

View File

@ -11,6 +11,7 @@ import (
) )
type Config struct { type Config struct {
ArticleDir string
DBName string DBName string
Description string Description string
Domain string Domain string
@ -28,6 +29,7 @@ type Config struct {
func newConfig() *Config { func newConfig() *Config {
return &Config{ return &Config{
ArticleDir: "/var/www/cpolis/articles",
DBName: "cpolis", DBName: "cpolis",
FirebaseKey: "/var/www/cpolis/serviceAccountKey.json", FirebaseKey: "/var/www/cpolis/serviceAccountKey.json",
KeyFile: "/var/www/cpolis/cpolis.key", KeyFile: "/var/www/cpolis/cpolis.key",
@ -77,6 +79,7 @@ func (c *Config) handleCliArgs() error {
var err error var err error
port := 8080 port := 8080
flag.StringVar(&c.ArticleDir, "articles", c.ArticleDir, "articles directory")
flag.StringVar(&c.DBName, "db", c.DBName, "DB name") flag.StringVar(&c.DBName, "db", c.DBName, "DB name")
flag.StringVar(&c.Description, "desc", c.Description, "channel description") flag.StringVar(&c.Description, "desc", c.Description, "channel description")
flag.StringVar(&c.Domain, "domain", c.Domain, "domain name") flag.StringVar(&c.Domain, "domain", c.Domain, "domain name")
@ -92,6 +95,14 @@ func (c *Config) handleCliArgs() error {
flag.IntVar(&port, "port", port, "port") flag.IntVar(&port, "port", port, "port")
flag.Parse() flag.Parse()
c.ArticleDir, err = filepath.Abs(c.ArticleDir)
if err != nil {
return fmt.Errorf("error finding absolute path for articles directory: %v", err)
}
if err = os.MkdirAll(c.ArticleDir, 0755); err != nil {
return fmt.Errorf("error creating articles directory: %v", err)
}
c.FirebaseKey, err = filepath.Abs(c.FirebaseKey) c.FirebaseKey, err = filepath.Abs(c.FirebaseKey)
if err != nil { if err != nil {
return fmt.Errorf("error finding absolute path for Firebase service account key file: %v", err) return fmt.Errorf("error finding absolute path for Firebase service account key file: %v", err)
@ -109,12 +120,18 @@ func (c *Config) handleCliArgs() error {
c.PDFDir, err = filepath.Abs(c.PDFDir) c.PDFDir, err = filepath.Abs(c.PDFDir)
if err != nil { if err != nil {
return fmt.Errorf("error finding absolute path for pdfs dir: %v", err) return fmt.Errorf("error finding absolute path for pdfs directory: %v", err)
}
if err = os.MkdirAll(c.PDFDir, 0755); err != nil {
return fmt.Errorf("error creating pdfs directory: %v", err)
} }
c.PicsDir, err = filepath.Abs(c.PicsDir) c.PicsDir, err = filepath.Abs(c.PicsDir)
if err != nil { if err != nil {
return fmt.Errorf("error finding absolute path for pics dir: %v", err) return fmt.Errorf("error finding absolute path for pics directory: %v", err)
}
if err = os.MkdirAll(c.PicsDir, 0755); err != nil {
return fmt.Errorf("error creating pics directory: %v", err)
} }
c.Port = fmt.Sprint(":", port) c.Port = fmt.Sprint(":", port)
@ -126,7 +143,10 @@ func (c *Config) handleCliArgs() error {
c.WebDir, err = filepath.Abs(c.WebDir) c.WebDir, err = filepath.Abs(c.WebDir)
if err != nil { if err != nil {
return fmt.Errorf("error finding absolute path for web dir: %v", err) return fmt.Errorf("error finding absolute path for web directory: %v", err)
}
if err = os.MkdirAll(c.WebDir, 0755); err != nil {
return fmt.Errorf("error creating web directory: %v", err)
} }
return nil return nil

View File

@ -39,7 +39,7 @@ func GetChannel(db *DB, title, link, description string) (*rss.Channel, error) {
channel.Items = append(channel.Items, &rss.Item{ channel.Items = append(channel.Items, &rss.Item{
Title: article.Title, Title: article.Title,
Author: user.FirstName + user.LastName, Author: fmt.Sprint(user.FirstName, " ", user.LastName),
PubDate: article.Created.Format(time.RFC1123Z), PubDate: article.Created.Format(time.RFC1123Z),
Description: article.Description, Description: article.Description,
Content: &rss.Content{Value: article.Content}, Content: &rss.Content{Value: article.Content},
@ -89,14 +89,22 @@ func GenerateRSS(c *Config, db *DB) (*string, error) {
return nil, fmt.Errorf("error converting description to plain text for RSS feed: %v", err) return nil, fmt.Errorf("error converting description to plain text for RSS feed: %v", err)
} }
channel.Items = append(channel.Items, &rss.Item{ item := &rss.Item{
Author: fmt.Sprint(user.FirstName, user.LastName), Author: fmt.Sprint(user.FirstName, " ", user.LastName),
Categories: tagNames, Categories: tagNames,
Description: articleDescription, Description: articleDescription,
Link: fmt.Sprintf("http://%s/article/serve/%d", c.Domain, article.ID),
PubDate: article.Created.Format(time.RFC1123Z), PubDate: article.Created.Format(time.RFC1123Z),
Title: articleTitle, Title: articleTitle,
}) }
fmt.Println(article.Link, ": ", len(article.Link))
if article.Link == "" {
item.Link = fmt.Sprint("http://", c.Domain, "/article/serve/", article.ID, ".html")
} else {
item.Link = article.Link
}
channel.Items = append(channel.Items, item)
} }
feed := rss.NewFeed() feed := rss.NewFeed()

View File

@ -11,23 +11,25 @@ import (
func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc { func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if tokenIsVerified(w, r, c) { if !tokenIsVerified(w, r, c) {
idString := r.PathValue("id") return
id, err := strconv.ParseInt(idString, 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article, err := db.GetArticle(id)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprint(w, article.Content)
} }
idString := r.PathValue("id")
id, err := strconv.ParseInt(idString, 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article, err := db.GetArticle(id)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprint(w, article.Content)
} }
} }

View File

@ -10,15 +10,17 @@ import (
func ServeImage(c *b.Config, s *b.CookieStore) http.HandlerFunc { func ServeImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if tokenIsVerified(w, r, c) { if !tokenIsVerified(w, r, c) {
absFilepath, err := filepath.Abs(c.PicsDir) return
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.ServeFile(w, r, absFilepath+"/"+r.PathValue("pic"))
} }
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

@ -12,33 +12,37 @@ import (
func ServePDFList(c *b.Config) http.HandlerFunc { func ServePDFList(c *b.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if tokenIsVerified(w, r, c) { if !tokenIsVerified(w, r, c) {
files, err := os.ReadDir(c.PDFDir) return
if err != nil { }
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fileNames := make([]string, 0) files, err := os.ReadDir(c.PDFDir)
for _, file := range files { if err != nil {
fileNames = append(fileNames, file.Name()) log.Println(err)
} http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json") fileNames := make([]string, 0)
if err = json.NewEncoder(w).Encode(fileNames); err != nil { for _, file := range files {
log.Println(err) fileNames = append(fileNames, file.Name())
http.Error(w, err.Error(), http.StatusInternalServerError) }
return
} w.Header().Set("Content-Type", "application/json")
if err = json.NewEncoder(w).Encode(fileNames); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
} }
} }
func ServePDF(c *b.Config) http.HandlerFunc { func ServePDF(c *b.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if tokenIsVerified(w, r, c) { if !tokenIsVerified(w, r, c) {
http.ServeFile(w, r, fmt.Sprint(c.PDFDir, "/", r.PathValue("id"))) return
} }
http.ServeFile(w, r, fmt.Sprint(c.PDFDir, "/", r.PathValue("id")))
} }
} }

View File

@ -8,8 +8,10 @@ import (
func ServeRSS(c *b.Config) http.HandlerFunc { func ServeRSS(c *b.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if tokenIsVerified(w, r, c) { if !tokenIsVerified(w, r, c) {
http.ServeFile(w, r, c.RSSFile) return
} }
http.ServeFile(w, r, c.RSSFile)
} }
} }

View File

@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"io" "io"
"io/fs"
"log" "log"
"net/http" "net/http"
"os" "os"
@ -347,6 +346,27 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return return
} }
article, err := db.GetArticle(id)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
content, err := b.ConvertToHTML(article.Content)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = os.WriteFile(fmt.Sprint(c.ArticleDir, "/", article.ID, ".html"), []byte(content), 0444)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.AddArticleToCurrentIssue(id); err != nil { if err = db.AddArticleToCurrentIssue(id); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -427,7 +447,7 @@ func ShowCurrentArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFu
} }
} }
func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc { func UploadArticleImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil { if _, err := getSession(w, r, c, s); err != nil {
return return
@ -436,7 +456,7 @@ func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
file, header, err := r.FormFile("article-image") file, header, err := r.FormFile("article-image")
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
defer file.Close() defer file.Close()
@ -451,12 +471,6 @@ func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return 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) img, err := os.Create(absFilepath)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@ -471,7 +485,7 @@ func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return return
} }
url := fmt.Sprint(c.Domain, "/pics/", filename) url := fmt.Sprint(c.Domain, "/image/serve/", filename)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(url) json.NewEncoder(w).Encode(url)
} }

View File

@ -1,10 +1,17 @@
package frontend package frontend
import ( import (
"fmt"
"html/template" "html/template"
"io"
"log" "log"
"net/http" "net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/google/uuid"
b "streifling.com/jason/cpolis/cmd/backend" b "streifling.com/jason/cpolis/cmd/backend"
) )
@ -15,14 +22,141 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
return return
} }
session.Values["article"] = nil
if err = session.Save(r, w); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["issue-image"] == nil {
err := "error: Image required"
log.Println(err)
http.Error(w, err, http.StatusBadRequest)
return
}
article := &b.Article{
Title: "Autogenerated Issue Article",
Content: r.PostFormValue("issue-content"),
Link: session.Values["issue-image"].(string),
Published: true,
Rejected: false,
Created: time.Now(),
AuthorID: session.Values["id"].(int64),
}
fmt.Println(article.Link)
content, err := b.ConvertToHTML(article.Content)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = os.WriteFile(fmt.Sprint(c.ArticleDir, "/", article.ID, ".html"), []byte(content), 0444)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article.ID, err = db.AddArticle(article)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.AddArticleToCurrentIssue(article.ID); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := db.PublishLatestIssue(); err != nil { if err := db.PublishLatestIssue(); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
feed, err := b.GenerateRSS(c, db)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = b.SaveRSS(c.RSSFile, feed); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session.Values["issue-image"] = 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") tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err) tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"]) tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
} }
} }
func UploadIssueImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
if err != nil {
return
}
if err := r.ParseMultipartForm(10 << 20); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
file, header, err := r.FormFile("issue-image")
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer file.Close()
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)
return
}
defer img.Close()
if _, err = io.Copy(img, file); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session.Values["issue-image"] = filename
if err = session.Save(r, w); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
}

View File

@ -60,7 +60,7 @@ func Login(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
id, ok := db.GetID(userName) id, ok := db.GetID(userName)
if !ok { if !ok {
http.Error(w, fmt.Sprintf("no such user: %v", userName), http.StatusInternalServerError) http.Error(w, fmt.Sprintf("no such user: %v", userName), http.StatusBadRequest)
return return
} }
@ -96,7 +96,6 @@ func Logout(c *b.Config, s *b.CookieStore) http.HandlerFunc {
} }
session.Options.MaxAge = -1 session.Options.MaxAge = -1
if err = session.Save(r, w); err != nil { if err = session.Save(r, w); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -198,10 +198,6 @@ func UpdateSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil {
return
}
var err error var err error
htmlData := UserData{ htmlData := UserData{
User: &b.User{ User: &b.User{

View File

@ -62,7 +62,6 @@ func main() {
mux.HandleFunc("GET /article/write", f.WriteArticle(config, db, store)) mux.HandleFunc("GET /article/write", f.WriteArticle(config, db, store))
mux.HandleFunc("GET /hub", f.ShowHub(config, db, store)) mux.HandleFunc("GET /hub", f.ShowHub(config, db, store))
mux.HandleFunc("GET /image/serve/{pic}", c.ServeImage(config, store)) mux.HandleFunc("GET /image/serve/{pic}", c.ServeImage(config, store))
mux.HandleFunc("GET /issue/publish", f.PublishLatestIssue(config, db, store))
mux.HandleFunc("GET /issue/this", f.ShowCurrentArticles(config, db, store)) mux.HandleFunc("GET /issue/this", f.ShowCurrentArticles(config, db, store))
mux.HandleFunc("GET /logout", f.Logout(config, store)) mux.HandleFunc("GET /logout", f.Logout(config, store))
mux.HandleFunc("GET /pdf/get-list", c.ServePDFList(config)) mux.HandleFunc("GET /pdf/get-list", c.ServePDFList(config))
@ -78,7 +77,9 @@ func main() {
mux.HandleFunc("POST /article/resubmit/{id}", f.ResubmitArticle(config, db, store)) mux.HandleFunc("POST /article/resubmit/{id}", f.ResubmitArticle(config, db, store))
mux.HandleFunc("POST /article/submit", f.SubmitArticle(config, db, store)) mux.HandleFunc("POST /article/submit", f.SubmitArticle(config, db, store))
mux.HandleFunc("POST /image/upload", f.UploadImage(config, store)) mux.HandleFunc("POST /article/upload-image", f.UploadArticleImage(config, store))
mux.HandleFunc("POST /issue/publish", f.PublishLatestIssue(config, db, store))
mux.HandleFunc("POST /issue/upload-image", f.UploadIssueImage(config, store))
mux.HandleFunc("POST /login", f.Login(config, db, store)) mux.HandleFunc("POST /login", f.Login(config, db, store))
mux.HandleFunc("POST /tag/add", f.AddTag(config, db, store)) mux.HandleFunc("POST /tag/add", f.AddTag(config, db, store))
mux.HandleFunc("POST /user/add", f.AddUser(config, db, store)) mux.HandleFunc("POST /user/add", f.AddUser(config, db, store))

View File

@ -25,7 +25,7 @@ CREATE TABLE articles (
title VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
description TEXT NOT NULL, description TEXT NOT NULL,
content TEXT NOT NULL, link VARCHAR(255),
published BOOL NOT NULL, published BOOL NOT NULL,
rejected BOOL NOT NULL, rejected BOOL NOT NULL,
author_id INT NOT NULL, author_id INT NOT NULL,

View File

@ -1,17 +1,68 @@
{{define "page-content"}} {{define "page-content"}}
<h2>Aktuelle Artikel</h2> <h2>Diese Ausgabe</h2>
<div class="flex flex-col gap-4"> <form hx-encoding="multipart/form-data">
{{range .}} <div class="flex flex-col gap-4">
<div class="border px-2 py-1 rounded-md"> <div>
<h1 class="font-bold text-2xl">{{.Title}}</h1> <h3>Aktuelle Artikel</h3>
<p>{{.Description}}</p> <div class="flex flex-col gap-4">
{{range .}}
<div class="border px-2 py-1 rounded-md">
<h1 class="font-bold text-2xl">{{.Title}}</h1>
<p>{{.Description}}</p>
</div>
{{end}}
</div>
</div>
<div>
<h3>Cover</h3>
<input id="image-upload" name="issue-image" type="file" required hx-post="/issue/upload-image" />
</div>
<div>
<h3>Über diese Ausgabe</h3>
<div>
<textarea id="easyMDE" placeholder="Beschreibung dieser Ausgabe"></textarea>
<input id="issue-content" name="issue-content" type="hidden" />
</div>
</div>
</div> </div>
{{end}}
</div>
<div class="btn-area"> <div class="btn-area">
<button class="action-btn" hx-get="/issue/publish" hx-target="#page-content">Ausgabe publizieren</button> <button class="action-btn" hx-post="/issue/publish" hx-target="#page-content">Ausgabe publizieren</button>
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button> <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
</div> </div>
</form>
<script>
var easyMDE = new EasyMDE({
element: document.getElementById('easyMDE'),
hideIcons: ['image'],
imageTexts: {sbInit: ''},
showIcons: ["code", "table", "upload-image"],
uploadImage: true,
imageUploadFunction: function (file, onSuccess, onError) {
var formData = new FormData();
formData.append('article-image', file);
fetch('/article/upload-image', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
onSuccess(data);
})
.catch(error => {
onError(error);
});
},
});
easyMDE.codemirror.on("change", () => {
document.getElementById('issue-content').value = easyMDE.value();
});
</script>
{{end}} {{end}}

View File

@ -6,15 +6,17 @@
<label for="article-title">Titel</label> <label for="article-title">Titel</label>
<input name="article-title" type="text" value="{{.Title}}" /> <input name="article-title" type="text" value="{{.Title}}" />
</div> </div>
<div class="flex flex-col">
<div class="flex flex-col gap-y-1">
<label for="article-description">Beschreibung</label> <label for="article-description">Beschreibung</label>
<textarea name="article-description">{{.Description}}</textarea> <textarea name="article-description">{{.Description}}</textarea>
</div> </div>
<div class="flex flex-col">
<label for="article-content">Artikel</label> <div class="flex flex-col gap-y-1">
<label for="easyMDE">Artikel</label>
<textarea id="easyMDE">{{.Content}}</textarea> <textarea id="easyMDE">{{.Content}}</textarea>
<input id="article-content" name="article-content" type="hidden" />
</div> </div>
<input id="article-content" name="article-content" type="hidden" />
<div> <div>
<span>Tags</span> <span>Tags</span>
@ -46,7 +48,7 @@
var formData = new FormData(); var formData = new FormData();
formData.append('article-image', file); formData.append('article-image', file);
fetch('/image/upload', { fetch('/article/upload-image', {
method: 'POST', method: 'POST',
body: formData body: formData
}) })

View File

@ -38,7 +38,7 @@
{{if eq . 0}} {{if eq . 0}}
<div class="mb-3"> <div class="mb-3">
<h2>Administrator</h2> <h2>Administrator</h2>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-x-4 gap-y-2">
<button class="btn" hx-get="/user/create" hx-target="#page-content">Benutzer hinzufügen</button> <button class="btn" hx-get="/user/create" hx-target="#page-content">Benutzer hinzufügen</button>
<button class="btn" hx-get="/user/show-all/edit" hx-target="#page-content">Benutzer bearbeiten</button> <button class="btn" hx-get="/user/show-all/edit" hx-target="#page-content">Benutzer bearbeiten</button>
<button class="btn" hx-get="/user/show-all/delete" hx-target="#page-content">Benutzer löschen</button> <button class="btn" hx-get="/user/show-all/delete" hx-target="#page-content">Benutzer löschen</button>

View File

@ -6,15 +6,17 @@
<label for="article-title">Titel</label> <label for="article-title">Titel</label>
<input name="article-title" type="text" value="{{.Article.Title}}" /> <input name="article-title" type="text" value="{{.Article.Title}}" />
</div> </div>
<div class="flex flex-col">
<div class="flex flex-col gap-y-1">
<label for="article-description">Beschreibung</label> <label for="article-description">Beschreibung</label>
<textarea name="article-description">{{.Article.Description}}</textarea> <textarea name="article-description">{{.Article.Description}}</textarea>
</div> </div>
<div class="flex flex-col">
<label for="article-content">Artikel</label> <div class="flex flex-col gap-y-1">
<textarea name="article-content" placeholder="Artikel">{{.Article.Content}}</textarea> <label for="easyMDE">Artikel</label>
<textarea id="easyMDE">{{.Article.Content}}</textarea>
<input id="article-content" name="article-content" type="hidden" />
</div> </div>
<input id="article-content" name="article-content" type="hidden" />
<div> <div>
<span>Tags</span> <span>Tags</span>
@ -37,6 +39,8 @@
</form> </form>
<script> <script>
document.getElementById('article-content').value = easyMDE.value();
var easyMDE = new EasyMDE({ var easyMDE = new EasyMDE({
element: document.getElementById('easyMDE'), element: document.getElementById('easyMDE'),
hideIcons: ['image'], hideIcons: ['image'],
@ -48,7 +52,7 @@
var formData = new FormData(); var formData = new FormData();
formData.append('article-image', file); formData.append('article-image', file);
fetch('/image/upload', { fetch('/article/upload-image', {
method: 'POST', method: 'POST',
body: formData body: formData
}) })