aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSamuel Johnson <[email protected]>2025-11-25 19:47:20 -0500
committerSamuel Johnson <[email protected]>2025-11-25 19:47:20 -0500
commit3c237fc659c2829042407697ca7aa3e1442a5719 (patch)
tree6557b2faa27eb9880ef96c8755bed3f8a461d2ae
parent368a462bc744d8e9084eacfaddeb9afcaf7f7133 (diff)
Add post editing interface
-rw-r--r--cmd/parser/main.go55
-rw-r--r--cmd/web/handlers/blog.go206
-rw-r--r--cmd/web/handlers/login.go26
-rw-r--r--cmd/web/handlers/routes.go4
-rw-r--r--cmd/web/middleware/auth.go67
-rw-r--r--internal/dbmigrations.go2
-rw-r--r--internal/models/post.go9
-rw-r--r--static/app.css87
-rw-r--r--static/mde.js29
-rw-r--r--ui/html/base.tmpl.html2
-rw-r--r--ui/html/pages/index.tmpl.html27
-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.html8
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}}