Compare commits

..

20 Commits

Author SHA1 Message Date
42d6e0c198 Merge branch 'devel' 2024-08-31 12:17:41 +02:00
e1af2979af Merge branch 'devel' 2024-08-31 11:27:15 +02:00
f6dedc6f10 Merge branch 'devel' 2024-08-31 01:43:53 +02:00
cdf0a49550 Merge branch 'devel' 2024-08-31 01:38:28 +02:00
c3c0650210 Merge branch 'devel' 2024-08-31 01:00:55 +02:00
d077f700d8 Merge branch 'devel' 2024-08-31 00:36:48 +02:00
ec752b1c66 Merge branch 'devel' 2024-08-30 23:43:12 +02:00
46aef4f12f Merge branch 'devel' 2024-08-25 10:51:35 +02:00
1b29e328cf Merge branch 'devel' 2024-08-25 06:38:55 +02:00
e50cb819f3 Merge branch 'devel' 2024-08-23 21:45:30 +02:00
c32e38ca10 Merge branch 'devel' 2024-08-23 20:57:11 +02:00
d7c8c7a43a Merge branch 'devel' 2024-08-18 17:31:00 +02:00
1cd3edc90c Merge branch 'devel' 2024-08-18 12:06:29 +02:00
0e768c9f61 Merge branch 'devel' 2024-08-08 21:27:07 +02:00
1fcd775cc5 Merge branch 'devel' 2024-08-08 21:14:24 +02:00
203a1ed147 Implemented EasyMDE 2024-08-08 21:13:25 +02:00
ef1914ee5c Implemented article preview 2024-08-08 21:13:25 +02:00
084b101e31 Register f.ArticlePreviewHtmlData in init() 2024-08-08 21:13:25 +02:00
b2db128aa9 Shorten lines by referencing frontend as f and backend as b 2024-08-08 21:13:25 +02:00
081e880fb6 Change structure of code tor frontend and backend one 2024-08-08 21:13:25 +02:00
28 changed files with 240 additions and 476 deletions

View File

