Compare commits
	
		
			53 Commits
		
	
	
		
			v0.7.7
			...
			bc4d8fa37e
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| bc4d8fa37e | |||
| 1368593c75 | |||
| e4bef7006c | |||
| afa1b65563 | |||
| a9bef63174 | |||
| 4d944ef65a | |||
| d2b21e7405 | |||
| 4bd255a7c4 | |||
| 2743899b65 | |||
| 065ffcdc30 | |||
| 38ef7b80d5 | |||
| e3c192359f | |||
| c777a77824 | |||
| 46532e4c85 | |||
| c183043dac | |||
| c722135a56 | |||
| 391b3bf157 | |||
| 0aa479763d | |||
| ff0e229f03 | |||
| 8ef6ff729e | |||
| e4624b8705 | |||
| 887fa863bc | |||
| 4592bdf970 | |||
| dadd610b2d | |||
| 74d71cfb6a | |||
| 4004bcb8f0 | |||
| 9e0182ed03 | |||
| e554174c28 | |||
| 8597f1b849 | |||
| b04e0e5e81 | |||
| ca7e7cddd3 | |||
| 62921c8e2a | |||
| 94431a2aa9 | |||
| c7761e2dc8 | |||
| 5b1f20c5bc | |||
| 013cddc157 | |||
| 523ff9d2db | |||
| 84960fdd44 | |||
| d0c566f8df | |||
| 0a387c1087 | |||
| 38caf02b2c | |||
| 435157a42f | |||
| 4163afb0b7 | |||
| 5e586aa49a | |||
| 6cc0fb40d7 | |||
| 66b2743d3d | |||
| 18617f1dbc | |||
| 3723b2b5e6 | |||
| 79ee20a50e | |||
| e05521591b | |||
| 0c9a79e24a | |||
| ce788bfd50 | |||
| 8147d9bef6 | 
| @@ -5,13 +5,16 @@ tmp_dir = "tmp" | ||||
| [build] | ||||
| args_bin = [ | ||||
|     "-articles tmp/articles", | ||||
|     "-config tmp/config.toml", | ||||
|     "-desc 'Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität'", | ||||
|     "-domain localhost", | ||||
|     "-firebase tmp/firebase.json", | ||||
|     "-key tmp/key.gob", | ||||
|     "-link https://distrikt-ni-st.de", | ||||
|     "-log tmp/cpolis.log", | ||||
|     "-pdfs tmp/pdfs", | ||||
|     "-pics tmp/pics", | ||||
|     "-port 8080", | ||||
|     "-rss tmp/orientexpress_alle.rss", | ||||
|     "-title 'Freimaurer Distrikt Niedersachsen und Sachsen-Anhalt'", | ||||
|     "-web web", | ||||
|   | ||||
| @@ -9,18 +9,21 @@ import ( | ||||
| ) | ||||
|  | ||||
| type Article struct { | ||||
| 	Title       string | ||||
| 	Created     time.Time | ||||
| 	Description string | ||||
| 	Link        string | ||||
| 	EncURL      string | ||||
| 	EncLength   int | ||||
| 	EncType     string | ||||
| 	Published   bool | ||||
| 	Rejected    bool | ||||
| 	ID          int64 | ||||
| 	AuthorID    int64 | ||||
| 	IssueID     int64 | ||||
| 	Created       time.Time | ||||
| 	Title         string | ||||
| 	Description   string | ||||
| 	Link          string | ||||
| 	EncURL        string | ||||
| 	EncType       string | ||||
| 	ID            int64 | ||||
| 	AuthorID      int64 | ||||
| 	IssueID       int64 | ||||
| 	EditedID      int64 | ||||
| 	EncLength     int | ||||
| 	Published     bool | ||||
| 	Rejected      bool | ||||
| 	IsInIssue     bool | ||||
| 	AutoGenerated bool | ||||
| } | ||||
|  | ||||
| func (db *DB) AddArticle(a *Article) (int64, error) { | ||||
| @@ -29,8 +32,9 @@ func (db *DB) AddArticle(a *Article) (int64, error) { | ||||
| 	selectQuery := "SELECT id FROM issues WHERE published = false" | ||||
| 	insertQuery := ` | ||||
|     INSERT INTO articles | ||||
|         (title, description, link, enc_url, enc_length, enc_type, published, rejected, author_id, issue_id) | ||||
|     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | ||||
|         (title, description, link, enc_url, enc_length, enc_type, published, | ||||
|         rejected, author_id, issue_id, edited_id, is_in_issue, auto_generated) | ||||
|     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | ||||
|     ` | ||||
|  | ||||
| 	for i := 0; i < TxMaxRetries; i++ { | ||||
| @@ -48,7 +52,8 @@ func (db *DB) AddArticle(a *Article) (int64, error) { | ||||
| 			} | ||||
|  | ||||
| 			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.EditedID, a.IsInIssue, a.AutoGenerated) | ||||
| 			if err != nil { | ||||
| 				if rollbackErr := tx.Rollback(); rollbackErr != nil { | ||||
| 					log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) | ||||
| @@ -82,7 +87,9 @@ func (db *DB) AddArticle(a *Article) (int64, error) { | ||||
|  | ||||
| func (db *DB) GetArticle(id int64) (*Article, error) { | ||||
| 	query := ` | ||||
|     SELECT title, created, description, link, enc_url, enc_length, enc_type, published, author_id | ||||
|     SELECT | ||||
|         title, created, description, link, enc_url, enc_length, enc_type, | ||||
|         published, author_id, issue_id, edited_id, is_in_issue, auto_generated | ||||
|     FROM articles | ||||
|     WHERE id = ? | ||||
|     ` | ||||
| @@ -94,7 +101,8 @@ func (db *DB) GetArticle(id int64) (*Article, error) { | ||||
|  | ||||
| 	if err := row.Scan(&article.Title, &created, &article.Description, | ||||
| 		&article.Link, &article.EncURL, &article.EncLength, &article.EncType, | ||||
| 		&article.Published, &article.AuthorID); err != nil { | ||||
| 		&article.Published, &article.AuthorID, &article.IssueID, &article.EditedID, | ||||
| 		&article.IsInIssue, &article.AutoGenerated); err != nil { | ||||
| 		return nil, fmt.Errorf("error scanning article row: %v", err) | ||||
| 	} | ||||
|  | ||||
| @@ -107,14 +115,15 @@ func (db *DB) GetArticle(id int64) (*Article, error) { | ||||
| 	return article, nil | ||||
| } | ||||
|  | ||||
| func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) { | ||||
| 	query := ` | ||||
|     SELECT id, title, created, description, link, enc_url, enc_length, enc_type, author_id, issue_id | ||||
| func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, error) { | ||||
| 	query := fmt.Sprintf(` | ||||
|     SELECT | ||||
|         id, title, created, description, link, enc_url, enc_length, enc_type, | ||||
|         author_id, issue_id, published, rejected, is_in_issue, auto_generated | ||||
|     FROM articles | ||||
|     WHERE published = ? | ||||
|     AND rejected = ? | ||||
|     ` | ||||
| 	rows, err := db.Query(query, published, rejected) | ||||
|     WHERE %s = ? | ||||
|     `, attribute) | ||||
| 	rows, err := db.Query(query, value) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("error querying articles: %v", err) | ||||
| 	} | ||||
| @@ -126,11 +135,11 @@ func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) { | ||||
|  | ||||
| 		if err = rows.Scan(&article.ID, &article.Title, &created, | ||||
| 			&article.Description, &article.Link, &article.EncURL, &article.EncLength, | ||||
| 			&article.EncType, &article.AuthorID, &article.IssueID); err != nil { | ||||
| 			&article.EncType, &article.AuthorID, &article.IssueID, &article.Published, | ||||
| 			&article.Rejected, &article.IsInIssue, &article.AutoGenerated); err != nil { | ||||
| 			return nil, fmt.Errorf("error scanning article row: %v", err) | ||||
| 		} | ||||
|  | ||||
| 		article.Published = false | ||||
| 		article.Created, err = time.Parse("2006-01-02 15:04:05", string(created)) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("error parsing created: %v", err) | ||||
| @@ -147,9 +156,11 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) { | ||||
| 	txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable} | ||||
| 	issueQuery := "SELECT id FROM issues WHERE published = false" | ||||
| 	articlesQuery := ` | ||||
|     SELECT id, title, created, description, link, enc_url, enc_length, enc_type, author_id | ||||
|     SELECT | ||||
|         id, title, created, description, link, enc_url, enc_length, enc_type, | ||||
|         author_id, auto_generated | ||||
|     FROM articles | ||||
|     WHERE issue_id = ? AND published = true | ||||
|     WHERE issue_id = ? AND published = true AND is_in_issue = true | ||||
|     ` | ||||
|  | ||||
| 	for i := 0; i < TxMaxRetries; i++ { | ||||
| @@ -182,7 +193,7 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) { | ||||
|  | ||||
| 				if err = rows.Scan(&article.ID, &article.Title, &created, | ||||
| 					&article.Description, &article.Link, &article.EncURL, &article.EncLength, | ||||
| 					&article.EncType, &article.AuthorID); err != nil { | ||||
| 					&article.EncType, &article.AuthorID, &article.AutoGenerated); err != nil { | ||||
| 					if rollbackErr := tx.Rollback(); rollbackErr != nil { | ||||
| 						log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) | ||||
| 					} | ||||
| @@ -264,14 +275,13 @@ func (db *DB) AddArticleToCurrentIssue(id int64) error { | ||||
|  | ||||
| func (db *DB) DeleteArticle(id int64) error { | ||||
| 	articlesTagsQuery := "DELETE FROM articles_tags WHERE article_id = ?" | ||||
| 	articlesQuery := "DELETE FROM articles WHERE id = ?" | ||||
|  | ||||
| 	_, err := db.Exec(articlesTagsQuery, id) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error deleting article %v from DB: %v", id, err) | ||||
| 	} | ||||
|  | ||||
| 	articlesQuery := "DELETE FROM articles WHERE id = ?" | ||||
|  | ||||
| 	_, err = db.Exec(articlesQuery, id) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error deleting article %v from DB: %v", id, err) | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package backend | ||||
| import ( | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"io/fs" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| @@ -12,6 +13,7 @@ import ( | ||||
|  | ||||
| type Config struct { | ||||
| 	ArticleDir  string | ||||
| 	ConfigFile  string | ||||
| 	DBName      string | ||||
| 	Description string | ||||
| 	Domain      string | ||||
| @@ -30,56 +32,72 @@ type Config struct { | ||||
| func newConfig() *Config { | ||||
| 	return &Config{ | ||||
| 		ArticleDir:  "/var/www/cpolis/articles", | ||||
| 		ConfigFile:  "/etc/cpolis/config.toml", | ||||
| 		DBName:      "cpolis", | ||||
| 		FirebaseKey: "/var/www/cpolis/serviceAccountKey.json", | ||||
| 		KeyFile:     "/var/www/cpolis/cpolis.key", | ||||
| 		LogFile:     "/var/log/cpolis.log", | ||||
| 		PDFDir:      "/var/www/cpolis/pdfs", | ||||
| 		PicsDir:     "/var/www/cpolis/pics", | ||||
| 		Port:        ":8080", | ||||
| 		RSSFile:     "/var/www/cpolis/cpolis.rss", | ||||
| 		WebDir:      "/var/www/cpolis/web", | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (c *Config) readFile() error { | ||||
| 	cfgFile, err := filepath.Abs(os.Getenv("HOME") + "/.config/cpolis/config.toml") | ||||
| func mkDir(path string, perm fs.FileMode) (string, error) { | ||||
| 	var err error | ||||
|  | ||||
| 	stringSlice := strings.Split(path, "/") | ||||
| 	name := stringSlice[len(stringSlice)-1] | ||||
|  | ||||
| 	path, err = filepath.Abs(path) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error getting absolute path for config file: %v", err) | ||||
| 		return "", fmt.Errorf("error finding absolute path for %v directory: %v", name, err) | ||||
| 	} | ||||
| 	if err = os.MkdirAll(path, perm); err != nil { | ||||
| 		return "", fmt.Errorf("error creating %v directory: %v", name, err) | ||||
| 	} | ||||
|  | ||||
| 	_, err = os.Stat(cfgFile) | ||||
| 	if os.IsNotExist(err) { | ||||
| 		fileStrings := strings.Split(cfgFile, "/") | ||||
| 	return path, nil | ||||
| } | ||||
|  | ||||
| 		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) | ||||
| 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) { | ||||
| 		dir := strings.Join(stringSlice[:len(stringSlice)-1], "/") | ||||
| 		if err = os.MkdirAll(dir, dirPerm); err != nil { | ||||
| 			return "", fmt.Errorf("error creating %v: %v", dir, err) | ||||
| 		} | ||||
|  | ||||
| 		fileName := fileStrings[len(fileStrings)-1] | ||||
| 		fileName := stringSlice[len(stringSlice)-1] | ||||
| 		file, err := os.Create(dir + "/" + fileName) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("error creating config file: %v", err) | ||||
| 			return "", fmt.Errorf("error creating %v: %v", fileName, err) | ||||
| 		} | ||||
| 		defer file.Close() | ||||
| 		if err = file.Chmod(0644); err != nil { | ||||
| 			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) | ||||
| 		if err = file.Chmod(filePerm); err != nil { | ||||
| 			return "", fmt.Errorf("error setting permissions for %v: %v", fileName, err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| 	return path, nil | ||||
| } | ||||
|  | ||||
| func (c *Config) handleCliArgs() error { | ||||
| 	var port int | ||||
| 	var err error | ||||
| 	port := 8080 | ||||
|  | ||||
| 	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.Description, "desc", c.Description, "channel description") | ||||
| 	flag.StringVar(&c.Domain, "domain", c.Domain, "domain name") | ||||
| @@ -95,58 +113,117 @@ func (c *Config) handleCliArgs() error { | ||||
| 	flag.IntVar(&port, "port", port, "port") | ||||
| 	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) | ||||
| 	if port != 0 { | ||||
| 		c.Port = fmt.Sprint(":", port) | ||||
| 	} | ||||
|  | ||||
| 	c.FirebaseKey, err = filepath.Abs(c.FirebaseKey) | ||||
| 	c.ConfigFile, err = mkFile(c.ConfigFile, 0600, 0700) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error finding absolute path for Firebase service account key file: %v", err) | ||||
| 		return fmt.Errorf("error setting up file: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	c.KeyFile, err = filepath.Abs(c.KeyFile) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (c *Config) handleFile(configFile string) error { | ||||
| 	_, err := toml.DecodeFile(configFile, c) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error finding absolute path for key file: %v", err) | ||||
| 		return fmt.Errorf("error reading config file: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	c.LogFile, err = filepath.Abs(c.LogFile) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| 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 { | ||||
| 		return fmt.Errorf("error finding absolute path for log file: %v", err) | ||||
| 		return fmt.Errorf("error setting up directory: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	c.PDFDir, err = filepath.Abs(c.PDFDir) | ||||
| 	if err != nil { | ||||
| 		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.DBName != defaultConfig.DBName { | ||||
| 		c.DBName = cliConfig.DBName | ||||
| 	} | ||||
|  | ||||
| 	c.PicsDir, err = filepath.Abs(c.PicsDir) | ||||
| 	if err != nil { | ||||
| 		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.Description != defaultConfig.Description { | ||||
| 		c.Description = cliConfig.Description | ||||
| 	} | ||||
|  | ||||
| 	c.Port = fmt.Sprint(":", port) | ||||
|  | ||||
| 	c.RSSFile, err = filepath.Abs(c.RSSFile) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error finding absolute path for RSS file: %v", err) | ||||
| 	if cliConfig.Domain != defaultConfig.Domain { | ||||
| 		c.Domain = cliConfig.Domain | ||||
| 	} | ||||
|  | ||||
| 	c.WebDir, err = filepath.Abs(c.WebDir) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error finding absolute path for web directory: %v", err) | ||||
| 	if cliConfig.FirebaseKey != defaultConfig.FirebaseKey { | ||||
| 		c.FirebaseKey = cliConfig.FirebaseKey | ||||
| 	} | ||||
| 	if err = os.MkdirAll(c.WebDir, 0755); err != nil { | ||||
| 		return fmt.Errorf("error creating web directory: %v", err) | ||||
| 	c.FirebaseKey, err = mkFile(c.FirebaseKey, 0600, 0700) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error setting up file: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if cliConfig.KeyFile != defaultConfig.KeyFile { | ||||
| 		c.KeyFile = cliConfig.KeyFile | ||||
| 	} | ||||
| 	c.KeyFile, err = mkFile(c.KeyFile, 0600, 0700) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error setting up file: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if cliConfig.Link != defaultConfig.Link { | ||||
| 		c.Link = cliConfig.Link | ||||
| 	} | ||||
|  | ||||
| 	if cliConfig.LogFile != defaultConfig.LogFile { | ||||
| 		c.LogFile = cliConfig.LogFile | ||||
| 	} | ||||
| 	c.LogFile, err = mkFile(c.LogFile, 0600, 0700) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error setting up file: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if cliConfig.PDFDir != defaultConfig.PDFDir { | ||||
| 		c.PDFDir = cliConfig.PDFDir | ||||
| 	} | ||||
| 	c.PDFDir, err = mkDir(c.PDFDir, 0700) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error setting up directory: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if cliConfig.PicsDir != defaultConfig.PicsDir { | ||||
| 		c.PicsDir = cliConfig.PicsDir | ||||
| 	} | ||||
| 	c.PicsDir, err = mkDir(c.PicsDir, 0700) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error setting up directory: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if cliConfig.Port != defaultConfig.Port { | ||||
| 		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 | ||||
| @@ -154,13 +231,19 @@ func (c *Config) handleCliArgs() error { | ||||
|  | ||||
| func HandleConfig() (*Config, error) { | ||||
| 	config := newConfig() | ||||
| 	cliConfig := newConfig() | ||||
|  | ||||
| 	if err := config.readFile(); err != nil { | ||||
| 		return nil, fmt.Errorf("error reading config file: %v", err) | ||||
| 	if err := cliConfig.handleCliArgs(); err != nil { | ||||
| 		return nil, fmt.Errorf("error handling cli arguments: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if err := config.handleCliArgs(); err != nil { | ||||
| 		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 | ||||
|   | ||||
							
								
								
									
										28
									
								
								cmd/backend/files.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								cmd/backend/files.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| package backend | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"os" | ||||
| ) | ||||
|  | ||||
| func CopyFile(src, dst string) error { | ||||
| 	srcFile, err := os.Open(src) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error opening source file: %v", err) | ||||
| 	} | ||||
| 	defer srcFile.Close() | ||||
|  | ||||
| 	dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error opening destination file: %v", err) | ||||
| 	} | ||||
| 	defer dstFile.Close() | ||||
|  | ||||
| 	_, err = io.Copy(dstFile, srcFile) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error copying file: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	return dstFile.Sync() | ||||
| } | ||||
| @@ -8,9 +8,7 @@ import ( | ||||
| 	"google.golang.org/api/option" | ||||
| ) | ||||
|  | ||||
| type Client struct { | ||||
| 	*auth.Client | ||||
| } | ||||
| type Client struct{ *auth.Client } | ||||
|  | ||||
| func NewClient(c *Config) (*Client, error) { | ||||
| 	var err error | ||||
|   | ||||
| @@ -17,7 +17,7 @@ func GenerateRSS(c *Config, db *DB) (*string, error) { | ||||
| 		Items:       make([]*rss.Item, 0), | ||||
| 	} | ||||
|  | ||||
| 	articles, err := db.GetCertainArticles(true, false) | ||||
| 	articles, err := db.GetCertainArticles("published", true) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("error getting published articles for RSS feed: %v", err) | ||||
| 	} | ||||
| @@ -31,7 +31,13 @@ func GenerateRSS(c *Config, db *DB) (*string, error) { | ||||
| 		for _, tag := range tags { | ||||
| 			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) | ||||
| 		if err != nil { | ||||
| @@ -57,9 +63,8 @@ func GenerateRSS(c *Config, db *DB) (*string, error) { | ||||
| 			PubDate:     article.Created.Format(time.RFC1123Z), | ||||
| 			Title:       articleTitle, | ||||
| 		} | ||||
| 		fmt.Println(article.Link, ": ", len(article.Link)) | ||||
|  | ||||
| 		if article.Title == "Autogenerated cpolis Issue Article" { | ||||
| 		if article.AutoGenerated { | ||||
| 			item.Enclosure = &rss.Enclosure{ | ||||
| 				Url:    article.EncURL, | ||||
| 				Lenght: article.EncLength, | ||||
|   | ||||
| @@ -10,9 +10,10 @@ import ( | ||||
| 	"github.com/gorilla/sessions" | ||||
| ) | ||||
|  | ||||
| type CookieStore struct { | ||||
| 	sessions.CookieStore | ||||
| } | ||||
| type ( | ||||
| 	CookieStore struct{ sessions.CookieStore } | ||||
| 	Session     struct{ sessions.Session } | ||||
| ) | ||||
|  | ||||
| func NewKey() ([]byte, error) { | ||||
| 	key := make([]byte, 32) | ||||
|   | ||||
| @@ -22,6 +22,17 @@ const ( | ||||
| 	PreviewMode | ||||
| ) | ||||
|  | ||||
| type EditorHTMLData struct { | ||||
| 	Selected     map[int64]bool | ||||
| 	Content      string | ||||
| 	Action       string | ||||
| 	ActionTitle  string | ||||
| 	ActionButton string | ||||
| 	HTMLContent  template.HTML | ||||
| 	Article      *b.Article | ||||
| 	Tags         []*b.Tag | ||||
| } | ||||
|  | ||||
| func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 	return func(w http.ResponseWriter, r *http.Request) { | ||||
| 		session, err := getSession(w, r, c, s) | ||||
| @@ -29,22 +40,13 @@ func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		type editorHTMLData struct { | ||||
| 			Title       string | ||||
| 			Description string | ||||
| 			Content     string | ||||
| 			HTMLContent template.HTML | ||||
| 			Tags        []*b.Tag | ||||
| 			Mode        int | ||||
| 		} | ||||
| 		data := &EditorHTMLData{Action: "submit"} | ||||
|  | ||||
| 		var data editorHTMLData | ||||
| 		if session.Values["article"] == nil { | ||||
| 			data = editorHTMLData{} | ||||
| 			data = &EditorHTMLData{Article: new(b.Article)} | ||||
| 		} else { | ||||
| 			data = session.Values["article"].(editorHTMLData) | ||||
| 			data = session.Values["article"].(*EditorHTMLData) | ||||
| 		} | ||||
| 		data.Mode = EditMode | ||||
|  | ||||
| 		data.Tags, err = db.GetTagList() | ||||
| 		if err != nil { | ||||
| @@ -73,11 +75,13 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 		} | ||||
|  | ||||
| 		article := &b.Article{ | ||||
| 			Title:       r.PostFormValue("article-title"), | ||||
| 			Description: r.PostFormValue("article-description"), | ||||
| 			Published:   false, | ||||
| 			Rejected:    false, | ||||
| 			AuthorID:    session.Values["id"].(int64), | ||||
| 			Title:         r.PostFormValue("article-title"), | ||||
| 			Description:   r.PostFormValue("article-description"), | ||||
| 			Published:     false, | ||||
| 			Rejected:      false, | ||||
| 			AuthorID:      session.Values["id"].(int64), | ||||
| 			IsInIssue:     r.PostFormValue("issue") == "on", | ||||
| 			AutoGenerated: false, | ||||
| 		} | ||||
|  | ||||
| 		article.ID, err = db.AddArticle(article) | ||||
| @@ -153,6 +157,7 @@ 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: "description", Value: description}, | ||||
| 			&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 { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| @@ -182,22 +187,41 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func ShowUnpublishedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| func ShowUnpublishedUnrejectedAndPublishedRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 	return func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if _, err := getSession(w, r, c, s); err != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		unpublishedArticles, err := db.GetCertainArticles(false, false) | ||||
| 		rejectedArticles, err := db.GetCertainArticles("rejected", true) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		articles := make([]*b.Article, 0) | ||||
| 		for _, article := range rejectedArticles { | ||||
| 			if article.Published { | ||||
| 				articles = append(articles, article) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		unpublishedArticles, err := db.GetCertainArticles("published", false) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		for _, article := range unpublishedArticles { | ||||
| 			if !article.Rejected { | ||||
| 				articles = append(articles, article) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/unpublished-articles.html") | ||||
| 		tmpl = template.Must(tmpl, err) | ||||
| 		tmpl.ExecuteTemplate(w, "page-content", unpublishedArticles) | ||||
| 		template.Must(tmpl, err).ExecuteTemplate(w, "page-content", articles) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -208,13 +232,12 @@ func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerF | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		type htmlData struct { | ||||
| 		data := new(struct { | ||||
| 			MyIDs            map[int64]bool | ||||
| 			RejectedArticles []*b.Article | ||||
| 		} | ||||
| 		data := new(htmlData) | ||||
| 		}) | ||||
|  | ||||
| 		data.RejectedArticles, err = db.GetCertainArticles(false, true) | ||||
| 		data.RejectedArticles, err = db.GetCertainArticles("rejected", true) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| @@ -234,94 +257,12 @@ func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerF | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func ReviewUnpublishedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 	return func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if _, err := getSession(w, r, c, s); err != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		type htmlData struct { | ||||
| 			Title       string | ||||
| 			Description string | ||||
| 			Content     template.HTML | ||||
| 			Tags        []*b.Tag | ||||
| 			ID          int64 | ||||
| 		} | ||||
|  | ||||
| 		var err error | ||||
| 		data := new(htmlData) | ||||
|  | ||||
| 		data.ID, err = strconv.ParseInt(r.PathValue("id"), 10, 64) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		article, err := db.GetArticle(data.ID) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		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) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		content, err := b.ConvertToHTML(string(contentBytes)) | ||||
| 		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) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/to-be-published.html") | ||||
| 		tmpl = template.Must(tmpl, err) | ||||
| 		tmpl.ExecuteTemplate(w, "page-content", data) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 	return func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if _, err := getSession(w, r, c, s); err != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		type htmlData struct { | ||||
| 			Selected map[int64]bool | ||||
| 			Article  *b.Article | ||||
| 			Content  string | ||||
| 			Tags     []*b.Tag | ||||
| 		} | ||||
| 		data := new(htmlData) | ||||
|  | ||||
| 		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| @@ -329,6 +270,7 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		data := new(EditorHTMLData) | ||||
| 		data.Article, err = db.GetArticle(id) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| @@ -337,14 +279,13 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler | ||||
| 		} | ||||
|  | ||||
| 		articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md") | ||||
| 		contentBytes, err := os.ReadFile(articleAbsName) | ||||
| 		content, err := os.ReadFile(articleAbsName) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		data.Content = string(contentBytes) | ||||
| 		data.Content = string(content) | ||||
|  | ||||
| 		data.Tags, err = db.GetTagList() | ||||
| 		if err != nil { | ||||
| @@ -364,7 +305,9 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler | ||||
| 			data.Selected[tag.ID] = true | ||||
| 		} | ||||
|  | ||||
| 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/rework-article.html") | ||||
| 		data.Action = fmt.Sprint("resubmit/", data.Article.ID) | ||||
|  | ||||
| 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html") | ||||
| 		tmpl = template.Must(tmpl, err) | ||||
| 		tmpl.ExecuteTemplate(w, "page-content", data) | ||||
| 	} | ||||
| @@ -378,13 +321,20 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 		} | ||||
|  | ||||
| 		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 { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if err = db.AddArticleToCurrentIssue(id); err != nil { | ||||
| 		if err = db.AddArticleToCurrentIssue(article.ID); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| @@ -400,6 +350,36 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if article.EditedID != 0 { | ||||
| 			oldArticle, err := db.GetArticle(article.EditedID) | ||||
| 			if err != nil { | ||||
| 				log.Println(err) | ||||
| 				http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			if err = db.DeleteArticle(oldArticle.ID); err != nil { | ||||
| 				log.Println(err) | ||||
| 				http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			if err = os.Remove(fmt.Sprint(c.ArticleDir, "/", oldArticle.ID, ".md")); err != nil { | ||||
| 				log.Println(err) | ||||
| 				http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			if err = db.UpdateAttributes( | ||||
| 				&b.Attribute{Table: "articles", ID: id, AttName: "link", Value: fmt.Sprint(c.Domain, "/article/serve/", article.ID)}, | ||||
| 				&b.Attribute{Table: "articles", ID: id, AttName: "edited_id", Value: 0}, | ||||
| 			); err != nil { | ||||
| 				log.Println(err) | ||||
| 				http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		feed, err := b.GenerateRSS(c, db) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| @@ -432,9 +412,7 @@ func RejectArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if err = db.UpdateAttributes( | ||||
| 			&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: true}, | ||||
| 		); err != nil { | ||||
| 		if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: true}); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| @@ -508,71 +486,75 @@ func UploadArticleImage(c *b.Config, s *b.CookieStore) http.HandlerFunc { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func ShowPublishedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| func ShowPublishedArticles(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.HandlerFunc { | ||||
| 	return func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if _, err := getSession(w, r, c, s); err != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		publishedArticles, err := db.GetCertainArticles(true, false) | ||||
| 		data := new(struct { | ||||
| 			Action   string | ||||
| 			Articles []*b.Article | ||||
| 		}) | ||||
| 		data.Action = action | ||||
|  | ||||
| 		publishedArticles, err := db.GetCertainArticles("published", true) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		filteredArticles := make([]*b.Article, 0) | ||||
| 		for _, article := range publishedArticles { | ||||
| 			if article.Title != "Autogenerated cpolis Issue Article" { | ||||
| 				filteredArticles = append(filteredArticles, article) | ||||
| 			if !article.AutoGenerated { | ||||
| 				data.Articles = append(data.Articles, article) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/published-articles.html") | ||||
| 		tmpl = template.Must(tmpl, err) | ||||
| 		tmpl.ExecuteTemplate(w, "page-content", filteredArticles) | ||||
| 		tmpl.ExecuteTemplate(w, "page-content", data) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func ReviewArticleForDeletion(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| func ReviewArticle(c *b.Config, db *b.DB, s *b.CookieStore, action, title, button string) http.HandlerFunc { | ||||
| 	return func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if _, err := getSession(w, r, c, s); err != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		type htmlData struct { | ||||
| 			Title       string | ||||
| 			Description string | ||||
| 			Content     template.HTML | ||||
| 			Tags        []*b.Tag | ||||
| 			ID          int64 | ||||
| 		} | ||||
|  | ||||
| 		var err error | ||||
| 		data := new(htmlData) | ||||
|  | ||||
| 		data.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.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		article, err := db.GetArticle(data.ID) | ||||
| 		article, err := db.GetArticle(id) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		data.Title, err = b.ConvertToPlain(article.Title) | ||||
| 		data := &EditorHTMLData{ | ||||
| 			Article: &b.Article{ | ||||
| 				ID:        id, | ||||
| 				IsInIssue: article.IsInIssue, | ||||
| 			}, | ||||
| 			Action:       action, | ||||
| 			ActionTitle:  title, | ||||
| 			ActionButton: button, | ||||
| 		} | ||||
|  | ||||
| 		data.Article.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) | ||||
| 		data.Article.Description, err = b.ConvertToPlain(article.Description) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| @@ -580,31 +562,29 @@ func ReviewArticleForDeletion(c *b.Config, db *b.DB, s *b.CookieStore) http.Hand | ||||
| 		} | ||||
|  | ||||
| 		articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md") | ||||
| 		contentBytes, err := os.ReadFile(articleAbsName) | ||||
| 		content, err := os.ReadFile(articleAbsName) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 		data.Content, err = b.ConvertToHTML(string(content)) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 		data.HTMLContent = template.HTML(data.Content) | ||||
|  | ||||
| 		data.Tags, err = db.GetArticleTags(id) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		content, err := b.ConvertToHTML(string(contentBytes)) | ||||
| 		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) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/to-be-deleted.html") | ||||
| 		tmpl = template.Must(tmpl, err) | ||||
| 		tmpl.ExecuteTemplate(w, "page-content", data) | ||||
| 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/review-article.html") | ||||
| 		template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -651,3 +631,110 @@ func DeleteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 		tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int)) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func AllowEditArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 	return func(w http.ResponseWriter, r *http.Request) { | ||||
| 		session, err := getSession(w, r, c, s) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		oldID, err := strconv.ParseInt(r.PathValue("id"), 10, 64) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 		fmt.Println(oldID) | ||||
|  | ||||
| 		oldArticle, err := db.GetArticle(oldID) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		newArticle := oldArticle | ||||
| 		newArticle.Published = false | ||||
| 		newArticle.Rejected = true | ||||
| 		newArticle.EditedID = oldArticle.ID | ||||
|  | ||||
| 		newID, err := db.AddArticle(newArticle) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: oldID, AttName: "edited_id", Value: newID}); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if err = b.CopyFile(fmt.Sprint(c.ArticleDir, "/", oldID, ".md"), fmt.Sprint(c.ArticleDir, "/", newID, ".md")); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html") | ||||
| 		tmpl = template.Must(tmpl, err) | ||||
| 		tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int)) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func EditArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 	return func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if _, err := getSession(w, r, c, s); err != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		data := new(EditorHTMLData) | ||||
|  | ||||
| 		data.Article, err = db.GetArticle(id) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		content, err := os.ReadFile(fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md")) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 		data.Content = string(content) | ||||
|  | ||||
| 		data.Tags, err = db.GetArticleTags(data.Article.ID) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		selectedTags, err := db.GetArticleTags(id) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 		data.Selected = make(map[int64]bool) | ||||
| 		for _, tag := range selectedTags { | ||||
| 			data.Selected[tag.ID] = true | ||||
| 		} | ||||
|  | ||||
| 		data.Action = fmt.Sprint("save/", data.Article.ID) | ||||
|  | ||||
| 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html") | ||||
| 		template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -30,6 +30,14 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		title := r.PostFormValue("issue-title") | ||||
| 		if len(title) == 0 { | ||||
| 			err = fmt.Errorf("error: no title for issue specified") | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if session.Values["issue-image"] == nil { | ||||
| 			err := "error: Image required" | ||||
| 			log.Println(err) | ||||
| @@ -55,20 +63,17 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		imgSize := imgInfo.Size() | ||||
| 		mimeType := mime.TypeByExtension(filepath.Ext(imgAbsName)) | ||||
|  | ||||
| 		article := &b.Article{ | ||||
| 			Title:     "Autogenerated cpolis Issue Article", | ||||
| 			EncURL:    fmt.Sprint(c.Domain, "/image/serve/", imgFileName), | ||||
| 			EncLength: int(imgSize), | ||||
| 			EncType:   mimeType, | ||||
| 			Published: true, | ||||
| 			Rejected:  false, | ||||
| 			Created:   time.Now(), | ||||
| 			AuthorID:  session.Values["id"].(int64), | ||||
| 			Title:         title, | ||||
| 			EncURL:        fmt.Sprint(c.Domain, "/image/serve/", imgFileName), | ||||
| 			EncLength:     int(imgInfo.Size()), | ||||
| 			EncType:       mime.TypeByExtension(filepath.Ext(imgAbsName)), | ||||
| 			Published:     true, | ||||
| 			Rejected:      false, | ||||
| 			Created:       time.Now(), | ||||
| 			AuthorID:      session.Values["id"].(int64), | ||||
| 			AutoGenerated: true, | ||||
| 		} | ||||
| 		fmt.Println(article.Link) | ||||
|  | ||||
| 		article.ID, err = db.AddArticle(article) | ||||
| 		if err != nil { | ||||
|   | ||||
							
								
								
									
										59
									
								
								cmd/frontend/pdf.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								cmd/frontend/pdf.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| 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) | ||||
| 	} | ||||
| } | ||||
| @@ -1,6 +1,7 @@ | ||||
| package frontend | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"html/template" | ||||
| 	"log" | ||||
| @@ -26,6 +27,26 @@ func saveSession(w http.ResponseWriter, r *http.Request, s *b.CookieStore, u *b. | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // getSession is used for verifying that the user is logged in and returns their session and an error. | ||||
| func getSession(w http.ResponseWriter, r *http.Request, c *b.Config, s *b.CookieStore) (*b.Session, error) { | ||||
| 	msg := "Keine gültige Session. Bitte erneut anmelden." | ||||
| 	tmpl, tmplErr := template.ParseFiles(c.WebDir+"/templates/index.html", c.WebDir+"/templates/login.html") | ||||
|  | ||||
| 	tmpSession, err := s.Get(r, "cookie") | ||||
| 	if err != nil { | ||||
| 		template.Must(tmpl, tmplErr).ExecuteTemplate(w, "page-content", msg) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	session := &b.Session{Session: *tmpSession} | ||||
| 	if session.IsNew { | ||||
| 		template.Must(tmpl, tmplErr).ExecuteTemplate(w, "page-content", msg) | ||||
| 		return session, errors.New("error: no existing session") | ||||
| 	} | ||||
|  | ||||
| 	return session, nil | ||||
| } | ||||
|  | ||||
| func HomePage(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 	return func(w http.ResponseWriter, r *http.Request) { | ||||
| 		numRows, err := db.CountEntries("users") | ||||
|   | ||||
| @@ -277,12 +277,12 @@ func ShowAllUsers(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.H | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		type htmlData struct { | ||||
| 		data := new(struct { | ||||
| 			Users  map[int64]*b.User | ||||
| 			Action string | ||||
| 		} | ||||
| 		}) | ||||
|  | ||||
| 		data := &htmlData{Action: action} | ||||
| 		data.Action = action | ||||
| 		data.Users, err = db.GetAllUsers() | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
|   | ||||
| @@ -1,29 +0,0 @@ | ||||
| package frontend | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"html/template" | ||||
| 	"net/http" | ||||
|  | ||||
| 	"github.com/gorilla/sessions" | ||||
| 	b "streifling.com/jason/cpolis/cmd/backend" | ||||
| ) | ||||
|  | ||||
| // getSession is used for verifying that the user is logged in and returns their session and an error. | ||||
| func getSession(w http.ResponseWriter, r *http.Request, c *b.Config, s *b.CookieStore) (*sessions.Session, error) { | ||||
| 	msg := "Keine gültige Session. Bitte erneut anmelden." | ||||
| 	tmpl, tmplErr := template.ParseFiles(c.WebDir+"/templates/index.html", c.WebDir+"/templates/login.html") | ||||
|  | ||||
| 	session, err := s.Get(r, "cookie") | ||||
| 	if err != nil { | ||||
| 		template.Must(tmpl, tmplErr).ExecuteTemplate(w, "page-content", msg) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if session.IsNew { | ||||
| 		template.Must(tmpl, tmplErr).ExecuteTemplate(w, "page-content", msg) | ||||
| 		return session, errors.New("error: no existing session") | ||||
| 	} | ||||
|  | ||||
| 	return session, nil | ||||
| } | ||||
							
								
								
									
										13
									
								
								cmd/main.go
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								cmd/main.go
									
									
									
									
									
								
							| @@ -49,15 +49,19 @@ func main() { | ||||
| 		http.FileServer(http.Dir(config.WebDir+"/static/")))) | ||||
| 	mux.HandleFunc("/", f.HomePage(config, db, store)) | ||||
|  | ||||
| 	mux.HandleFunc("GET /article/all-published", f.ShowPublishedArticles(config, db, store)) | ||||
| 	mux.HandleFunc("GET /article/allow-edit/{id}", f.AllowEditArticle(config, db, store)) | ||||
| 	mux.HandleFunc("GET /article/all-published/review-edit", f.ShowPublishedArticles(config, db, store, "review-edit")) | ||||
| 	mux.HandleFunc("GET /article/all-published/delete", f.ShowPublishedArticles(config, db, store, "review-delete")) | ||||
| 	mux.HandleFunc("GET /article/all-rejected", f.ShowRejectedArticles(config, db, store)) | ||||
| 	mux.HandleFunc("GET /article/all-unpublished", f.ShowUnpublishedArticles(config, db, store)) | ||||
| 	mux.HandleFunc("GET /article/all-unpublished-unrejected-and-published-rejected", f.ShowUnpublishedUnrejectedAndPublishedRejectedArticles(config, db, store)) | ||||
| 	mux.HandleFunc("GET /article/delete/{id}", f.DeleteArticle(config, db, store)) | ||||
| 	mux.HandleFunc("GET /article/edit/{id}", f.EditArticle(config, db, store)) | ||||
| 	mux.HandleFunc("GET /article/publish/{id}", f.PublishArticle(config, db, store)) | ||||
| 	mux.HandleFunc("GET /article/reject/{id}", f.RejectArticle(config, db, store)) | ||||
| 	mux.HandleFunc("GET /article/review-deletion/{id}", f.ReviewArticleForDeletion(config, db, store)) | ||||
| 	mux.HandleFunc("GET /article/review-delete/{id}", f.ReviewArticle(config, db, store, "delete", "Artikel löschen", "Löschen")) | ||||
| 	mux.HandleFunc("GET /article/review-edit/{id}", f.ReviewArticle(config, db, store, "allow-edit", "Artikel bearbeiten", "Bearbeiten erlauben")) | ||||
| 	mux.HandleFunc("GET /article/review-rejected/{id}", f.ReviewRejectedArticle(config, db, store)) | ||||
| 	mux.HandleFunc("GET /article/review-unpublished/{id}", f.ReviewUnpublishedArticle(config, db, store)) | ||||
| 	mux.HandleFunc("GET /article/review-unpublished/{id}", f.ReviewArticle(config, db, store, "publish", "Artikel veröffentlichen", "Veröffentlichen")) | ||||
| 	mux.HandleFunc("GET /article/serve/{id}", c.ServeArticle(config, db)) | ||||
| 	mux.HandleFunc("GET /article/write", f.WriteArticle(config, db, store)) | ||||
| 	mux.HandleFunc("GET /hub", f.ShowHub(config, db, store)) | ||||
| @@ -81,6 +85,7 @@ func main() { | ||||
| 	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 /pdf/upload", f.UploadPDF(config, 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-first", f.AddFirstUser(config, db, store)) | ||||
|   | ||||
| @@ -21,18 +21,21 @@ CREATE TABLE issues ( | ||||
| ); | ||||
|  | ||||
| CREATE TABLE articles ( | ||||
|     id          INT             AUTO_INCREMENT, | ||||
|     title       VARCHAR(255)    NOT NULL, | ||||
|     created     TIMESTAMP       DEFAULT CURRENT_TIMESTAMP, | ||||
|     description TEXT            NOT NULL, | ||||
|     link        VARCHAR(255), | ||||
|     enc_url     VARCHAR(255), | ||||
|     enc_length  INT, | ||||
|     enc_type    VARCHAR(255), | ||||
|     published   BOOL            NOT NULL, | ||||
|     rejected    BOOL            NOT NULL, | ||||
|     author_id   INT             NOT NULL, | ||||
|     issue_id    INT             NOT NULL, | ||||
|     id              INT             AUTO_INCREMENT, | ||||
|     title           VARCHAR(255)    NOT NULL, | ||||
|     created         TIMESTAMP       DEFAULT CURRENT_TIMESTAMP, | ||||
|     description     TEXT            NOT NULL, | ||||
|     link            VARCHAR(255), | ||||
|     enc_url         VARCHAR(255), | ||||
|     enc_length      INT, | ||||
|     enc_type        VARCHAR(255), | ||||
|     published       BOOL            NOT NULL, | ||||
|     rejected        BOOL            NOT NULL, | ||||
|     author_id       INT             NOT NULL, | ||||
|     issue_id        INT             NOT NULL, | ||||
|     edited_id       INT, | ||||
|     is_in_issue     BOOL            NOT NULL, | ||||
|     auto_generated  BOOL            NOT NULL, | ||||
|     PRIMARY KEY (id), | ||||
|     FOREIGN KEY (author_id) REFERENCES users (id), | ||||
|     FOREIGN KEY (issue_id) REFERENCES issues (id) | ||||
|   | ||||
| @@ -7,4 +7,5 @@ module.exports = { | ||||
|     plugins: [ | ||||
|         require('@tailwindcss/typography') | ||||
|     ], | ||||
|     darkMode: 'selector', | ||||
| } | ||||
|   | ||||
| @@ -43,7 +43,9 @@ chmod +x $CPOLIS_DIR/tailwindcss | ||||
| $CPOLIS_DIR/tailwindcss -i $CPOLIS_DIR/web/static/css/input.css -o $CPOLIS_DIR/web/static/css/style.css | ||||
|  | ||||
| echo '\nBuilding cpolis...' >&2 | ||||
| go build -o $BIN_DIR/cpolis $CPOLIS_DIR/cmd/main.go | ||||
| cd $CPOLIS_DIR | ||||
| go build -o $BIN_DIR/cpolis cmd/main.go | ||||
| cd | ||||
|  | ||||
| echo '\nSetting up system files...' >&2 | ||||
| sudo chown root:root $BIN_DIR/cpolis | ||||
|   | ||||
| @@ -2,40 +2,71 @@ | ||||
| @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; | ||||
| h3 { | ||||
|     @apply font-bold mb-2 text-xl; | ||||
| } | ||||
|  | ||||
| input[type="password"], | ||||
| input[type="text"] { | ||||
|     @apply border h-8 rounded-md; | ||||
|     @apply bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 h-8 rounded-md; | ||||
| } | ||||
|  | ||||
| textarea { | ||||
|     @apply border h-32 rounded-md; | ||||
|     @apply bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 h-32 rounded-md; | ||||
| } | ||||
|  | ||||
| .btn-area-1 { | ||||
|     @apply grid grid-cols-1 gap-x-4 gap-y-1 mt-4; | ||||
| } | ||||
|  | ||||
| .btn-area { | ||||
|     @apply flex gap-4 mt-4; | ||||
|     @apply grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-1 mt-4; | ||||
| } | ||||
|  | ||||
| .btn-area-3 { | ||||
|     @apply grid grid-cols-1 md:grid-cols-3 gap-x-4 gap-y-1 mt-4; | ||||
| } | ||||
|  | ||||
| .btn { | ||||
|     @apply bg-slate-200 border my-2 px-3 py-2 rounded-md w-full hover:bg-slate-100; | ||||
|     @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; | ||||
| } | ||||
|  | ||||
| .action-btn { | ||||
|     @apply bg-slate-800 border my-2 px-3 py-2 rounded-md text-slate-50 w-full hover:bg-slate-700; | ||||
|     @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; | ||||
| } | ||||
|  | ||||
| .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 | ||||
| } | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| <h2>Neuer Tag</h2> | ||||
|  | ||||
| <form> | ||||
|     <input required name="tag" placeholder="Tag eingeben" type="text" /> | ||||
|     <input class="w-full" name="tag" placeholder="Tag eingeben" required type="text" /> | ||||
|     <div class="btn-area"> | ||||
|         <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> | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| <h2>Neuer Benutzer</h2> | ||||
|  | ||||
| <form> | ||||
|     <div class="grid grid-cols-3 gap-4"> | ||||
|     <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> | ||||
|         <div> | ||||
|             <label for="username">Benutzername</label> | ||||
|             <input class="w-full" required name="username" type="text" value="{{.UserName}}" /> | ||||
| @@ -25,7 +25,7 @@ | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex gap-4"> | ||||
|     <div class="flex flex-wrap gap-4"> | ||||
|         <div> | ||||
|             <input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} /> | ||||
|             <label for="author">Autor</label> | ||||
|   | ||||
| @@ -7,7 +7,7 @@ | ||||
|             <h3>Aktuelle Artikel</h3> | ||||
|             <div class="flex flex-col gap-4"> | ||||
|                 {{range .}} | ||||
|                 <div class="border px-2 py-1 rounded-md"> | ||||
|                 <div class="border border-slate-200 dark:border-slate-800 px-2 py-1 rounded-md"> | ||||
|                     <h1 class="font-bold text-2xl">{{.Title}}</h1> | ||||
|                     <p>{{.Description}}</p> | ||||
|                 </div> | ||||
| @@ -16,8 +16,13 @@ | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <h3>Cover</h3> | ||||
|             <input id="image-upload" name="issue-image" type="file" required hx-post="/issue/upload-image" /> | ||||
|             <h3>Titelseite</h3> | ||||
|             <div class="grid grid-cols-2 gap-4 items-center"> | ||||
|                 <input class="h-full" name="issue-title" placeholder="Titel" required 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> | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| <h2>Profil bearbeiten</h2> | ||||
|  | ||||
| <form> | ||||
|     <div class="grid grid-cols-3 gap-4"> | ||||
|     <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> | ||||
|         <div> | ||||
|             <label for="username">Benutzername</label> | ||||
|             <input class="w-full" name="username" type="text" value="{{.UserName}}" /> | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| <h2>Profil von {{.FirstName}} {{.LastName}} bearbeiten</h2> | ||||
|  | ||||
| <form> | ||||
|     <div class="grid grid-cols-3 gap-4"> | ||||
|     <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> | ||||
|         <div> | ||||
|             <label for="username">Benutzername</label> | ||||
|             <input class="w-full" name="username" type="text" value="{{.UserName}}" /> | ||||
| @@ -29,7 +29,7 @@ | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex gap-4"> | ||||
|     <div class="flex flex-wrap gap-4"> | ||||
|         <div> | ||||
|             <input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} /> | ||||
|             <label for="author">Autor</label> | ||||
|   | ||||
| @@ -4,34 +4,41 @@ | ||||
| <form id="edit-area"> | ||||
|     <div class="flex flex-col gap-y-1"> | ||||
|         <label for="article-title">Titel</label> | ||||
|         <input name="article-title" type="text" value="{{.Title}}" /> | ||||
|         <input name="article-title" type="text" value="{{.Article.Title}}" /> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex flex-col gap-y-1"> | ||||
|         <label for="article-description">Beschreibung</label> | ||||
|         <textarea name="article-description">{{.Description}}</textarea> | ||||
|         <textarea name="article-description">{{.Article.Description}}</textarea> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex flex-col gap-y-1"> | ||||
|         <label for="easyMDE">Artikel</label> | ||||
|         <textarea id="easyMDE">{{.Content}}</textarea> | ||||
|         <input id="article-content" name="article-content" type="hidden" /> | ||||
|         <input id="article-content" name="article-content" type="hidden" value="{{.Content}}" /> | ||||
|     </div> | ||||
|  | ||||
|     <div> | ||||
|         <span>Tags</span> | ||||
|         <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}} | ||||
|             <div> | ||||
|                 <input id="{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" /> | ||||
|                 <label for="{{.Name}}">{{.Name}}</label> | ||||
|                 <input id="tag-{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" {{if index $.Selected | ||||
|                     .ID}}checked{{end}} /> | ||||
|                 <label for="tag-{{.Name}}">{{.Name}}</label> | ||||
|             </div> | ||||
|             {{end}} | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="btn-area"> | ||||
|         <input class="action-btn" type="submit" value="Senden" hx-post="/article/submit" hx-target="#page-content" /> | ||||
|         <input class="action-btn" type="submit" value="Senden" hx-post="/article/{{.Action}}" | ||||
|             hx-target="#page-content" /> | ||||
|         <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button> | ||||
|     </div> | ||||
| </form> | ||||
|   | ||||
| @@ -25,7 +25,7 @@ | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="btn-area"> | ||||
|     <div class="btn-area-1"> | ||||
|         <input class="action-btn" type="submit" value="Anlegen" hx-post="/user/add-first" hx-target="#page-content" /> | ||||
|     </div> | ||||
| </form> | ||||
|   | ||||
| @@ -4,44 +4,46 @@ | ||||
|  | ||||
|     {{if lt . 4}} | ||||
|     <div class="mb-3"> | ||||
|         <h2>Autor</h2> | ||||
|         <div class="grid grid-cols-2 gap-x-4 gap-y-2"> | ||||
|         <h2>Artikel</h2> | ||||
|         <div class="grid grid-cols-1 md: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/all-rejected" hx-target="#page-content">Abgelehnte Artikel</button> | ||||
|             <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 | ||||
|             </button> | ||||
|             <button class="btn" hx-get="/tag/create" hx-target="#page-content">Neuer Tag</button> | ||||
|             <button class="btn" hx-get="/article/all-rejected" hx-target="#page-content">Artikel bearbeiten</button> | ||||
|             {{if lt . 3}}<button class="btn" hx-get="/article/all-unpublished-unrejected-and-published-rejected" | ||||
|                 hx-target="#page-content">Artikel veröffentlichen</button>{{end}} | ||||
|             {{if lt . 2}}<button class="btn" hx-get="/article/all-published/delete" hx-target="#page-content">Artikel | ||||
|                 löschen</button>{{end}} | ||||
|             {{if lt . 2}}<button class="btn" hx-get="/article/all-published/review-edit" | ||||
|                 hx-target="#page-content">Artikel bearbeiten lassen</button>{{end}} | ||||
|             {{if lt . 3}}<button class="btn" hx-get="/tag/create" hx-target="#page-content">Neuer Tag</button>{{end}} | ||||
|         </div> | ||||
|     </div> | ||||
|     {{end}} | ||||
|  | ||||
|     {{if lt . 2}} | ||||
|     <div class="mb-3"> | ||||
|         <h2>Herausgeber</h2> | ||||
|         <div class="grid grid-cols-2 gap-4"> | ||||
|         <h2>Ausgabe</h2> | ||||
|         <div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2"> | ||||
|             <button class="btn" hx-get="/issue/this" hx-target="#page-content">Diese Ausgabe</button> | ||||
|             <button class="btn" hx-get="/article/all-published" hx-target="#page-content">Artikel löschen</button> | ||||
|             <form class="flex" hx-encoding="multipart/form-data"> | ||||
|                 <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> | ||||
|         {{end}} | ||||
|     </div> | ||||
|     {{end}} | ||||
|  | ||||
|     {{if eq . 0}} | ||||
|     {{if lt . 4}} | ||||
|     <div class="mb-3"> | ||||
|         <h2>Administrator</h2> | ||||
|         <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/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> | ||||
|         <h2>Benutzer</h2> | ||||
|         <div class="grid grid-cols-1 md: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> | ||||
|             {{if eq . 0}}<button class="btn" hx-get="/user/create" hx-target="#page-content">Benutzer | ||||
|                 hinzufügen</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> | ||||
|     {{end}} | ||||
|   | ||||
| @@ -5,13 +5,26 @@ | ||||
|     <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 rel="stylesheet" href="https://unpkg.com/easymde/dist/easymde.min.css"> | ||||
| </head> | ||||
|  | ||||
| <body class="flex flex-col justify-between min-h-screen bg-slate-50"> | ||||
| <body | ||||
|     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"> | ||||
|         <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> | ||||
|  | ||||
|     <main class="mx-4"> | ||||
| @@ -20,17 +33,39 @@ | ||||
|         </div> | ||||
|     </main> | ||||
|  | ||||
|     <footer class="my-8"> | ||||
|         <p class="text-center text-gray-500 dark:text-gray-400"> | ||||
|             © 2024 Jason Streifling. Alle Rechte vorbehalten. | ||||
|         </p> | ||||
|         <p class="text-center text-gray-500 dark:text-gray-400"> | ||||
|             <strong>Hinweis:</strong> Diese Software befindet sich noch in der Entwicklung und kann Fehler enthalten. | ||||
|         </p> | ||||
|     <footer class="text-center text-gray-500 my-8"> | ||||
|         <p>© 2024 Jason Streifling. Alle Rechte vorbehalten.</p> | ||||
|         <p>v0.11.1 - <strong>Alpha: Drastische Änderungen und Fehler vorbehalten.</strong></p> | ||||
|     </footer> | ||||
|  | ||||
|     <script src="https://unpkg.com/htmx.org@2.0.1"></script> | ||||
|     <script src="https://unpkg.com/htmx.org@2.0.2"></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> | ||||
|  | ||||
| </html> | ||||
|   | ||||
| @@ -2,11 +2,13 @@ | ||||
| <h2>Anmeldung</h2> | ||||
|  | ||||
| <form> | ||||
|     <div class="btn-area"> | ||||
|     <div class="grid grid-cols-1 md:grid-cols-2 gap-4 my-1"> | ||||
|         <input class="w-full" name="username" placeholder="Benutzername" type="text" /> | ||||
|         <input class="w-full" name="password" placeholder="Passwort" type="password" /> | ||||
|     </div> | ||||
|  | ||||
|     <input class="action-btn" type="submit" value="Anmelden" hx-post="/login" hx-target="#page-content" /> | ||||
|     <div class="mt-2"> | ||||
|         <input class="action-btn" type="submit" value="Anmelden" hx-post="/login" hx-target="#page-content" /> | ||||
|     </div> | ||||
| </form> | ||||
| {{end}} | ||||
|   | ||||
| @@ -2,12 +2,13 @@ | ||||
| <h2>Artikel löschen</h2> | ||||
|  | ||||
| <div class="flex flex-col gap-4"> | ||||
|     {{range .}} | ||||
|     <button class="btn" hx-get="/article/review-deletion/{{.ID}}" hx-target="#page-content"> | ||||
|     {{range .Articles}} | ||||
|     <button class="btn" hx-get="/article/{{$.Action}}/{{.ID}}" hx-target="#page-content"> | ||||
|         <h1 class="font-bold text-2xl">{{.Title}}</h1> | ||||
|         <p>{{.Description}}</p> | ||||
|     </button> | ||||
|     {{end}} | ||||
|  | ||||
|     <button class="action-btn" hx-get="/hub" hx-target="#page-content">Zurück</button> | ||||
| </div> | ||||
| {{end}} | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| {{define "page-content"}} | ||||
| <h2>Abgelehnte Artikel</h2> | ||||
| <h2>Artikel bearbeiten</h2> | ||||
|  | ||||
| <div class="flex flex-col gap-4"> | ||||
|     {{range .RejectedArticles}} | ||||
|   | ||||
							
								
								
									
										50
									
								
								web/templates/review-article.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								web/templates/review-article.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| {{define "page-content"}} | ||||
| <h2>{{.ActionTitle}}</h2> | ||||
|  | ||||
| <div> | ||||
|     <span>Titel</span> | ||||
|     <div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full"> | ||||
|         {{.Article.Title}} | ||||
|     </div> | ||||
|  | ||||
|     <span>Beschreibung</span> | ||||
|     <div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full"> | ||||
|         {{.Article.Description}} | ||||
|     </div> | ||||
|  | ||||
|     <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="prose text-slate-900 dark:text-slate-100"> | ||||
|             {{.HTMLContent}} | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <span>Tags</span> | ||||
|     <div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full"> | ||||
|         {{if .Article.IsInIssue}} | ||||
|         <span>Orient Express</span> | ||||
|         <br> | ||||
|         {{end}} | ||||
|         {{range .Tags}} | ||||
|         <span>{{.Name}}</span> | ||||
|         <br> | ||||
|         {{end}} | ||||
|     </div> | ||||
|  | ||||
|     {{if eq .Action "publish"}} | ||||
|     <div class="btn-area-3"> | ||||
|         <input class="action-btn" type="submit" value="{{.ActionButton}}" hx-get="/article/{{.Action}}/{{.Article.ID}}" | ||||
|             hx-target="#page-content" /> | ||||
|         <input class="btn" type="submit" value="Ablehnen" hx-get="/article/reject/{{.Article.ID}}" | ||||
|             hx-target="#page-content" /> | ||||
|         <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button> | ||||
|     </div> | ||||
|     {{else}} | ||||
|     <div class="btn-area"> | ||||
|         <input class="action-btn" type="submit" value="{{.ActionButton}}" hx-get="/article/{{.Action}}/{{.Article.ID}}" | ||||
|             hx-target="#page-content" /> | ||||
|         <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button> | ||||
|     </div> | ||||
|     {{end}} | ||||
| </div> | ||||
| {{end}} | ||||
| @@ -1,73 +0,0 @@ | ||||
| {{define "page-content"}} | ||||
| <h2>Editor</h2> | ||||
|  | ||||
| <form> | ||||
|     <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 gap-y-1"> | ||||
|         <label for="article-description">Beschreibung</label> | ||||
|         <textarea name="article-description">{{.Article.Description}}</textarea> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex flex-col gap-y-1"> | ||||
|         <label for="easyMDE">Artikel</label> | ||||
|         <textarea id="easyMDE">{{.Content}}</textarea> | ||||
|         <input id="article-content" name="article-content" type="hidden" /> | ||||
|     </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 | ||||
|                     .ID}}checked{{end}} /> | ||||
|                 <label for="tag-{{.Name}}">{{.Name}}</label> | ||||
|             </div> | ||||
|             {{end}} | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="btn-area"> | ||||
|         <input class="action-btn" type="submit" value="Senden" hx-post="/article/resubmit/{{.Article.ID}}" | ||||
|             hx-target="#page-content" /> | ||||
|         <button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button> | ||||
|     </div> | ||||
| </form> | ||||
|  | ||||
| <script> | ||||
|     document.getElementById('article-content').value = easyMDE.value(); | ||||
|  | ||||
|     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('article-content').value = easyMDE.value(); | ||||
|     }); | ||||
| </script> | ||||
| {{end}} | ||||
| @@ -1,36 +0,0 @@ | ||||
| {{define "page-content"}} | ||||
| <h2>Artikel löschen</h2> | ||||
|  | ||||
| <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}} | ||||
|     </div> | ||||
|  | ||||
|     <div class="btn-area"> | ||||
|         <input class="action-btn" type="submit" value="Löschen" hx-get="/article/delete/{{.ID}}" | ||||
|             hx-target="#page-content" /> | ||||
|         <button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button> | ||||
|     </div> | ||||
| </div> | ||||
| {{end}} | ||||
| @@ -1,37 +0,0 @@ | ||||
| {{define "page-content"}} | ||||
| <h2>Artikel veröffentlichen</h2> | ||||
|  | ||||
| <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}} | ||||
|     </div> | ||||
|  | ||||
|     <div class="btn-area"> | ||||
|         <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/{{.ID}}" hx-target="#page-content" /> | ||||
|         <button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button> | ||||
|     </div> | ||||
| </div> | ||||
| {{end}} | ||||
| @@ -1,5 +1,5 @@ | ||||
| {{define "page-content"}} | ||||
| <h2>Unveröffentlichte Artikel</h2> | ||||
| <h2>Artikel veröffentlichen</h2> | ||||
|  | ||||
| <div class="flex flex-col gap-4"> | ||||
|     {{range .}} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user