diff options
| author | Samuel Johnson <[email protected]> | 2025-12-11 15:09:30 -0500 |
|---|---|---|
| committer | Samuel Johnson <[email protected]> | 2025-12-11 15:09:30 -0500 |
| commit | 61ba5dcc29a16bb3727209a76ee75fffff037dee (patch) | |
| tree | 4505ebfe8aadeae024e2f36fb948f9e79841a0f1 | |
| parent | 154e2a5417bca37cd7474f4e16fd8901844e473f (diff) | |
Add commenting system
| -rw-r--r-- | cmd/web/handlers/blog.go | 131 | ||||
| -rw-r--r-- | cmd/web/handlers/routes.go | 3 | ||||
| -rw-r--r-- | internal/dbmigrations.go | 8 | ||||
| -rw-r--r-- | internal/models/comment.go | 12 | ||||
| -rw-r--r-- | static/app.css | 4 | ||||
| -rw-r--r-- | ui/html/pages/post.tmpl.html | 36 |
6 files changed, 194 insertions, 0 deletions
diff --git a/cmd/web/handlers/blog.go b/cmd/web/handlers/blog.go index 4c81669..472b270 100644 --- a/cmd/web/handlers/blog.go +++ b/cmd/web/handlers/blog.go @@ -3,6 +3,7 @@ package handlers import ( "bytes" "database/sql" + "html" "html/template" "io" "log" @@ -21,16 +22,113 @@ type blogContext struct { Post models.Post Rows []models.Post + Comments []models.Comment Name string IsAuth bool PagePopulated bool Offset int } +func (ctx *blogContext) comment(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + w.WriteHeader(http.StatusMethodNotAllowed) + w.Write([]byte("Method Not Allowed")) + return + } + + postId := r.URL.Query().Get("id") + + err := r.ParseForm() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Failed to retrieve form data: %v\n", err) + return + } + + name := r.PostForm.Get("name") + content := html.EscapeString(r.PostForm.Get("comment")) + + insertStmt, err := ctx.db.Prepare("INSERT INTO comments (verified, name, post_id, content) VALUES (FALSE, $1, $2, $3);") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not prepare insert statement to DB: %v\n", err) + return + } + defer insertStmt.Close() + + _, err = insertStmt.Exec(name, postId, content) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not execute comment into DB: %v\n", err) + return + } + + http.Redirect(w, r, r.Header.Get("Referer"), http.StatusFound) + return +} + +func (ctx *blogContext) verifyComment(w http.ResponseWriter, r *http.Request) { + commentId := r.URL.Query().Get("id") + + updateStmt, err := ctx.db.Prepare("UPDATE comments SET verified = TRUE WHERE id = $1;") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not prepare update statement to DB: %v\n", err) + return + } + defer updateStmt.Close() + + _, err = updateStmt.Exec(commentId) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not execute to DB: %v\n", err) + return + } + + http.Redirect(w, r, r.Header.Get("Referer"), http.StatusFound) + return +} + +func (ctx *blogContext) deleteComment(w http.ResponseWriter, r *http.Request) { + commentId := r.URL.Query().Get("id") + + updateStmt, err := ctx.db.Prepare("DELETE FROM comments WHERE id = $1;") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not prepare update statement to DB: %v\n", err) + return + } + defer updateStmt.Close() + + _, err = updateStmt.Exec(commentId) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not execute to DB: %v\n", err) + return + } + + http.Redirect(w, r, r.Header.Get("Referer"), http.StatusFound) + return +} + func (ctx *blogContext) viewPost(w http.ResponseWriter, r *http.Request) { ctx.Rows = []models.Post{} + ctx.Comments = []models.Comment{} + ctx.IsAuth = false postId := r.URL.Query().Get("id") + _, err := r.Cookie("paterissa_session_token") + if err == nil { + ctx.IsAuth = true + } + 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) @@ -60,6 +158,39 @@ func (ctx *blogContext) viewPost(w http.ResponseWriter, r *http.Request) { p.FormattedTime = p.Time.Format(time.ANSIC) ctx.Post = p + commentStmt, err := ctx.db.Prepare("SELECT id, verified, time, name, content FROM comments WHERE post_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 + } + defer commentStmt.Close() + + commentRows, err := commentStmt.Query(p.Id) + 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 + } + defer commentRows.Close() + + for commentRows.Next() { + var c models.Comment + + if err = commentRows.Scan(&c.Id, &c.IsVerified, &c.Time, &c.Name, &c.Content); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not load row: %v\n", err) + return + } + + c.FormattedTime = c.Time.Format(time.ANSIC) + + ctx.Comments = append(ctx.Comments, c) + } + files := []string{ "ui/html/base.tmpl.html", "ui/html/music_player.tmpl.html", diff --git a/cmd/web/handlers/routes.go b/cmd/web/handlers/routes.go index 1c14705..f6d8dc3 100644 --- a/cmd/web/handlers/routes.go +++ b/cmd/web/handlers/routes.go @@ -47,6 +47,9 @@ func RegisterEndpoints(app types.Application, db *sql.DB) *http.ServeMux { blogRouter.HandleFunc("/", auth.CheckAndInvalidate(blog.index)) blogRouter.HandleFunc("/post/new", auth.Resolve(blog.post)) blogRouter.HandleFunc("/post", blog.viewPost) + blogRouter.HandleFunc("/comments/verify", auth.Resolve(blog.verifyComment)) + blogRouter.HandleFunc("/comments/delete", auth.Resolve(blog.deleteComment)) + blogRouter.HandleFunc("/comments/post", blog.comment) blogRouter.HandleFunc("/login", login.handle) blogRouter.HandleFunc("/logout", auth.Resolve(login.logout)) diff --git a/internal/dbmigrations.go b/internal/dbmigrations.go index fcbaaaf..41180c7 100644 --- a/internal/dbmigrations.go +++ b/internal/dbmigrations.go @@ -56,4 +56,12 @@ func Migrate(db *sql.DB, webmaster string, passOne string, passTwo string) { fmt.Fprintf(os.Stderr, "Unable to create cookies table: %v\n", err) } } + + _, table_check = db.Query("SELECT * FROM comments;") + if table_check != nil { + _, err := db.Exec("CREATE TABLE comments (id SERIAL PRIMARY KEY, verified BOOLEAN NOT NULL, time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, name TEXT NOT NULL, post_id INTEGER REFERENCES posts(id), content TEXT NOT NULL);") + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to create comments table: %v\n", err) + } + } } diff --git a/internal/models/comment.go b/internal/models/comment.go new file mode 100644 index 0000000..df27ca3 --- /dev/null +++ b/internal/models/comment.go @@ -0,0 +1,12 @@ +package models + +import "time" + +type Comment struct { + Id int + IsVerified bool + Time time.Time + FormattedTime string + Name string + Content string +} diff --git a/static/app.css b/static/app.css index 2ed671f..4e26607 100644 --- a/static/app.css +++ b/static/app.css @@ -22,6 +22,10 @@ pre { word-wrap: inherit; } +.w100 { + width: 100%; +} + .rss { margin-left: 1.5em; } diff --git a/ui/html/pages/post.tmpl.html b/ui/html/pages/post.tmpl.html index eefbd82..d3b78f0 100644 --- a/ui/html/pages/post.tmpl.html +++ b/ui/html/pages/post.tmpl.html @@ -5,4 +5,40 @@ <div class="card"> {{.Post.Content}} </div> + <div class="card"> + <h3>Leave Comment</h3> + <form action="/comments/post?id={{.Post.Id}}" method="POST"> + <label for="comment"></label> + <textarea name="comment" id="comment" rows="5" class="w100" required></textarea><br><br> + <div class="flex-between"> + <label for="name">Name:</label> + <input type="text" name="name" id="name" required> + <input type="submit" value="Post"> + </div> + </form> + </div> + {{range .Comments}} + {{if .IsVerified}} + <div class="card"> + <h3>{{.Name}} - {{.FormattedTime}}</h3> + {{if $.IsAuth}} + <div class="nav_tag right"> + <a href="/comments/delete?id={{.Id}}" class="nav_tag right">Delete</a> + </div> + {{end}} + <p><pre>{{.Content}}</pre></p> + </div> + {{else}} + {{if $.IsAuth}} + <div class="card"> + <h3>{{.Name}} - {{.FormattedTime}}</h3> + <div class="nav_tag right"> + <a href="/comments/verify?id={{.Id}}" class="nav_tag right">Verify</a> + <a href="/comments/delete?id={{.Id}}" class="nav_tag right">Delete</a> + </div> + <p><pre>{{.Content}}</pre></p> + </div> + {{end}} + {{end}} + {{end}} {{end}} |