@ -5,16 +5,13 @@ tmp_dir = "tmp"
[build] [build]
args_bin = [ args_bin = [
"-articles tmp/articles", "-articles tmp/articles",
"-config tmp/config.toml",
"-desc 'Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität'", "-desc 'Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität'",
"-domain localhost", "-domain localhost",
"-firebase tmp/firebase.json",
"-key tmp/key.gob", "-key tmp/key.gob",
"-link https://distrikt-ni-st.de", "-link https://distrikt-ni-st.de",
"-log tmp/cpolis.log", "-log tmp/cpolis.log",
"-pdfs tmp/pdfs", "-pdfs tmp/pdfs",
"-pics tmp/pics", "-pics tmp/pics",
"-port 8080",
"-rss tmp/orientexpress_alle.rss", "-rss tmp/orientexpress_alle.rss",
"-title 'Freimaurer Distrikt Niedersachsen und Sachsen-Anhalt'", "-title 'Freimaurer Distrikt Niedersachsen und Sachsen-Anhalt'",
"-web web", "-web web",

View File

@ -9,20 +9,18 @@ import (
) )
type Article struct { type Article struct {
Created time.Time Title string
Title string Created time.Time
Description string Description string
Link string Link string
EncURL string EncURL string
EncType string EncLength int
ID int64 EncType string
AuthorID int64 Published bool
IssueID int64 Rejected bool
EncLength int ID int64
Published bool AuthorID int64
Rejected bool IssueID int64
IsInIssue bool
AutoGenerated bool
} }
func (db *DB) AddArticle(a *Article) (int64, error) { func (db *DB) AddArticle(a *Article) (int64, error) {
@ -31,8 +29,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, link, enc_url, enc_length, enc_type, published, rejected, author_id, issue_id, is_in_issue, auto_generated) (title, description, link, enc_url, enc_length, enc_type, published, rejected, author_id, issue_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
` `
for i := 0; i < TxMaxRetries; i++ { for i := 0; i < TxMaxRetries; i++ {
@ -50,8 +48,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
} }
result, err := tx.Exec(insertQuery, a.Title, a.Description, a.Link, result, err := tx.Exec(insertQuery, a.Title, a.Description, a.Link,
a.EncURL, a.EncLength, a.EncType, a.Published, a.Rejected, a.AuthorID, id, a.EncURL, a.EncLength, a.EncType, a.Published, a.Rejected, a.AuthorID, id)
a.IsInIssue, a.AutoGenerated)
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)
@ -85,7 +82,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, link, enc_url, enc_length, enc_type, published, author_id, issue_id, is_in_issue, auto_generated SELECT title, created, description, link, enc_url, enc_length, enc_type, published, author_id
FROM articles FROM articles
WHERE id = ? WHERE id = ?
` `
@ -97,8 +94,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
if err := row.Scan(&article.Title, &created, &article.Description, if err := row.Scan(&article.Title, &created, &article.Description,
&article.Link, &article.EncURL, &article.EncLength, &article.EncType, &article.Link, &article.EncURL, &article.EncLength, &article.EncType,
&article.Published, &article.AuthorID, &article.IssueID, &article.Published, &article.AuthorID); err != nil {
&article.IsInIssue, &article.AutoGenerated); err != nil {
return nil, fmt.Errorf("error scanning article row: %v", err) return nil, fmt.Errorf("error scanning article row: %v", err)
} }
@ -113,7 +109,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, link, enc_url, enc_length, enc_type, author_id, issue_id, is_in_issue, auto_generated SELECT id, title, created, description, link, enc_url, enc_length, enc_type, author_id, issue_id
FROM articles FROM articles
WHERE published = ? WHERE published = ?
AND rejected = ? AND rejected = ?
@ -130,8 +126,7 @@ func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) {
if err = rows.Scan(&article.ID, &article.Title, &created, if err = rows.Scan(&article.ID, &article.Title, &created,
&article.Description, &article.Link, &article.EncURL, &article.EncLength, &article.Description, &article.Link, &article.EncURL, &article.EncLength,
&article.EncType, &article.AuthorID, &article.IssueID, &article.EncType, &article.AuthorID, &article.IssueID); err != nil {
&article.IsInIssue, &article.AutoGenerated); err != nil {
return nil, fmt.Errorf("error scanning article row: %v", err) return nil, fmt.Errorf("error scanning article row: %v", err)
} }
@ -152,9 +147,9 @@ 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, link, enc_url, enc_length, enc_type, author_id, auto_generated SELECT id, title, created, description, link, enc_url, enc_length, enc_type, author_id
FROM articles FROM articles
WHERE issue_id = ? AND published = true AND is_in_issue = true WHERE issue_id = ? AND published = true
` `
for i := 0; i < TxMaxRetries; i++ { for i := 0; i < TxMaxRetries; i++ {
@ -187,7 +182,7 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) {
if err = rows.Scan(&article.ID, &article.Title, &created, if err = rows.Scan(&article.ID, &article.Title, &created,
&article.Description, &article.Link, &article.EncURL, &article.EncLength, &article.Description, &article.Link, &article.EncURL, &article.EncLength,
&article.EncType, &article.AuthorID, &article.AutoGenerated); err != nil { &article.EncType, &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

@ -3,7 +3,6 @@ package backend
import ( import (
"flag" "flag"
"fmt" "fmt"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -13,7 +12,6 @@ import (
type Config struct { type Config struct {
ArticleDir string ArticleDir string
ConfigFile string
DBName string DBName string
Description string Description string
Domain string Domain string
@ -32,72 +30,56 @@ type Config struct {
func newConfig() *Config { func newConfig() *Config {
return &Config{ return &Config{
ArticleDir: "/var/www/cpolis/articles", ArticleDir: "/var/www/cpolis/articles",
ConfigFile: "/etc/cpolis/config.toml",
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",
LogFile: "/var/log/cpolis.log", LogFile: "/var/log/cpolis.log",
PDFDir: "/var/www/cpolis/pdfs", PDFDir: "/var/www/cpolis/pdfs",
PicsDir: "/var/www/cpolis/pics", PicsDir: "/var/www/cpolis/pics",
Port: ":8080",
RSSFile: "/var/www/cpolis/cpolis.rss", RSSFile: "/var/www/cpolis/cpolis.rss",
WebDir: "/var/www/cpolis/web", WebDir: "/var/www/cpolis/web",
} }
} }
func mkDir(path string, perm fs.FileMode) (string, error) { func (c *Config) readFile() error {
var err error cfgFile, err := filepath.Abs(os.Getenv("HOME") + "/.config/cpolis/config.toml")
stringSlice := strings.Split(path, "/")
name := stringSlice[len(stringSlice)-1]
path, err = filepath.Abs(path)
if err != nil { if err != nil {
return "", fmt.Errorf("error finding absolute path for %v directory: %v", name, err) return fmt.Errorf("error getting absolute path for config file: %v", err)
}
if err = os.MkdirAll(path, perm); err != nil {
return "", fmt.Errorf("error creating %v directory: %v", name, err)
} }
return path, nil _, err = os.Stat(cfgFile)
}
func mkFile(path string, filePerm, dirPerm fs.FileMode) (string, error) {
var err error
path, err = filepath.Abs(path)
if err != nil {
return "", fmt.Errorf("error finding absolute path for %v: %v", path, err)
}
stringSlice := strings.Split(path, "/")
_, err = os.Stat(path)
if os.IsNotExist(err) { if os.IsNotExist(err) {
dir := strings.Join(stringSlice[:len(stringSlice)-1], "/") fileStrings := strings.Split(cfgFile, "/")
if err = os.MkdirAll(dir, dirPerm); err != nil {
return "", fmt.Errorf("error creating %v: %v", dir, err) dir := strings.Join(fileStrings[0:len(fileStrings)-1], "/")
if err = os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("error creating config directory: %v", err)
} }
fileName := stringSlice[len(stringSlice)-1] fileName := fileStrings[len(fileStrings)-1]
file, err := os.Create(dir + "/" + fileName) file, err := os.Create(dir + "/" + fileName)
if err != nil { if err != nil {
return "", fmt.Errorf("error creating %v: %v", fileName, err) return fmt.Errorf("error creating config file: %v", err)
} }
defer file.Close() defer file.Close()
if err = file.Chmod(filePerm); err != nil { if err = file.Chmod(0644); err != nil {
return "", fmt.Errorf("error setting permissions for %v: %v", fileName, err) return fmt.Errorf("error setting permissions for config file: %v", err)
}
} else {
_, err = toml.DecodeFile(cfgFile, c)
if err != nil {
return fmt.Errorf("error reading config file: %v", err)
} }
} }
return path, nil return nil
} }
func (c *Config) handleCliArgs() error { func (c *Config) handleCliArgs() error {
var port int
var err error var err error
port := 8080
flag.StringVar(&c.ArticleDir, "articles", c.ArticleDir, "articles directory") flag.StringVar(&c.ArticleDir, "articles", c.ArticleDir, "articles directory")
flag.StringVar(&c.ConfigFile, "config", c.ConfigFile, "config file")
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")
@ -113,117 +95,58 @@ func (c *Config) handleCliArgs() error {
flag.IntVar(&port, "port", port, "port") flag.IntVar(&port, "port", port, "port")
flag.Parse() flag.Parse()
if port != 0 { c.ArticleDir, err = filepath.Abs(c.ArticleDir)
c.Port = fmt.Sprint(":", port)
}
c.ConfigFile, err = mkFile(c.ConfigFile, 0600, 0700)
if err != nil { if err != nil {
return fmt.Errorf("error setting up file: %v", err) 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)
} }
return nil c.FirebaseKey, err = filepath.Abs(c.FirebaseKey)
}
func (c *Config) handleFile(configFile string) error {
_, err := toml.DecodeFile(configFile, c)
if err != nil { if err != nil {
return fmt.Errorf("error reading config file: %v", err) return fmt.Errorf("error finding absolute path for Firebase service account key file: %v", err)
} }
return nil c.KeyFile, err = filepath.Abs(c.KeyFile)
}
func (c *Config) setupConfig(cliConfig *Config) error {
var err error
defaultConfig := newConfig()
if cliConfig.ArticleDir != defaultConfig.ArticleDir {
c.ArticleDir = cliConfig.ArticleDir
}
c.ArticleDir, err = mkDir(c.ArticleDir, 0700)
if err != nil { if err != nil {
return fmt.Errorf("error setting up directory: %v", err) return fmt.Errorf("error finding absolute path for key file: %v", err)
} }
if cliConfig.DBName != defaultConfig.DBName { c.LogFile, err = filepath.Abs(c.LogFile)
c.DBName = cliConfig.DBName
}
if cliConfig.Description != defaultConfig.Description {
c.Description = cliConfig.Description
}
if cliConfig.Domain != defaultConfig.Domain {
c.Domain = cliConfig.Domain
}
if cliConfig.FirebaseKey != defaultConfig.FirebaseKey {
c.FirebaseKey = cliConfig.FirebaseKey
}
c.FirebaseKey, err = mkFile(c.FirebaseKey, 0600, 0700)
if err != nil { if err != nil {
return fmt.Errorf("error setting up file: %v", err) return fmt.Errorf("error finding absolute path for log file: %v", err)
} }
if cliConfig.KeyFile != defaultConfig.KeyFile { c.PDFDir, err = filepath.Abs(c.PDFDir)
c.KeyFile = cliConfig.KeyFile
}
c.KeyFile, err = mkFile(c.KeyFile, 0600, 0700)
if err != nil { if err != nil {
return fmt.Errorf("error setting up file: %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)
} }
if cliConfig.Link != defaultConfig.Link { c.PicsDir, err = filepath.Abs(c.PicsDir)
c.Link = cliConfig.Link
}
if cliConfig.LogFile != defaultConfig.LogFile {
c.LogFile = cliConfig.LogFile
}
c.LogFile, err = mkFile(c.LogFile, 0600, 0700)
if err != nil { if err != nil {
return fmt.Errorf("error setting up file: %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)
} }
if cliConfig.PDFDir != defaultConfig.PDFDir { c.Port = fmt.Sprint(":", port)
c.PDFDir = cliConfig.PDFDir
} c.RSSFile, err = filepath.Abs(c.RSSFile)
c.PDFDir, err = mkDir(c.PDFDir, 0700)
if err != nil { if err != nil {
return fmt.Errorf("error setting up directory: %v", err) return fmt.Errorf("error finding absolute path for RSS file: %v", err)
} }
if cliConfig.PicsDir != defaultConfig.PicsDir { c.WebDir, err = filepath.Abs(c.WebDir)
c.PicsDir = cliConfig.PicsDir
}
c.PicsDir, err = mkDir(c.PicsDir, 0700)
if err != nil { if err != nil {
return fmt.Errorf("error setting up directory: %v", err) return fmt.Errorf("error finding absolute path for web directory: %v", err)
} }
if err = os.MkdirAll(c.WebDir, 0755); err != nil {
if cliConfig.Port != defaultConfig.Port { return fmt.Errorf("error creating web directory: %v", err)
c.Port = cliConfig.Port
}
if cliConfig.RSSFile != defaultConfig.RSSFile {
c.RSSFile = cliConfig.RSSFile
}
c.RSSFile, err = mkFile(c.RSSFile, 0600, 0700)
if err != nil {
return fmt.Errorf("error setting up file: %v", err)
}
if cliConfig.Title != defaultConfig.Title {
c.Title = cliConfig.Title
}
if cliConfig.WebDir != defaultConfig.WebDir {
c.WebDir = cliConfig.WebDir
}
c.WebDir, err = mkDir(c.WebDir, 0700)
if err != nil {
return fmt.Errorf("error setting up directory: %v", err)
} }
return nil return nil
@ -231,20 +154,14 @@ func (c *Config) setupConfig(cliConfig *Config) error {
func HandleConfig() (*Config, error) { func HandleConfig() (*Config, error) {
config := newConfig() config := newConfig()
cliConfig := newConfig()
if err := cliConfig.handleCliArgs(); err != nil { if err := config.readFile(); err != nil {
return nil, fmt.Errorf("error reading config file: %v", err)
}
if err := config.handleCliArgs(); err != nil {
return nil, fmt.Errorf("error handling cli arguments: %v", err) return nil, fmt.Errorf("error handling cli arguments: %v", err)
} }
err := config.handleFile(cliConfig.ConfigFile)
if err != nil {
return nil, fmt.Errorf("error reading configuration file: %v", err)
}
if err = config.setupConfig(cliConfig); err != nil {
return nil, fmt.Errorf("error setting up files: %v", err)
}
return config, nil return config, nil
} }

View File

@ -8,7 +8,9 @@ import (
"google.golang.org/api/option" "google.golang.org/api/option"
) )
type Client struct{ *auth.Client } type Client struct {
*auth.Client
}
func NewClient(c *Config) (*Client, error) { func NewClient(c *Config) (*Client, error) {
var err error var err error

View File

@ -23,6 +23,10 @@ func GenerateRSS(c *Config, db *DB) (*string, error) {
} }
for _, article := range articles { for _, article := range articles {
if !article.Published {
continue
}
tags, err := db.GetArticleTags(article.ID) tags, err := db.GetArticleTags(article.ID)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting tags for articles for RSS feed: %v", err) return nil, fmt.Errorf("error getting tags for articles for RSS feed: %v", err)
@ -31,13 +35,7 @@ func GenerateRSS(c *Config, db *DB) (*string, error) {
for _, tag := range tags { for _, tag := range tags {
tagNames = append(tagNames, tag.Name) tagNames = append(tagNames, tag.Name)
} }
tagNames = append(tagNames, fmt.Sprint("Orient Express ", article.IssueID))
if article.IsInIssue || article.AutoGenerated {
tagNames = append(tagNames, fmt.Sprint("Orient Express ", article.IssueID))
}
if article.AutoGenerated {
tagNames = append(tagNames, "autogenerated")
}
user, err := db.GetUser(article.AuthorID) user, err := db.GetUser(article.AuthorID)
if err != nil { if err != nil {
@ -63,8 +61,9 @@ func GenerateRSS(c *Config, db *DB) (*string, error) {
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.AutoGenerated { if article.Title == "Autogenerated cpolis Issue Article" {
item.Enclosure = &rss.Enclosure{ item.Enclosure = &rss.Enclosure{
Url: article.EncURL, Url: article.EncURL,
Lenght: article.EncLength, Lenght: article.EncLength,

View File

@ -10,7 +10,9 @@ import (
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
) )
type CookieStore struct{ sessions.CookieStore } type CookieStore struct {
sessions.CookieStore
}
func NewKey() ([]byte, error) { func NewKey() ([]byte, error) {
key := make([]byte, 32) key := make([]byte, 32)

View File

@ -29,7 +29,7 @@ func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return return
} }
type htmlData struct { type editorHTMLData struct {
Title string Title string
Description string Description string
Content string Content string
@ -37,12 +37,12 @@ func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
Tags []*b.Tag Tags []*b.Tag
Mode int Mode int
} }
var data htmlData
var data editorHTMLData
if session.Values["article"] == nil { if session.Values["article"] == nil {
data = htmlData{} data = editorHTMLData{}
} else { } else {
data = session.Values["article"].(htmlData) data = session.Values["article"].(editorHTMLData)
} }
data.Mode = EditMode data.Mode = EditMode
@ -73,13 +73,11 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
} }
article := &b.Article{ article := &b.Article{
Title: r.PostFormValue("article-title"), Title: r.PostFormValue("article-title"),
Description: r.PostFormValue("article-description"), Description: r.PostFormValue("article-description"),
Published: false, Published: false,
Rejected: false, Rejected: false,
AuthorID: session.Values["id"].(int64), AuthorID: session.Values["id"].(int64),
IsInIssue: r.PostFormValue("issue") == "on",
AutoGenerated: false,
} }
article.ID, err = db.AddArticle(article) article.ID, err = db.AddArticle(article)
@ -155,7 +153,6 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
&b.Attribute{Table: "articles", ID: id, AttName: "title", Value: title}, &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: "description", Value: description},
&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false}, &b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false},
&b.Attribute{Table: "articles", ID: id, AttName: "is_in_issue", Value: r.PostFormValue("issue") == "on"},
); err != nil { ); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -211,10 +208,11 @@ func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerF
return return
} }
data := new(struct { type htmlData struct {
MyIDs map[int64]bool MyIDs map[int64]bool
RejectedArticles []*b.Article RejectedArticles []*b.Article
}) }
data := new(htmlData)
data.RejectedArticles, err = db.GetCertainArticles(false, true) data.RejectedArticles, err = db.GetCertainArticles(false, true)
if err != nil { if err != nil {
@ -242,27 +240,46 @@ func ReviewUnpublishedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Hand
return return
} }
data := new(struct { type htmlData struct {
Article *b.Article Title string
Content template.HTML Description string
Tags []*b.Tag Content template.HTML
}) Tags []*b.Tag
ID int64
}
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) var err error
data := new(htmlData)
data.ID, err = strconv.ParseInt(r.PathValue("id"), 10, 64)
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.StatusInternalServerError)
return return
} }
data.Article, err = db.GetArticle(id) article, err := db.GetArticle(data.ID)
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.StatusInternalServerError)
return return
} }
articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md") data.Title, err = b.ConvertToPlain(article.Title)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Description, err = b.ConvertToPlain(article.Description)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
contentBytes, err := os.ReadFile(articleAbsName) contentBytes, err := os.ReadFile(articleAbsName)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@ -278,7 +295,7 @@ func ReviewUnpublishedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Hand
} }
data.Content = template.HTML(content) data.Content = template.HTML(content)
data.Tags, err = db.GetArticleTags(data.Article.ID) data.Tags, err = db.GetArticleTags(data.ID)
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.StatusInternalServerError)
@ -297,12 +314,13 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
return return
} }
data := new(struct { type htmlData struct {
Selected map[int64]bool Selected map[int64]bool
Article *b.Article Article *b.Article
Content string Content string
Tags []*b.Tag Tags []*b.Tag
}) }
data := new(htmlData)
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil { if err != nil {
@ -360,20 +378,13 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
} }
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
article, err := db.GetArticle(id)
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.StatusInternalServerError)
return return
} }
if err = db.AddArticleToCurrentIssue(article.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)
return return
@ -512,7 +523,7 @@ func ShowPublishedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
filteredArticles := make([]*b.Article, 0) filteredArticles := make([]*b.Article, 0)
for _, article := range publishedArticles { for _, article := range publishedArticles {
if !article.AutoGenerated { if article.Title != "Autogenerated cpolis Issue Article" {
filteredArticles = append(filteredArticles, article) filteredArticles = append(filteredArticles, article)
} }
} }
@ -529,14 +540,16 @@ func ReviewArticleForDeletion(c *b.Config, db *b.DB, s *b.CookieStore) http.Hand
return return
} }
var err error type htmlData struct {
data := new(struct {
Title string Title string
Description string Description string
Content template.HTML Content template.HTML
Tags []*b.Tag Tags []*b.Tag
ID int64 ID int64
}) }
var err error
data := new(htmlData)
data.ID, err = strconv.ParseInt(r.PathValue("id"), 10, 64) data.ID, err = strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil { if err != nil {

View File

@ -59,16 +59,16 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
mimeType := mime.TypeByExtension(filepath.Ext(imgAbsName)) mimeType := mime.TypeByExtension(filepath.Ext(imgAbsName))
article := &b.Article{ article := &b.Article{
Title: r.PostFormValue("issue-title"), Title: "Autogenerated cpolis Issue Article",
EncURL: fmt.Sprint(c.Domain, "/image/serve/", imgFileName), EncURL: fmt.Sprint(c.Domain, "/image/serve/", imgFileName),
EncLength: int(imgSize), EncLength: int(imgSize),
EncType: mimeType, EncType: mimeType,
Published: true, Published: true,
Rejected: false, Rejected: false,
Created: time.Now(), Created: time.Now(),
AuthorID: session.Values["id"].(int64), AuthorID: session.Values["id"].(int64),
AutoGenerated: true,
} }
fmt.Println(article.Link)
article.ID, err = db.AddArticle(article) article.ID, err = db.AddArticle(article)
if err != nil { if err != nil {

View File

@ -1,59 +0,0 @@
package frontend
import (
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"github.com/google/uuid"
b "streifling.com/jason/cpolis/cmd/backend"
)
func UploadPDF(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil {
return
}
if err := r.ParseMultipartForm(10 << 20); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
file, _, err := r.FormFile("pdf-upload")
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer file.Close()
filename := fmt.Sprint(uuid.New(), ".pdf")
absFilepath, err := filepath.Abs(fmt.Sprint(c.PDFDir, "/", filename))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
pdf, err := os.Create(absFilepath)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer pdf.Close()
if _, err = io.Copy(pdf, file); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
}

View File

@ -277,12 +277,12 @@ func ShowAllUsers(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.H
return return
} }
data := new(struct { type htmlData struct {
Users map[int64]*b.User Users map[int64]*b.User
Action string Action string
}) }
data.Action = action data := &htmlData{Action: action}
data.Users, err = db.GetAllUsers() data.Users, err = db.GetAllUsers()
if err != nil { if err != nil {
log.Println(err) log.Println(err)

View File

@ -81,7 +81,6 @@ func main() {
mux.HandleFunc("POST /issue/publish", f.PublishLatestIssue(config, db, store)) mux.HandleFunc("POST /issue/publish", f.PublishLatestIssue(config, db, store))
mux.HandleFunc("POST /issue/upload-image", f.UploadIssueImage(config, 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 /pdf/upload", f.UploadPDF(config, 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))
mux.HandleFunc("POST /user/add-first", f.AddFirstUser(config, db, store)) mux.HandleFunc("POST /user/add-first", f.AddFirstUser(config, db, store))

View File

@ -21,20 +21,18 @@ CREATE TABLE issues (
); );
CREATE TABLE articles ( CREATE TABLE articles (
id INT AUTO_INCREMENT, id INT AUTO_INCREMENT,
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,
link VARCHAR(255), link VARCHAR(255),
enc_url VARCHAR(255), enc_url VARCHAR(255),
enc_length INT, enc_length INT,
enc_type VARCHAR(255), enc_type 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,
issue_id INT NOT NULL, issue_id INT NOT NULL,
is_in_issue BOOL NOT NULL,
auto_generated BOOL NOT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
FOREIGN KEY (author_id) REFERENCES users (id), FOREIGN KEY (author_id) REFERENCES users (id),
FOREIGN KEY (issue_id) REFERENCES issues (id) FOREIGN KEY (issue_id) REFERENCES issues (id)

View File

@ -7,5 +7,4 @@ module.exports = {
plugins: [ plugins: [
require('@tailwindcss/typography') require('@tailwindcss/typography')
], ],
darkMode: 'selector',
} }

View File

@ -43,9 +43,7 @@ chmod +x $CPOLIS_DIR/tailwindcss
$CPOLIS_DIR/tailwindcss -i $CPOLIS_DIR/web/static/css/input.css -o $CPOLIS_DIR/web/static/css/style.css $CPOLIS_DIR/tailwindcss -i $CPOLIS_DIR/web/static/css/input.css -o $CPOLIS_DIR/web/static/css/style.css
echo '\nBuilding cpolis...' >&2 echo '\nBuilding cpolis...' >&2
cd $CPOLIS_DIR go build -o $BIN_DIR/cpolis $CPOLIS_DIR/cmd/main.go
go build -o $BIN_DIR/cpolis cmd/main.go
cd
echo '\nSetting up system files...' >&2 echo '\nSetting up system files...' >&2
sudo chown root:root $BIN_DIR/cpolis sudo chown root:root $BIN_DIR/cpolis

View File

@ -2,71 +2,40 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
body {
width: 800px;
@apply mx-auto text-slate-900;
}
h2 { h2 {
@apply font-bold mb-2 text-2xl; @apply font-bold mb-2 text-2xl;
} }
h3 { form {
@apply font-bold mb-2 text-xl; @apply flex flex-col gap-y-3;
}
input[type="file"] {
@apply border rounded-md w-full;
} }
input[type="password"], input[type="password"],
input[type="text"] { input[type="text"] {
@apply bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 h-8 rounded-md; @apply border h-8 rounded-md;
} }
textarea { textarea {
@apply bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 h-32 rounded-md; @apply border h-32 rounded-md;
}
.btn-area-1 {
@apply grid grid-cols-1 gap-x-4 gap-y-1 mt-4;
} }
.btn-area { .btn-area {
@apply grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-1 mt-4; @apply flex gap-4 mt-4;
}
.btn-area-3 {
@apply grid grid-cols-1 md:grid-cols-3 gap-x-4 gap-y-1 mt-4;
} }
.btn { .btn {
@apply bg-slate-200 dark:bg-slate-800 hover:bg-slate-100 dark:hover:bg-slate-900 border border-slate-200 dark:border-slate-800 my-2 px-3 py-2 rounded-md w-full; @apply bg-slate-200 border my-2 px-3 py-2 rounded-md w-full hover:bg-slate-100;
} }
.action-btn { .action-btn {
@apply bg-slate-800 dark:bg-slate-200 hover:bg-slate-700 dark:hover:bg-slate-300 my-2 px-3 py-2 rounded-md text-slate-50 dark:text-slate-950 w-full; @apply bg-slate-800 border my-2 px-3 py-2 rounded-md text-slate-50 w-full hover:bg-slate-700;
}
.EasyMDEContainer .CodeMirror {
@apply bg-slate-50 dark:bg-slate-950 border-slate-200 dark:border-slate-800 text-slate-900 dark:text-slate-100
}
.EasyMDEContainer .cm-s-easymde .CodeMirror-cursor {
@apply border-slate-900 dark:border-slate-100
}
.EasyMDEContainer .editor-toolbar > * {
@apply text-slate-900 dark:text-slate-100
}
.EasyMDEContainer .editor-toolbar > .active, .editor-toolbar > button:hover, .editor-preview pre, .cm-s-easymde .cm-comment {
@apply bg-slate-100 dark:bg-slate-900
}
.EasyMDEContainer .CodeMirror-fullscreen {
@apply bg-slate-50 dark:bg-slate-950
}
.editor-toolbar {
@apply border border-slate-200 dark:border-slate-800
}
.editor-toolbar.fullscreen {
@apply bg-slate-50 dark:bg-slate-950
}
.editor-preview {
@apply bg-slate-50 dark:bg-slate-950
} }

View File

@ -2,7 +2,7 @@
<h2>Neuer Tag</h2> <h2>Neuer Tag</h2>
<form> <form>
<input class="w-full" name="tag" placeholder="Tag eingeben" required type="text" /> <input required name="tag" placeholder="Tag eingeben" type="text" />
<div class="btn-area"> <div class="btn-area">
<input class="action-btn" type="submit" value="Anlegen" hx-post="/tag/add" hx-target="#page-content" /> <input class="action-btn" type="submit" value="Anlegen" hx-post="/tag/add" hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button> <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>

View File

@ -2,7 +2,7 @@
<h2>Neuer Benutzer</h2> <h2>Neuer Benutzer</h2>
<form> <form>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> <div class="grid grid-cols-3 gap-4">
<div> <div>
<label for="username">Benutzername</label> <label for="username">Benutzername</label>
<input class="w-full" required name="username" type="text" value="{{.UserName}}" /> <input class="w-full" required name="username" type="text" value="{{.UserName}}" />
@ -25,7 +25,7 @@
</div> </div>
</div> </div>
<div class="flex flex-wrap gap-4"> <div class="flex gap-4">
<div> <div>
<input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} /> <input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} />
<label for="author">Autor</label> <label for="author">Autor</label>

View File

@ -7,7 +7,7 @@
<h3>Aktuelle Artikel</h3> <h3>Aktuelle Artikel</h3>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
{{range .}} {{range .}}
<div class="border border-slate-200 dark:border-slate-800 px-2 py-1 rounded-md"> <div class="border px-2 py-1 rounded-md">
<h1 class="font-bold text-2xl">{{.Title}}</h1> <h1 class="font-bold text-2xl">{{.Title}}</h1>
<p>{{.Description}}</p> <p>{{.Description}}</p>
</div> </div>
@ -16,13 +16,8 @@
</div> </div>
<div> <div>
<h3>Titelseite</h3> <h3>Cover</h3>
<div class="grid grid-cols-2 gap-4 items-center"> <input id="image-upload" name="issue-image" type="file" required hx-post="/issue/upload-image" />
<input class="h-full" name="issue-title" placeholder="Titel" type="text" />
<label class="btn text-center" for="image-upload">Bild hochladen</label>
<input class="hidden" id="image-upload" name="issue-image" type="file" required
hx-post="/issue/upload-image" />
</div>
</div> </div>
<div> <div>

View File

@ -2,7 +2,7 @@
<h2>Profil bearbeiten</h2> <h2>Profil bearbeiten</h2>
<form> <form>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> <div class="grid grid-cols-3 gap-4">
<div> <div>
<label for="username">Benutzername</label> <label for="username">Benutzername</label>
<input class="w-full" name="username" type="text" value="{{.UserName}}" /> <input class="w-full" name="username" type="text" value="{{.UserName}}" />

View File

@ -2,7 +2,7 @@
<h2>Profil von {{.FirstName}} {{.LastName}} bearbeiten</h2> <h2>Profil von {{.FirstName}} {{.LastName}} bearbeiten</h2>
<form> <form>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> <div class="grid grid-cols-3 gap-4">
<div> <div>
<label for="username">Benutzername</label> <label for="username">Benutzername</label>
<input class="w-full" name="username" type="text" value="{{.UserName}}" /> <input class="w-full" name="username" type="text" value="{{.UserName}}" />
@ -29,7 +29,7 @@
</div> </div>
</div> </div>
<div class="flex flex-wrap gap-4"> <div class="flex gap-4">
<div> <div>
<input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} /> <input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} />
<label for="author">Autor</label> <label for="author">Autor</label>

View File

@ -21,11 +21,6 @@
<div> <div>
<span>Tags</span> <span>Tags</span>
<div class="flex flex-wrap gap-x-4"> <div class="flex flex-wrap gap-x-4">
<div>
<input id="issue" name="issue" type="checkbox" />
<label for="issue">Orient Express</label>
</div>
{{range .Tags}} {{range .Tags}}
<div> <div>
<input id="{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" /> <input id="{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" />

View File

@ -25,7 +25,7 @@
</div> </div>
</div> </div>
<div class="btn-area-1"> <div class="btn-area">
<input class="action-btn" type="submit" value="Anlegen" hx-post="/user/add-first" hx-target="#page-content" /> <input class="action-btn" type="submit" value="Anlegen" hx-post="/user/add-first" hx-target="#page-content" />
</div> </div>
</form> </form>

View File

@ -4,49 +4,44 @@
{{if lt . 4}} {{if lt . 4}}
<div class="mb-3"> <div class="mb-3">
<h2>Artikel</h2> <h2>Autor</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2"> <div class="grid grid-cols-2 gap-x-4 gap-y-2">
<button class="btn" hx-get="/article/write" hx-target="#page-content">Artikel schreiben</button> <button class="btn" hx-get="/article/write" hx-target="#page-content">Artikel schreiben</button>
<button class="btn" hx-get="/article/all-rejected" hx-target="#page-content">Abgelehnte Artikel</button> <button class="btn" hx-get="/article/all-rejected" hx-target="#page-content">Abgelehnte Artikel</button>
{{if lt . 3}}<button class="btn" hx-get="/article/all-unpublished" hx-target="#page-content"> <button class="btn" hx-get="/user/edit/self" hx-target="#page-content">Profil bearbeiten</button>
</div>
</div>
{{end}}
{{if lt . 3}}
<div class="mb-3">
<h2>Redakteur</h2>
<div class="grid grid-cols-2 gap-4">
<button class="btn" hx-get="/article/all-unpublished" hx-target="#page-content">
Unveröffentlichte Artikel Unveröffentlichte Artikel
</button>{{end}} </button>
{{if lt . 2}}<button class="btn" hx-get="/article/all-published" hx-target="#page-content"> <button class="btn" hx-get="/tag/create" hx-target="#page-content">Neuer Tag</button>
Artikel löschen
</button>{{end}}
{{if lt . 3}}<button class="btn" hx-get="/tag/create" hx-target="#page-content">Neuer Tag</button>{{end}}
</div> </div>
</div> </div>
{{end}} {{end}}
{{if lt . 2}} {{if lt . 2}}
<div class="mb-3"> <div class="mb-3">
<h2>Ausgabe</h2> <h2>Herausgeber</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2"> <div class="grid grid-cols-2 gap-4">
<button class="btn" hx-get="/issue/this" hx-target="#page-content">Diese Ausgabe</button> <button class="btn" hx-get="/issue/this" hx-target="#page-content">Diese Ausgabe</button>
<form class="flex" hx-encoding="multipart/form-data"> <button class="btn" hx-get="/article/all-published" hx-target="#page-content">Artikel löschen</button>
<label class="btn text-center" for="pdf-upload">PDF hochladen</label>
<input accept=".pdf" class="hidden" id="pdf-upload" name="pdf-upload" type="file"
hx-post="/pdf/upload" />
</form>
</div> </div>
{{end}}
</div> </div>
{{end}}
{{if lt . 4}} {{if eq . 0}}
<div class="mb-3"> <div class="mb-3">
<h2>Benutzer</h2> <h2>Administrator</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2"> <div class="grid grid-cols-2 gap-x-4 gap-y-2">
<button class="btn" hx-get="/user/edit/self" hx-target="#page-content">Mein Profil bearbeiten</button> <button class="btn" hx-get="/user/create" hx-target="#page-content">Benutzer hinzufügen</button>
{{if eq . 0}}<button class="btn" hx-get="/user/create" hx-target="#page-content"> <button class="btn" hx-get="/user/show-all/edit" hx-target="#page-content">Benutzer bearbeiten</button>
Benutzer hinzufügen <button class="btn" hx-get="/user/show-all/delete" hx-target="#page-content">Benutzer löschen</button>
</button>{{end}}
{{if eq . 0}}<button class="btn" hx-get="/user/show-all/edit" hx-target="#page-content">
Benutzer bearbeiten
</button>{{end}}
{{if eq . 0}}<button class="btn" hx-get="/user/show-all/delete" hx-target="#page-content">
Benutzer löschen
</button>{{end}}
</div> </div>
</div> </div>
{{end}} {{end}}

View File

@ -5,26 +5,13 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Orient Editor</title> <title>Orient Editor</title>
<link href="/web/static/css/style.css" rel="stylesheet"> <link href="/web/static/css/style.css" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/easymde/dist/easymde.min.css"> <link rel="stylesheet" href="https://unpkg.com/easymde/dist/easymde.min.css">
</head> </head>
<body <body class="flex flex-col justify-between min-h-screen bg-slate-50">
class="bg-slate-50 dark:bg-slate-950 container flex flex-col justify-between max-w-screen-lg min-h-screen mx-auto text-slate-900 dark:text-slate-100">
<header class="my-8"> <header class="my-8">
<h1 class="font-bold text-4xl text-center">Orient Editor</h1> <h1 class="font-bold text-4xl text-center">Orient Editor</h1>
<div class="ml-4">
<label class="cursor-pointer flex items-center relative" for="theme-toggle">
<div class="bg-slate-200 dark:bg-slate-800 block h-6 w-12 rounded-full"></div>
<div
class="absolute bg-slate-800 dark:bg-slate-50 dot left-1 top-1 h-4 w-4 rounded-full transform transition">
</div>
<span class="ml-2">Dunkel</span>
</label>
<input type="checkbox" id="theme-toggle" class="sr-only">
</div>
</header> </header>
<main class="mx-4"> <main class="mx-4">
@ -33,44 +20,17 @@
</div> </div>
</main> </main>
<footer class="text-center text-gray-500 my-8"> <footer class="my-8">
<p> <p class="text-center text-gray-500 dark:text-gray-400">
&copy; 2024 Jason Streifling. Alle Rechte vorbehalten. &copy; 2024 Jason Streifling. Alle Rechte vorbehalten.
</p> </p>
<p> <p class="text-center text-gray-500 dark:text-gray-400">
v0.10.3 - <strong>Hinweis:</strong> Diese Software befindet sich noch in der Entwicklung und kann Fehler <strong>Hinweis:</strong> Diese Software befindet sich noch in der Entwicklung und kann Fehler enthalten.
enthalten.
</p> </p>
</footer> </footer>
<script src="https://unpkg.com/htmx.org@2.0.2"></script> <script src="https://unpkg.com/htmx.org@2.0.1"></script>
<script src="https://unpkg.com/easymde/dist/easymde.min.js"></script> <script src="https://unpkg.com/easymde/dist/easymde.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const toggleSwitch = document.getElementById('theme-toggle');
const dot = document.querySelector('.dot');
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
toggleSwitch.checked = true;
dot.classList.add('translate-x-6');
} else {
document.documentElement.classList.remove('dark')
}
toggleSwitch.addEventListener('change', () => {
if (toggleSwitch.checked) {
document.documentElement.classList.add('dark');
localStorage.theme = 'dark';
dot.classList.add('translate-x-6');
} else {
document.documentElement.classList.remove('dark');
localStorage.theme = 'light';
dot.classList.remove('translate-x-6');
}
});
});
</script>
</body> </body>
</html> </html>

View File

@ -2,13 +2,11 @@
<h2>Anmeldung</h2> <h2>Anmeldung</h2>
<form> <form>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 my-1"> <div class="btn-area">
<input class="w-full" name="username" placeholder="Benutzername" type="text" /> <input class="w-full" name="username" placeholder="Benutzername" type="text" />
<input class="w-full" name="password" placeholder="Passwort" type="password" /> <input class="w-full" name="password" placeholder="Passwort" type="password" />
</div> </div>
<div class="mt-2"> <input class="action-btn" 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" />
</div>
</form> </form>
{{end}} {{end}}

View File

@ -15,17 +15,12 @@
<div class="flex flex-col gap-y-1"> <div class="flex flex-col gap-y-1">
<label for="easyMDE">Artikel</label> <label for="easyMDE">Artikel</label>
<textarea id="easyMDE">{{.Content}}</textarea> <textarea id="easyMDE">{{.Content}}</textarea>
<input id="article-content" name="article-content" type="hidden" value="{{.Content}}" /> <input id="article-content" name="article-content" type="hidden" />
</div> </div>
<div> <div>
<span>Tags</span> <span>Tags</span>
<div class="flex flex-wrap gap-x-4"> <div class="flex flex-wrap gap-x-4">
<div>
<input id="issue" name="issue" type="checkbox" {{if .Article.IsInIssue}}checked{{end}} />
<label for="issue">Orient Express</label>
</div>
{{range .Tags}} {{range .Tags}}
<div> <div>
<input id="tag-{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" {{if index $.Selected <input id="tag-{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" {{if index $.Selected
@ -44,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'],

View File

@ -3,24 +3,24 @@
<div> <div>
<span>Titel</span> <span>Titel</span>
<div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full"> <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
{{.Title}} {{.Title}}
</div> </div>
<span>Beschreibung</span> <span>Beschreibung</span>
<div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full"> <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
{{.Description}} {{.Description}}
</div> </div>
<span>Artikel</span> <span>Artikel</span>
<div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full"> <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
<div class="prose text-slate-900 dark:text-slate-100"> <div class="prose">
{{.Content}} {{.Content}}
</div> </div>
</div> </div>
<span>Tags</span> <span>Tags</span>
<div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full"> <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
{{range .Tags}} {{range .Tags}}
{{.Name}} {{.Name}}
<br> <br>

View File

@ -3,39 +3,34 @@
<div> <div>
<span>Titel</span> <span>Titel</span>
<div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full"> <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
{{.Article.Title}} {{.Title}}
</div> </div>
<span>Beschreibung</span> <span>Beschreibung</span>
<div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full"> <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
{{.Article.Description}} {{.Description}}
</div> </div>
<span>Artikel</span> <span>Artikel</span>
<div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full"> <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
<div class="prose"> <div class="prose">
{{.Content}} {{.Content}}
</div> </div>
</div> </div>
<span>Tags</span> <span>Tags</span>
<div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full"> <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
{{if .Article.IsInIssue}}
<span>Orient Express</span>
<br>
{{end}}
{{range .Tags}} {{range .Tags}}
<span>{{.Name}}</span> {{.Name}}
<br> <br>
{{end}} {{end}}
</div> </div>
<div class="btn-area-3"> <div class="btn-area">
<input class="action-btn" type="submit" value="Veröffentlichen" hx-get="/article/publish/{{.Article.ID}}" <input class="action-btn" type="submit" value="Veröffentlichen" hx-get="/article/publish/{{.ID}}"
hx-target="#page-content" />
<input class="btn" type="submit" value="Ablehnen" hx-get="/article/reject/{{.Article.ID}}"
hx-target="#page-content" /> hx-target="#page-content" />
<input class="btn" type="submit" value="Ablehnen" hx-get="/article/reject/{{.ID}}" hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button> <button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
</div> </div>
</div> </div>