diff options
| author | Samuel Johnson <[email protected]> | 2025-11-25 19:47:20 -0500 |
|---|---|---|
| committer | Samuel Johnson <[email protected]> | 2025-11-25 19:47:20 -0500 |
| commit | 3c237fc659c2829042407697ca7aa3e1442a5719 (patch) | |
| tree | 6557b2faa27eb9880ef96c8755bed3f8a461d2ae | |
| parent | 368a462bc744d8e9084eacfaddeb9afcaf7f7133 (diff) | |
Add post editing interface
| -rw-r--r-- | cmd/parser/main.go | 55 | ||||
| -rw-r--r-- | cmd/web/handlers/blog.go | 206 | ||||
| -rw-r--r-- | cmd/web/handlers/login.go | 26 | ||||
| -rw-r--r-- | cmd/web/handlers/routes.go | 4 | ||||
| -rw-r--r-- | cmd/web/middleware/auth.go | 67 | ||||
| -rw-r--r-- | internal/dbmigrations.go | 2 | ||||
| -rw-r--r-- | internal/models/post.go | 9 | ||||
| -rw-r--r-- | static/app.css | 87 | ||||
| -rw-r--r-- | static/mde.js | 29 | ||||
| -rw-r--r-- | ui/html/base.tmpl.html | 2 | ||||
| -rw-r--r-- | ui/html/pages/index.tmpl.html | 27 | ||||
| -rw-r--r-- | ui/html/pages/login.tmpl.html (renamed from ui/html/login.tmpl.html) | 5 | ||||
| -rw-r--r-- | ui/html/pages/post.tmpl.html | 8 |
13 files changed, 457 insertions, 70 deletions
diff --git a/cmd/parser/main.go b/cmd/parser/main.go index 3d0b132..44fca42 100644 --- a/cmd/parser/main.go +++ b/cmd/parser/main.go @@ -32,56 +32,79 @@ func writeErr(w http.ResponseWriter, errcode int, msg string) { func mdServe(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { - writeErr(w, 405, "Method Not Allowed") + writeErr(w, http.StatusMethodNotAllowed, "Method Not Allowed") return } - + err := r.ParseMultipartForm(4 << 20) if err != nil { - writeErr(w, 500, "Failed to retrieve form data") + writeErr(w, http.StatusUnprocessableEntity, fmt.Sprintf("Failed to retrieve form data: %v", err)) return } - var buf bytes.Buffer + var longBuf bytes.Buffer + var shortBuf bytes.Buffer + md := r.Form.Get("raw") + userId := r.Form.Get("user_id") - md := r.PostForm.Get("raw") + shouldCreateShort := len(md) > 255 - err = mdParser.Convert([]byte(md), &buf) + err = mdParser.Convert([]byte(md), &longBuf) if err != nil { - writeErr(w, 500, fmt.Sprintf("Failed to compile markdown into html: %v", err)) + writeErr(w, http.StatusUnprocessableEntity, fmt.Sprintf("Failed to compile markdown into html: %v", err)) return } + + var shortMd string + if shouldCreateShort { + shortMd = md[0:249] + err = mdParser.Convert([]byte(shortMd), &shortBuf) + + if err != nil { + writeErr(w, http.StatusUnprocessableEntity, fmt.Sprintf("Failed to compile markdown into html: %v", err)) + return + } + } ctx, cancel := context.WithTimeout(r.Context(), 5 * time.Second) defer cancel() tx, err := db.BeginTx(ctx, nil) if err != nil { - writeErr(w, 500, fmt.Sprintf("Failed to initialize transaction: %v", err)) + writeErr(w, http.StatusInternalServerError, fmt.Sprintf("Failed to initialize transaction: %v", err)) return } defer tx.Rollback() - stmt, err := tx.PrepareContext(ctx, "INSERT INTO posts (name, content) VALUES ($1, $2);") + stmt, err := tx.PrepareContext(ctx, "INSERT INTO posts (user_id, brief, content) VALUES ($1, $2, $3);") if err != nil { - writeErr(w, 500, fmt.Sprintf("Failed to prepare DB statement: %v", err)) + writeErr(w, http.StatusInternalServerError, fmt.Sprintf("Failed to prepare DB statement: %v", err)) return } defer stmt.Close() - _, err = stmt.ExecContext(ctx, os.Getenv("webmaster"), buf.Bytes()) + userIdInt, err := strconv.ParseUint(userId, 10, 64) if err != nil { - writeErr(w, 500, fmt.Sprintf("Failed to execute statement: %v", err)) + writeErr(w, http.StatusUnprocessableEntity, fmt.Sprintf("Failed to parse userId: %v\n", err)) return } - err = tx.Commit() + if shouldCreateShort { + _, err = stmt.ExecContext(ctx, userIdInt, shortBuf.Bytes(), longBuf.Bytes()) + } else { + _, err = stmt.ExecContext(ctx, userIdInt, longBuf.Bytes(), longBuf.Bytes()) + } + if err != nil { - writeErr(w, 500, fmt.Sprintf("Failed to commit DB transaction: %v", err)) + writeErr(w, http.StatusInternalServerError, fmt.Sprintf("Failed to execute statement: %v", err)) + return } - w.WriteHeader(200) - w.Write([]byte("Successfully parsed and stored markdown")) + err = tx.Commit() + if err != nil { + writeErr(w, http.StatusInternalServerError, fmt.Sprintf("Failed to commit DB transaction: %v", err)) + return + } } func main() { diff --git a/cmd/web/handlers/blog.go b/cmd/web/handlers/blog.go index 573e52f..8d9a811 100644 --- a/cmd/web/handlers/blog.go +++ b/cmd/web/handlers/blog.go @@ -1,10 +1,16 @@ package handlers import ( + "bytes" "database/sql" "html/template" + "io" "log" + "mime/multipart" "net/http" + "os" + "strconv" + "time" "paterissa.net/mblog/internal/models" ) @@ -13,9 +19,167 @@ type blogContext struct { err *log.Logger db *sql.DB + Post models.Post Rows []models.Post Name string IsAuth bool + Offset int +} + +func (ctx *blogContext) viewPost(w http.ResponseWriter, r *http.Request) { + ctx.Rows = []models.Post{} + postId := r.URL.Query().Get("id") + + stmt, err := ctx.db.Prepare("SELECT p.id, u.name, p.time, p.brief, p.content FROM posts p INNER JOIN logins u ON p.user_id = u.id WHERE p.id = $1;") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not prepare statement for DB: %v\n", err) + return + } + + row := stmt.QueryRow(postId) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not load post from DB: %v\n", err) + return + } + + var p models.Post + err = row.Scan(&p.Id, &p.Name, &p.Time, &p.Brief, &p.Content) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not load post from DB: %v\n", err) + return + } + + p.FormattedTime = p.Time.Format(time.ANSIC) + ctx.Post = p + + files := []string{ + "ui/html/base.tmpl.html", + "ui/html/music_player.tmpl.html", + "ui/html/pages/post.tmpl.html", + } + + compiled, err := template.ParseFiles(files...) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not parse template: %v\n", err) + return + } + + err = compiled.ExecuteTemplate(w, "base", ctx) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not execute template: %v\n", err) + return + } +} + +func (ctx *blogContext) post(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + w.WriteHeader(http.StatusMethodNotAllowed) + w.Write([]byte("Method Not Allowed")) + return + } + + cookie, err := r.Cookie("paterissa_session_token") + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("Unauthorized")) + return + } + + stmt, err := ctx.db.Prepare("SELECT * FROM cookies WHERE content = $1;") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not load cookies from DB: %v\n", err) + return + } + + var id uint64 + var content string + var userId uint64 + var expiration time.Time + + row := stmt.QueryRow(cookie.Value) + err = row.Scan(&id, &content, &userId, &expiration) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + return; + } + + err = r.ParseMultipartForm(4 << 20) + if err != nil { + w.WriteHeader(http.StatusUnprocessableEntity) + w.Write([]byte("Failed to retrieve form data")) + ctx.err.Printf("Could not parse request form: %v\n", err) + return + } + + var buffer bytes.Buffer + boundary := "----internal-parser-req" + writer := multipart.NewWriter(&buffer) + writer.SetBoundary(boundary) + writer.WriteField("user_id", strconv.Itoa(int(userId))) + + part, err := writer.CreateFormField("raw") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not create form field: %v\n", err) + return + } + + _, err = part.Write([]byte(r.Form.Get("raw"))) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not create form field: %v\n", err) + return + } + + writer.Close() + + proxyReq, err := http.NewRequest(r.Method, "http://127.0.0.1:" + os.Getenv("parser_port"), bytes.NewReader(buffer.Bytes())) + proxyReq.Header = make(http.Header) + for key, val := range r.Header { + if key != "Content-Length" { + proxyReq.Header[key] = val; + } + } + + proxyReq.Header.Set("Content-Type", writer.FormDataContentType()) + + resp, err := http.DefaultClient.Do(proxyReq) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Fail response from parser: %v\n", err) + return + } + if resp.StatusCode != http.StatusOK { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + + body, err := io.ReadAll(resp.Body) + if err == nil { + ctx.err.Printf("Fail response from parser: %s\n", body) + } + + return + } + + defer resp.Body.Close() + + return } func (ctx *blogContext) index(w http.ResponseWriter, r *http.Request) { @@ -32,15 +196,13 @@ func (ctx *blogContext) index(w http.ResponseWriter, r *http.Request) { ctx.IsAuth = true; } - offset := r.URL.Query().Get("offset") - if offset == "" { - offset = "20" - } + ctx.Offset, _ = strconv.Atoi(r.URL.Query().Get("offset")) - rows, err := ctx.db.Query("SELECT p.id, u.name, p.time, p.content FROM posts p INNER JOIN logins u ON p.user_id = u.id WHERE p.id < " + offset + " ORDER BY p.id DESC LIMIT 20;") + rows, err := ctx.db.Query("SELECT p.id, u.name, p.time, p.brief FROM posts p INNER JOIN logins u ON p.user_id = u.id WHERE p.id > " + strconv.Itoa(ctx.Offset) + " ORDER BY p.id DESC LIMIT 20;") if err != nil { - ctx.err.Print(err.Error()) - http.Error(w, "Internal Server Error", 500) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not load posts from DB: %v\n", err) return } defer rows.Close() @@ -48,12 +210,15 @@ func (ctx *blogContext) index(w http.ResponseWriter, r *http.Request) { for rows.Next() { var p models.Post - if err = rows.Scan(&p.Id, &p.Name, &p.Time, &p.Content); err != nil { - ctx.err.Print(err.Error()) - http.Error(w, "Internal Server Error", 500) + if err = rows.Scan(&p.Id, &p.Name, &p.Time, &p.Brief); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not load row: %v\n", err) return } + p.FormattedTime = p.Time.Format(time.ANSIC) + ctx.Rows = append(ctx.Rows, p) } @@ -63,17 +228,28 @@ func (ctx *blogContext) index(w http.ResponseWriter, r *http.Request) { "ui/html/pages/index.tmpl.html", } - compiled, err := template.ParseFiles(files...) + funcMap := template.FuncMap{ + "add": func(a int, b int) int { + return a + b; + }, + "sub": func(a int, b int) int { + return a - b; + }, + } + + compiled, err := template.New("blog").Funcs(funcMap).ParseFiles(files...) if err != nil { - ctx.err.Print(err.Error()) - http.Error(w, "Internal Server Error", 500) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not parse template: %v\n", err) return } err = compiled.ExecuteTemplate(w, "base", ctx) if err != nil { - ctx.err.Print(err.Error()) - http.Error(w, "Internal Server Error", 500) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not execute template: %v\n", err) return } } diff --git a/cmd/web/handlers/login.go b/cmd/web/handlers/login.go index e867e07..87684ba 100644 --- a/cmd/web/handlers/login.go +++ b/cmd/web/handlers/login.go @@ -21,13 +21,13 @@ type loginContext struct { func (ctx *loginContext) index(w http.ResponseWriter, r *http.Request) { files := []string{ "ui/html/base.tmpl.html", - "ui/html/login.tmpl.html", "ui/html/music_player.tmpl.html", + "ui/html/pages/login.tmpl.html", } compiled, err := template.ParseFiles(files...) if err != nil { - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("Internal Server Error")) ctx.err.Printf("Failed to parse templates: %v\n", err) return @@ -35,7 +35,7 @@ func (ctx *loginContext) index(w http.ResponseWriter, r *http.Request) { err = compiled.ExecuteTemplate(w, "base", ctx) if err != nil { - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("Internal Server Error")) ctx.err.Printf("Failed to parse templates: %v\n", err) return @@ -46,14 +46,14 @@ func (ctx *loginContext) index(w http.ResponseWriter, r *http.Request) { func (ctx *loginContext) login(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { - w.WriteHeader(405) + w.WriteHeader(http.StatusMethodNotAllowed) w.Write([]byte("Method Not Allowed")) return } err := r.ParseForm() if err != nil { - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("Internal Error")) ctx.err.Printf("Failed to retrieve form data: %v\n", err) return @@ -65,7 +65,7 @@ func (ctx *loginContext) login(w http.ResponseWriter, r *http.Request) { stmt, err := ctx.db.Prepare("SELECT * FROM logins WHERE name = $1;") if err != nil { - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("Internal Error")) ctx.err.Printf("Failed to retrieve form data: %v\n", err) return @@ -77,7 +77,7 @@ func (ctx *loginContext) login(w http.ResponseWriter, r *http.Request) { row := stmt.QueryRow(user) err = row.Scan(&u.Id, &u.Name, &u.Time, &u.PassOne, &u.PassTwo) if err != nil { - w.WriteHeader(401) + w.WriteHeader(http.StatusUnauthorized) w.Write([]byte("Unauthorized")) ctx.err.Printf("Failed to retrieve user info from DB: %v\n", err) return @@ -86,7 +86,7 @@ func (ctx *loginContext) login(w http.ResponseWriter, r *http.Request) { passOneErr := bcrypt.CompareHashAndPassword([]byte(u.PassOne), []byte(passOne)) passTwoErr := bcrypt.CompareHashAndPassword([]byte(u.PassTwo), []byte(passTwo)) if passOneErr != nil || passTwoErr != nil { - w.WriteHeader(401) + w.WriteHeader(http.StatusUnauthorized) w.Write([]byte("Failed to login - not authorized")) return } @@ -103,7 +103,7 @@ func (ctx *loginContext) login(w http.ResponseWriter, r *http.Request) { commit, err := ctx.db.Prepare("INSERT INTO cookies (content, user_id, expiration) VALUES ($1, $2, $3);") if err != nil { - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("Internal Error")) ctx.err.Printf("Failed to prepare DB statement: %v\n", err) return @@ -111,7 +111,7 @@ func (ctx *loginContext) login(w http.ResponseWriter, r *http.Request) { _, err = commit.Exec(cookie.Value, u.Id, cookie.Expires) if err != nil { - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("Internal Error")) ctx.err.Printf("Failed to prepare DB statement: %v\n", err) } @@ -124,14 +124,14 @@ func (ctx *loginContext) login(w http.ResponseWriter, r *http.Request) { func (ctx *loginContext) logout(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("paterissa_session_token") if err != nil { - w.WriteHeader(405) + w.WriteHeader(http.StatusUnauthorized) w.Write([]byte("Unauthorized")) return } stmt, err := ctx.db.Prepare("UPDATE cookies SET expiration = $1 WHERE content = $2;") if err != nil { - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("Internal Error")) ctx.err.Printf("Could not prepare DB statement: %v\n", err) return @@ -140,7 +140,7 @@ func (ctx *loginContext) logout(w http.ResponseWriter, r *http.Request) { _, err = stmt.Exec(time.Now(), cookie.Value) if err != nil { - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("Internal Error")) ctx.err.Printf("Could not execute DB statement: %v\n", err) return diff --git a/cmd/web/handlers/routes.go b/cmd/web/handlers/routes.go index 0196331..a4c9c09 100644 --- a/cmd/web/handlers/routes.go +++ b/cmd/web/handlers/routes.go @@ -34,7 +34,9 @@ func RegisterEndpoints(app types.Application, db *sql.DB) *http.ServeMux { } blogRouter := http.NewServeMux() - blogRouter.HandleFunc("/", blog.index) + blogRouter.HandleFunc("/", auth.CheckAndInvalidate(blog.index)) + blogRouter.HandleFunc("/post/new", auth.Resolve(blog.post)) + blogRouter.HandleFunc("/post", blog.viewPost) blogRouter.HandleFunc("/login", login.handle) blogRouter.HandleFunc("/logout", auth.Resolve(login.logout)) diff --git a/cmd/web/middleware/auth.go b/cmd/web/middleware/auth.go index b53980a..911eb44 100644 --- a/cmd/web/middleware/auth.go +++ b/cmd/web/middleware/auth.go @@ -12,12 +12,74 @@ type AuthMiddleware struct { Db *sql.DB } +func (auth *AuthMiddleware) CheckAndInvalidate(next http.HandlerFunc) http.HandlerFunc { + return http.HandlerFunc( + func (w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("paterissa_session_token") + if err != nil { + next.ServeHTTP(w, r) + return + } + + stmt, err := auth.Db.Prepare("SELECT * FROM cookies WHERE content = $1;") + if err != nil { + cookie = &http.Cookie{ + Name: "paterissa_session_token", + Value: "", + Path: "/", + MaxAge: -1, + HttpOnly: true, + } + http.SetCookie(w, cookie) + http.Redirect(w, r, "/", http.StatusFound) + return + } + defer stmt.Close() + + var id int + var content string + var userId int + var expiration time.Time + + row := stmt.QueryRow(cookie.Value) + err = row.Scan(&id, &content, &userId, &expiration) + if err != nil { + cookie = &http.Cookie{ + Name: "paterissa_session_token", + Value: "", + Path: "/", + MaxAge: -1, + HttpOnly: true, + } + http.SetCookie(w, cookie) + http.Redirect(w, r, "/", http.StatusFound) + return + } + + if time.Now().After(expiration) { + cookie = &http.Cookie{ + Name: "paterissa_session_token", + Value: "", + Path: "/", + MaxAge: -1, + HttpOnly: true, + } + http.SetCookie(w, cookie) + http.Redirect(w, r, "/", http.StatusFound) + return + } + + next.ServeHTTP(w, r) + return + }) +} + func (auth *AuthMiddleware) Resolve(next http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc( func (w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("paterissa_session_token") if err != nil { - w.WriteHeader(401) + w.WriteHeader(http.StatusUnauthorized) w.Write([]byte("Unauthorized")) return } @@ -35,6 +97,7 @@ func (auth *AuthMiddleware) Resolve(next http.HandlerFunc) http.HandlerFunc { w.Write([]byte("Unauthorized")) auth.Err.Printf("Could not retrieve cookie from DB: %v\n", err) + http.Redirect(w, r, "/", http.StatusUnauthorized) return } defer stmt.Close() @@ -58,6 +121,7 @@ func (auth *AuthMiddleware) Resolve(next http.HandlerFunc) http.HandlerFunc { w.Write([]byte("Unauthorized")) auth.Err.Printf("Could not retrieve cookie from DB: %v\n", err) + http.Redirect(w, r, "/", http.StatusUnauthorized) return } @@ -72,6 +136,7 @@ func (auth *AuthMiddleware) Resolve(next http.HandlerFunc) http.HandlerFunc { http.SetCookie(w, cookie) w.Write([]byte("Expired")) + http.Redirect(w, r, "/", http.StatusUnauthorized) return } diff --git a/internal/dbmigrations.go b/internal/dbmigrations.go index e6c11ff..d5f4836 100644 --- a/internal/dbmigrations.go +++ b/internal/dbmigrations.go @@ -33,7 +33,7 @@ func Migrate (db *sql.DB, webmaster string, passOne string, passTwo string) { _, table_check = db.Query("SELECT * FROM posts;") if table_check != nil { - _, err := db.Exec("CREATE TABLE posts (id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES logins(id), time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, content TEXT NOT NULL);") + _, err := db.Exec("CREATE TABLE posts (id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES logins(id), time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, brief TEXT NOT NULL, content TEXT NOT NULL);") if err != nil { fmt.Fprintf(os.Stderr, "Unable to create posts table: %v\n", err) os.Exit(1) diff --git a/internal/models/post.go b/internal/models/post.go index d7b006d..d7f1aaa 100644 --- a/internal/models/post.go +++ b/internal/models/post.go @@ -1,10 +1,15 @@ package models -import "html/template" +import ( + "html/template" + "time" +) type Post struct { Id int Name string - Time string + Time time.Time + FormattedTime string + Brief template.HTML Content template.HTML } diff --git a/static/app.css b/static/app.css index 7b2d083..2b86f97 100644 --- a/static/app.css +++ b/static/app.css @@ -1,5 +1,5 @@ header { - padding: 0rem 2rem 0rem 2rem; + padding: 0em 2em 0em 2em; display: flex; flex-direction: row; @@ -7,8 +7,8 @@ header { } header > h1 { - padding: 0.5rem; - font-size: 2rem; + padding: 0.25em; + font-size: 2em; } header > nav { @@ -17,9 +17,17 @@ header > nav { margin-left: 2em; } -.flex-between { +.flex_between { display: flex; flex-direction: row; + padding: 0.5rem; +} + +.flex_end { + display: flex; + flex-direction: row; + justify-content: flex-end; + padding: 0.5rem; } .spacer { @@ -89,22 +97,16 @@ a:hover::after { margin: 2em; } +.topline_apart { + justify-content: space-between; +} + .topline > p { margin-top: auto; margin-bottom: auto; margin-left: 2em; } -@media only screen and (max-width: 768px) { - header > h1 { - font-size: 1em; - } - - .topline > p { - display: none; - } -} - .card { border-top: 1px solid #fff; @@ -161,10 +163,40 @@ a:hover::after { margin: 2rem; } +.login_form .markdown_form { + margin-top: 2em; + margin-bottom: 2em; + padding: 0.5em; +} + .login_form { - margin-top: 2rem; + display: flex; + flex-direction: column; +} + +.login_form > input { + margin: auto; + margin-top: 0.5em; + margin-bottom: 2em; + width: 95%; +} + +.markdown_form { + margin-left: 2em; + margin-right: 2em; +} + +.bottom_elem { margin-bottom: 2rem; - padding: 0.5rem; +} + +.white { + background-color: white; + opacity: 1; +} + +.text_left { + text-align: left; } .vert_align { @@ -181,3 +213,26 @@ a:hover::after { width: 100%; height: 5%; } + +@media only screen and (max-width: 768px) { + header { + display: flex; + flex-direction: column; + } + + header > h1 { + font-size: 1.5em; + } + + header > nav { + margin-left: 0em; + } + + .topline > p { + display: none; + } + + .bottom_elem { + margin-bottom: 2.5em; + } +} diff --git a/static/mde.js b/static/mde.js new file mode 100644 index 0000000..a716ee4 --- /dev/null +++ b/static/mde.js @@ -0,0 +1,29 @@ +const form = document.getElementById('md_form'); +const textarea = document.getElementById('raw'); +textarea.value = ""; + +const editor = new EasyMDE({ + element: textarea, + autosave: { + enabled: true, + uniqueId: "paterissa-post-editor", + text: "Autosaved: ", + }, + placeholder: "Post...", + promptURLs: true, +}); + +form.addEventListener('submit', function(event) { + event.preventDefault(); + + const formData = new FormData(); + formData.set("raw", editor.value()); + + fetch('/post/new', { + method: 'POST', + body: formData + }).then(_ => { + window.location.reload(); + }); +}); + diff --git a/ui/html/base.tmpl.html b/ui/html/base.tmpl.html index 014868e..9ff8489 100644 --- a/ui/html/base.tmpl.html +++ b/ui/html/base.tmpl.html @@ -8,7 +8,7 @@ <meta property="og:type" content="object" /> <meta property="og:title" content="Paterissa - thehinterlander's blog" /> <meta property="og:url" content="https://paterissa.net" /> - <meta property="og:description" content="thehinterlander's repository of musings" /> + <meta property="og:description" content={{template "description" .}} /> <title>{{template "title" .}} - Paterissa</title> <link rel="stylesheet" href="/static/get?file=app.css"> </head> diff --git a/ui/html/pages/index.tmpl.html b/ui/html/pages/index.tmpl.html index 011f964..ee0a0e0 100644 --- a/ui/html/pages/index.tmpl.html +++ b/ui/html/pages/index.tmpl.html @@ -1,7 +1,8 @@ {{define "title"}}Blog{{end}} +{{define "description"}}"thehinterlander's repository of musings"{{end}} {{define "main"}} - <div class="flex-between"> + <div class="flex_between"> <div class="topline"> <h2>Blog</h2> <p>Home of {{.Name}}</p> @@ -14,18 +15,38 @@ {{end}} </div> {{if .IsAuth}} + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css"> + <script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script> + <script type="module" src="/static/get?file=mde.js"></script> <div class="card"> <div class="header"> <h4><b>New</b></h4> + <form id="md_form" class="markdown_form"> + <div class="white text_left"> + <label for="raw"></label> + <input type="textarea" id="raw" name="raw" style="white-space: pre-wrap;"> + </div> + <br> + <input type="submit" value="Post"> + </form> </div> </div> {{end}} {{range .Rows}} <div class="card"> <div class="header"> - <h4><b>{{.Name}} - {{.Time}}</b></h4> + <h4><b>{{.Name}} - {{.FormattedTime}}</b></h4> </div> - {{.Content}} + {{.Brief}} + <a href="/post?id={{.Id}}" class="nav_tag">View More</a> </div> {{end}} + <div class="flex_end bottom_elem"> + <div class="right"> + {{if ne .Offset 0}} + <a href="/?offset={{sub .Offset 10}}" class="nav_tag">Previous</a> + {{end}} + <a href="/?offset={{add .Offset 10}}">Next</a> + </div> + </div> {{end}} diff --git a/ui/html/login.tmpl.html b/ui/html/pages/login.tmpl.html index 6377dc0..6eb303a 100644 --- a/ui/html/login.tmpl.html +++ b/ui/html/pages/login.tmpl.html @@ -1,4 +1,5 @@ {{define "title"}}Login{{end}} +{{define "description"}}Login Portal{{end}} {{define "main"}} <div class="topline"> @@ -13,7 +14,9 @@ <label for="pass_two">Password:</label> <input type="text" id="pass_two" name="pass_two"> <br> - <input type="submit" value="Submit"> + <div class="right"> + <input type="submit" value="Submit"> + </div> </form> </div> {{end}} diff --git a/ui/html/pages/post.tmpl.html b/ui/html/pages/post.tmpl.html new file mode 100644 index 0000000..eefbd82 --- /dev/null +++ b/ui/html/pages/post.tmpl.html @@ -0,0 +1,8 @@ +{{define "title"}}{{.Post.Name}} - {{.Post.FormattedTime}}{{end}} +{{define "description"}}{{.Post.Brief}}{{end}} + +{{define "main"}} + <div class="card"> + {{.Post.Content}} + </div> +{{end}} |
