From 7537fa2e92f35a8973212d2678487b17d4e8fa86 Mon Sep 17 00:00:00 2001
From: yequari 
Date: Sun, 23 Mar 2025 12:57:49 -0700
Subject: [PATCH] implement comment deletion and debug mode
---
 cmd/web/handlers_guestbook.go       |  67 +++++-
 cmd/web/helpers.go                  |   5 +
 cmd/web/main.go                     |   3 +
 cmd/web/routes.go                   |   3 +-
 db/create-tables-sqlite.sql         |   4 +-
 internal/models/guestbook.go        |  21 +-
 internal/models/guestbookcomment.go |  70 ++++--
 internal/models/website.go          |  39 ++-
 ui/views/guestbooks.templ           |  37 ++-
 ui/views/guestbooks_templ.go        | 353 ++++++++++++++++------------
 ui/views/guestbooks_templ.txt       |   9 +-
 ui/views/home.templ                 |   3 +
 ui/views/home_templ.go              |   2 +-
 ui/views/home_templ.txt             |   2 +-
 ui/views/websites.templ             |   1 +
 ui/views/websites_templ.go          | 265 +++++++++++----------
 ui/views/websites_templ.txt         |   3 +-
 17 files changed, 539 insertions(+), 348 deletions(-)
diff --git a/cmd/web/handlers_guestbook.go b/cmd/web/handlers_guestbook.go
index d346b1b..88bbde2 100644
--- a/cmd/web/handlers_guestbook.go
+++ b/cmd/web/handlers_guestbook.go
@@ -4,6 +4,7 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
+	"time"
 
 	"git.32bit.cafe/32bitcafe/guestbook/internal/forms"
 	"git.32bit.cafe/32bitcafe/guestbook/internal/models"
@@ -129,7 +130,7 @@ func (app *application) getCommentQueue(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	comments, err := app.guestbookComments.GetQueue(website.Guestbook.ID)
+	comments, err := app.guestbookComments.GetUnpublished(website.Guestbook.ID)
 	if err != nil {
 		if errors.Is(err, models.ErrNoRecord) {
 			http.NotFound(w, r)
@@ -143,9 +144,71 @@ func (app *application) getCommentQueue(w http.ResponseWriter, r *http.Request)
 	views.GuestbookDashboardCommentsView("Message Queue", data, website, website.Guestbook, comments).Render(r.Context(), w)
 }
 
+func (app *application) getCommentTrash(w http.ResponseWriter, r *http.Request) {
+	slug := r.PathValue("id")
+	website, err := app.websites.Get(slugToShortId(slug))
+	if err != nil {
+		if errors.Is(err, models.ErrNoRecord) {
+			http.NotFound(w, r)
+		} else {
+			app.serverError(w, r, err)
+		}
+		return
+	}
+
+	comments, err := app.guestbookComments.GetDeleted(website.Guestbook.ID)
+	if err != nil {
+		if errors.Is(err, models.ErrNoRecord) {
+			http.NotFound(w, r)
+		} else {
+			app.serverError(w, r, err)
+		}
+		return
+	}
+
+	data := app.newCommonData(r)
+	views.GuestbookDashboardCommentsView("Trash", data, website, website.Guestbook, comments).Render(r.Context(), w)
+}
+
 func (app *application) putHideGuestbookComment(w http.ResponseWriter, r *http.Request) {
 
 }
 
-func (app *application) putDeleteGuestbookComment(w http.ResponseWriter, r *http.Request) {
+func (app *application) deleteGuestbookComment(w http.ResponseWriter, r *http.Request) {
+	user := app.getCurrentUser(r)
+	wSlug := r.PathValue("id")
+	website, err := app.websites.Get(slugToShortId(wSlug))
+	if err != nil {
+		app.logger.Info("website 404")
+		if errors.Is(err, models.ErrNoRecord) {
+			http.NotFound(w, r)
+		} else {
+			app.serverError(w, r, err)
+		}
+		return
+	}
+	if user.ID != website.UserId {
+		app.clientError(w, http.StatusUnauthorized)
+	}
+	cSlug := r.PathValue("commentId")
+	comment, err := app.guestbookComments.Get(slugToShortId(cSlug))
+	if err != nil {
+		app.logger.Info("comment 404")
+		if errors.Is(err, models.ErrNoRecord) {
+			http.NotFound(w, r)
+		} else {
+			app.serverError(w, r, err)
+		}
+		return
+	}
+	comment.Deleted = time.Now().UTC()
+	err = app.guestbookComments.UpdateComment(&comment)
+	if err != nil {
+		app.serverError(w, r, err)
+	}
+	comments, err := app.guestbookComments.GetAll(website.Guestbook.ID)
+	if err != nil {
+		app.serverError(w, r, err)
+	}
+	views.GuestbookCommentList(comments).Render(r.Context(), w)
 }
diff --git a/cmd/web/helpers.go b/cmd/web/helpers.go
index 164f603..ca0dfdd 100644
--- a/cmd/web/helpers.go
+++ b/cmd/web/helpers.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"math"
 	"net/http"
+	"runtime/debug"
 	"strconv"
 	"time"
 
@@ -21,6 +22,10 @@ func (app *application) serverError(w http.ResponseWriter, r *http.Request, err
 	)
 
 	app.logger.Error(err.Error(), "method", method, "uri", uri)
+	if app.debug {
+		http.Error(w, string(debug.Stack()), http.StatusInternalServerError)
+		app.logger.Error(err.Error(), "method", method, "uri", uri, "stack", string(debug.Stack()))
+	}
 	http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
 }
 
diff --git a/cmd/web/main.go b/cmd/web/main.go
index 7a6d51b..5bf5a1c 100644
--- a/cmd/web/main.go
+++ b/cmd/web/main.go
@@ -25,11 +25,13 @@ type application struct {
 	guestbookComments *models.GuestbookCommentModel
 	sessionManager    *scs.SessionManager
 	formDecoder       *schema.Decoder
+	debug             bool
 }
 
 func main() {
 	addr := flag.String("addr", ":3000", "HTTP network address")
 	dsn := flag.String("dsn", "guestbook.db", "data source name")
+	debug := flag.Bool("debug", false, "enable debug mode")
 	flag.Parse()
 
 	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
@@ -57,6 +59,7 @@ func main() {
 		users:             &models.UserModel{DB: db},
 		guestbookComments: &models.GuestbookCommentModel{DB: db},
 		formDecoder:       formDecoder,
+		debug:             *debug,
 	}
 
 	tlsConfig := &tls.Config{
diff --git a/cmd/web/routes.go b/cmd/web/routes.go
index fccac04..4d17cef 100644
--- a/cmd/web/routes.go
+++ b/cmd/web/routes.go
@@ -37,8 +37,9 @@ func (app *application) routes() http.Handler {
 	mux.Handle("GET /websites/{id}/dashboard", protected.ThenFunc(app.getWebsiteDashboard))
 	mux.Handle("GET /websites/{id}/dashboard/guestbook/comments", protected.ThenFunc(app.getGuestbookComments))
 	mux.Handle("GET /websites/{id}/dashboard/guestbook/comments/queue", protected.ThenFunc(app.getCommentQueue))
+	mux.Handle("DELETE /websites/{id}/dashboard/guestbook/comments/{commentId}", protected.ThenFunc(app.deleteGuestbookComment))
 	mux.Handle("GET /websites/{id}/dashboard/guestbook/blocklist", protected.ThenFunc(app.getComingSoon))
-	mux.Handle("GET /websites/{id}/dashboard/guestbook/comments/trash", protected.ThenFunc(app.getComingSoon))
+	mux.Handle("GET /websites/{id}/dashboard/guestbook/comments/trash", protected.ThenFunc(app.getCommentTrash))
 	mux.Handle("GET /websites/{id}/dashboard/guestbook/themes", protected.ThenFunc(app.getComingSoon))
 	mux.Handle("GET /websites/{id}/dashboard/guestbook/customize", protected.ThenFunc(app.getComingSoon))
 	mux.Handle("GET /websites/{id}/guestbook/comments/create", protected.ThenFunc(app.getGuestbookCommentCreate))
diff --git a/db/create-tables-sqlite.sql b/db/create-tables-sqlite.sql
index 7b6a121..ebeabcb 100644
--- a/db/create-tables-sqlite.sql
+++ b/db/create-tables-sqlite.sql
@@ -29,7 +29,7 @@ CREATE TABLE guestbooks (
     WebsiteId integer UNIQUE NOT NULL,
     UserId integer NOT NULL,
     Created datetime NOT NULL,
-    IsDeleted boolean NOT NULL DEFAULT FALSE,
+    Deleted datetime,
     IsActive boolean NOT NULL DEFAULT TRUE,
     FOREIGN KEY (UserId) REFERENCES users(Id)
         ON DELETE RESTRICT
@@ -51,7 +51,7 @@ CREATE TABLE guestbook_comments (
     PageUrl varchar(256),
     Created datetime NOT NULL,
     IsPublished boolean NOT NULL DEFAULT TRUE,
-    IsDeleted boolean NOT NULL DEFAULT FALSE,
+    Deleted datetime,
     FOREIGN KEY (GuestbookId) 
         REFERENCES guestbooks(Id)
         ON DELETE RESTRICT
diff --git a/internal/models/guestbook.go b/internal/models/guestbook.go
index a216572..21d899e 100644
--- a/internal/models/guestbook.go
+++ b/internal/models/guestbook.go
@@ -11,7 +11,7 @@ type Guestbook struct {
 	UserId    int64
 	WebsiteId int64
 	Created   time.Time
-	IsDeleted bool
+	Deleted   time.Time
 	IsActive  bool
 }
 
@@ -20,8 +20,8 @@ type GuestbookModel struct {
 }
 
 func (m *GuestbookModel) Insert(shortId uint64, userId int64, websiteId int64) (int64, error) {
-	stmt := `INSERT INTO guestbooks (ShortId, UserId, WebsiteId, Created, IsDeleted, IsActive)
-    VALUES(?, ?, ?, ?, FALSE, TRUE)`
+	stmt := `INSERT INTO guestbooks (ShortId, UserId, WebsiteId, Created, IsActive)
+    VALUES(?, ?, ?, ?, TRUE)`
 	result, err := m.DB.Exec(stmt, shortId, userId, websiteId, time.Now().UTC())
 	if err != nil {
 		return -1, err
@@ -34,21 +34,24 @@ func (m *GuestbookModel) Insert(shortId uint64, userId int64, websiteId int64) (
 }
 
 func (m *GuestbookModel) Get(shortId uint64) (Guestbook, error) {
-	stmt := `SELECT Id, ShortId, UserId, WebsiteId, Created, IsDeleted, IsActive FROM guestbooks
+	stmt := `SELECT Id, ShortId, UserId, WebsiteId, Created, Deleted, IsActive FROM guestbooks
     WHERE ShortId = ?`
 	row := m.DB.QueryRow(stmt, shortId)
 	var g Guestbook
-	err := row.Scan(&g.ID, &g.ShortId, &g.UserId, &g.WebsiteId, &g.Created, &g.IsDeleted, &g.IsActive)
+	var t sql.NullTime
+	err := row.Scan(&g.ID, &g.ShortId, &g.UserId, &g.WebsiteId, &g.Created, &t, &g.IsActive)
 	if err != nil {
 		return Guestbook{}, err
 	}
-
+	if t.Valid {
+		g.Deleted = t.Time
+	}
 	return g, nil
 }
 
 func (m *GuestbookModel) GetAll(userId int64) ([]Guestbook, error) {
-	stmt := `SELECT Id, ShortId, UserId, WebsiteId, Created, IsDeleted, IsActive FROM guestbooks
-    WHERE UserId = ?`
+	stmt := `SELECT Id, ShortId, UserId, WebsiteId, Created, IsActive FROM guestbooks
+    WHERE UserId = ? AND DELETED IS NULL`
 	rows, err := m.DB.Query(stmt, userId)
 	if err != nil {
 		return nil, err
@@ -56,7 +59,7 @@ func (m *GuestbookModel) GetAll(userId int64) ([]Guestbook, error) {
 	var guestbooks []Guestbook
 	for rows.Next() {
 		var g Guestbook
-		err = rows.Scan(&g.ID, &g.ShortId, &g.UserId, &g.WebsiteId, &g.Created, &g.IsDeleted, &g.IsActive)
+		err = rows.Scan(&g.ID, &g.ShortId, &g.UserId, &g.WebsiteId, &g.Created, &g.IsActive)
 		if err != nil {
 			return nil, err
 		}
diff --git a/internal/models/guestbookcomment.go b/internal/models/guestbookcomment.go
index e936304..1631fee 100644
--- a/internal/models/guestbookcomment.go
+++ b/internal/models/guestbookcomment.go
@@ -16,8 +16,8 @@ type GuestbookComment struct {
 	CommentText string
 	PageUrl     string
 	Created     time.Time
+	Deleted     time.Time
 	IsPublished bool
-	IsDeleted   bool
 }
 
 type GuestbookCommentModel struct {
@@ -27,8 +27,8 @@ type GuestbookCommentModel struct {
 func (m *GuestbookCommentModel) Insert(shortId uint64, guestbookId, parentId int64, authorName,
 	authorEmail, authorSite, commentText, pageUrl string, isPublished bool) (int64, error) {
 	stmt := `INSERT INTO guestbook_comments (ShortId, GuestbookId, ParentId, AuthorName,
-    AuthorEmail, AuthorSite, CommentText, PageUrl, Created, IsPublished, IsDeleted)
-    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, FALSE)`
+    AuthorEmail, AuthorSite, CommentText, PageUrl, Created, IsPublished)
+    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
 	result, err := m.DB.Exec(stmt, shortId, guestbookId, parentId, authorName, authorEmail,
 		authorSite, commentText, pageUrl, time.Now().UTC(), isPublished)
 	if err != nil {
@@ -43,21 +43,26 @@ func (m *GuestbookCommentModel) Insert(shortId uint64, guestbookId, parentId int
 
 func (m *GuestbookCommentModel) Get(shortId uint64) (GuestbookComment, error) {
 	stmt := `SELECT Id, ShortId, GuestbookId, ParentId, AuthorName, AuthorEmail, AuthorSite,
-    CommentText, PageUrl, Created, IsPublished, IsDeleted FROM guestbook_comments WHERE ShortId = ?`
+    CommentText, PageUrl, Created, IsPublished, Deleted FROM guestbook_comments WHERE ShortId = ?`
 	row := m.DB.QueryRow(stmt, shortId)
 	var c GuestbookComment
-	err := row.Scan(&c.ID, &c.ShortId, &c.GuestbookId, &c.ParentId, &c.AuthorName, &c.AuthorEmail, &c.AuthorSite, &c.CommentText, &c.PageUrl, &c.Created, &c.IsPublished, &c.IsDeleted)
+	var t sql.NullTime
+	err := row.Scan(&c.ID, &c.ShortId, &c.GuestbookId, &c.ParentId, &c.AuthorName, &c.AuthorEmail, &c.AuthorSite,
+		&c.CommentText, &c.PageUrl, &c.Created, &c.IsPublished, &t)
 	if err != nil {
 		return GuestbookComment{}, err
 	}
+	if t.Valid {
+		c.Deleted = t.Time
+	}
 	return c, nil
 }
 
 func (m *GuestbookCommentModel) GetAll(guestbookId int64) ([]GuestbookComment, error) {
 	stmt := `SELECT Id, ShortId, GuestbookId, ParentId, AuthorName, AuthorEmail, AuthorSite,
-    CommentText, PageUrl, Created, IsPublished, IsDeleted 
+    CommentText, PageUrl, Created, IsPublished 
 	    FROM guestbook_comments 
-	    WHERE GuestbookId = ? AND IsDeleted = FALSE AND IsPublished = TRUE
+	    WHERE GuestbookId = ? AND IsPublished = TRUE AND DELETED IS NULL
 	    ORDER BY Created DESC`
 	rows, err := m.DB.Query(stmt, guestbookId)
 	if err != nil {
@@ -66,7 +71,8 @@ func (m *GuestbookCommentModel) GetAll(guestbookId int64) ([]GuestbookComment, e
 	var comments []GuestbookComment
 	for rows.Next() {
 		var c GuestbookComment
-		err = rows.Scan(&c.ID, &c.ShortId, &c.GuestbookId, &c.ParentId, &c.AuthorName, &c.AuthorEmail, &c.AuthorSite, &c.CommentText, &c.PageUrl, &c.Created, &c.IsPublished, &c.IsDeleted)
+		err = rows.Scan(&c.ID, &c.ShortId, &c.GuestbookId, &c.ParentId, &c.AuthorName, &c.AuthorEmail, &c.AuthorSite,
+			&c.CommentText, &c.PageUrl, &c.Created, &c.IsPublished)
 		if err != nil {
 			return nil, err
 		}
@@ -78,11 +84,11 @@ func (m *GuestbookCommentModel) GetAll(guestbookId int64) ([]GuestbookComment, e
 	return comments, nil
 }
 
-func (m *GuestbookCommentModel) GetQueue(guestbookId int64) ([]GuestbookComment, error) {
+func (m *GuestbookCommentModel) GetDeleted(guestbookId int64) ([]GuestbookComment, error) {
 	stmt := `SELECT Id, ShortId, GuestbookId, ParentId, AuthorName, AuthorEmail, AuthorSite,
-    CommentText, PageUrl, Created, IsPublished, IsDeleted 
+    CommentText, PageUrl, Created, IsPublished, Deleted
 	    FROM guestbook_comments 
-	    WHERE GuestbookId = ? AND IsDeleted = FALSE AND IsPublished = FALSE
+	    WHERE GuestbookId = ? AND Deleted IS NOT NULL
 	    ORDER BY Created DESC`
 	rows, err := m.DB.Query(stmt, guestbookId)
 	if err != nil {
@@ -91,7 +97,38 @@ func (m *GuestbookCommentModel) GetQueue(guestbookId int64) ([]GuestbookComment,
 	var comments []GuestbookComment
 	for rows.Next() {
 		var c GuestbookComment
-		err = rows.Scan(&c.ID, &c.ShortId, &c.GuestbookId, &c.ParentId, &c.AuthorName, &c.AuthorEmail, &c.AuthorSite, &c.CommentText, &c.PageUrl, &c.Created, &c.IsPublished, &c.IsDeleted)
+		var t sql.NullTime
+		err = rows.Scan(&c.ID, &c.ShortId, &c.GuestbookId, &c.ParentId, &c.AuthorName, &c.AuthorEmail, &c.AuthorSite,
+			&c.CommentText, &c.PageUrl, &c.Created, &c.IsPublished, &t)
+		if err != nil {
+			return nil, err
+		}
+		if t.Valid {
+			c.Deleted = t.Time
+		}
+		comments = append(comments, c)
+	}
+	if err = rows.Err(); err != nil {
+		return nil, err
+	}
+	return comments, nil
+}
+
+func (m *GuestbookCommentModel) GetUnpublished(guestbookId int64) ([]GuestbookComment, error) {
+	stmt := `SELECT Id, ShortId, GuestbookId, ParentId, AuthorName, AuthorEmail, AuthorSite,
+    CommentText, PageUrl, Created, IsPublished 
+	    FROM guestbook_comments 
+	    WHERE GuestbookId = ? AND IsDeleted IS NULL AND IsPublished = FALSE
+	    ORDER BY Created DESC`
+	rows, err := m.DB.Query(stmt, guestbookId)
+	if err != nil {
+		return nil, err
+	}
+	var comments []GuestbookComment
+	for rows.Next() {
+		var c GuestbookComment
+		err = rows.Scan(&c.ID, &c.ShortId, &c.GuestbookId, &c.ParentId, &c.AuthorName, &c.AuthorEmail, &c.AuthorSite,
+			&c.CommentText, &c.PageUrl, &c.Created, &c.IsPublished)
 		if err != nil {
 			return nil, err
 		}
@@ -104,10 +141,13 @@ func (m *GuestbookCommentModel) GetQueue(guestbookId int64) ([]GuestbookComment,
 }
 
 func (m *GuestbookCommentModel) UpdateComment(comment *GuestbookComment) error {
-	stmt := `UPDATE guestbook_comments (CommentText, PageUrl, IsPublished, IsDeleted)
-		VALUES (?, ?, ?, ?)
+	stmt := `UPDATE guestbook_comments
+			SET CommentText = ?,
+				PageUrl = ?,
+				IsPublished = ?,
+				Deleted = ?
 		WHERE Id = ?`
-	_, err := m.DB.Exec(stmt, comment.CommentText, comment.PageUrl, comment.IsPublished, comment.IsDeleted, comment.ID)
+	_, err := m.DB.Exec(stmt, comment.CommentText, comment.PageUrl, comment.IsPublished, comment.Deleted, comment.ID)
 	if err != nil {
 		return err
 	}
diff --git a/internal/models/website.go b/internal/models/website.go
index 1b9da9f..54d023b 100644
--- a/internal/models/website.go
+++ b/internal/models/website.go
@@ -36,48 +36,38 @@ func (m *WebsiteModel) Insert(shortId uint64, userId int64, siteName, siteUrl, a
 }
 
 func (m *WebsiteModel) Get(shortId uint64) (Website, error) {
-	stmt := `SELECT w.Id, w.ShortId, w.Name, w.SiteUrl, w.AuthorName, w.UserId, w.Created, w.Deleted,
-	g.Id, g.ShortId, g.Created, g.IsDeleted, g.IsActive
+	stmt := `SELECT w.Id, w.ShortId, w.Name, w.SiteUrl, w.AuthorName, w.UserId, w.Created,
+	g.Id, g.ShortId, g.Created, g.IsActive
 	FROM websites AS w INNER JOIN guestbooks AS g ON w.Id = g.WebsiteId
-	WHERE w.ShortId = ?`
+	WHERE w.ShortId = ? AND w.DELETED IS NULL`
 	row := m.DB.QueryRow(stmt, shortId)
-	var t sql.NullTime
 	var w Website
-	err := row.Scan(&w.ID, &w.ShortId, &w.Name, &w.SiteUrl, &w.AuthorName, &w.UserId, &w.Created, &t,
-		&w.Guestbook.ID, &w.Guestbook.ShortId, &w.Guestbook.Created, &w.Guestbook.IsDeleted, &w.Guestbook.IsActive)
+	err := row.Scan(&w.ID, &w.ShortId, &w.Name, &w.SiteUrl, &w.AuthorName, &w.UserId, &w.Created,
+		&w.Guestbook.ID, &w.Guestbook.ShortId, &w.Guestbook.Created, &w.Guestbook.IsActive)
 	if err != nil {
 		return Website{}, err
 	}
-	// handle if Deleted is null
-	if t.Valid {
-		w.Deleted = t.Time
-	}
 	return w, nil
 }
 
 func (m *WebsiteModel) GetById(id int64) (Website, error) {
-	stmt := `SELECT w.Id, w.ShortId, w.Name, w.SiteUrl, w.AuthorName, w.UserId, w.Created, w.Deleted,
-	g.Id, g.ShortId, g.Created, g.IsDeleted, g.IsActive
+	stmt := `SELECT w.Id, w.ShortId, w.Name, w.SiteUrl, w.AuthorName, w.UserId, w.Created,
+	g.Id, g.ShortId, g.Created, g.IsActive
 	FROM websites AS w INNER JOIN guestbooks AS g ON w.Id = g.WebsiteId
-	WHERE w.Id = ?`
+	WHERE w.Id = ? AND w.DELETED IS NULL`
 	row := m.DB.QueryRow(stmt, id)
-	var t sql.NullTime
 	var w Website
-	err := row.Scan(&w.ID, &w.ShortId, &w.Name, &w.SiteUrl, &w.AuthorName, &w.UserId, &w.Created, &t,
-		&w.Guestbook.ID, &w.Guestbook.ShortId, &w.Guestbook.Created, &w.Guestbook.IsDeleted, &w.Guestbook.IsActive)
+	err := row.Scan(&w.ID, &w.ShortId, &w.Name, &w.SiteUrl, &w.AuthorName, &w.UserId, &w.Created,
+		&w.Guestbook.ID, &w.Guestbook.ShortId, &w.Guestbook.Created, &w.Guestbook.IsActive)
 	if err != nil {
 		return Website{}, err
 	}
-	// handle if Deleted is null
-	if t.Valid {
-		w.Deleted = t.Time
-	}
 	return w, nil
 }
 
 func (m *WebsiteModel) GetAll(userId int64) ([]Website, error) {
-	stmt := `SELECT w.Id, w.ShortId, w.Name, w.SiteUrl, w.AuthorName, w.UserId, w.Created, w.Deleted,
-	g.Id, g.ShortId, g.Created, g.IsDeleted, g.IsActive
+	stmt := `SELECT w.Id, w.ShortId, w.Name, w.SiteUrl, w.AuthorName, w.UserId, w.Created, 
+	g.Id, g.ShortId, g.Created, g.IsActive
 	FROM websites AS w INNER JOIN guestbooks AS g ON w.Id = g.WebsiteId
 	WHERE w.UserId = ?`
 	rows, err := m.DB.Query(stmt, userId)
@@ -86,10 +76,9 @@ func (m *WebsiteModel) GetAll(userId int64) ([]Website, error) {
 	}
 	var websites []Website
 	for rows.Next() {
-		var t sql.NullTime
 		var w Website
-		err := rows.Scan(&w.ID, &w.ShortId, &w.Name, &w.SiteUrl, &w.AuthorName, &w.UserId, &w.Created, &t,
-			&w.Guestbook.ID, &w.Guestbook.ShortId, &w.Guestbook.Created, &w.Guestbook.IsDeleted, &w.Guestbook.IsActive)
+		err := rows.Scan(&w.ID, &w.ShortId, &w.Name, &w.SiteUrl, &w.AuthorName, &w.UserId, &w.Created,
+			&w.Guestbook.ID, &w.Guestbook.ShortId, &w.Guestbook.Created, &w.Guestbook.IsActive)
 		if err != nil {
 			return nil, err
 		}
diff --git a/ui/views/guestbooks.templ b/ui/views/guestbooks.templ
index b77f61e..7d149fe 100644
--- a/ui/views/guestbooks.templ
+++ b/ui/views/guestbooks.templ
@@ -28,6 +28,12 @@ templ GuestbookDashboardCommentsView(title string, data CommonData, website mode
                     
                         { c.AuthorName }
                         { c.Created.Format("01-02-2006 03:04PM") }
+                        if c.Deleted.IsZero() {
+                            {{ commentUrl := fmt.Sprintf("%s/dashboard/guestbook/comments/%s", wUrl(website), shortIdToSlug(c.ShortId)) }}
+                            {{ hxHeaders := fmt.Sprintf("{\"X-CSRF-Token\": \"%s\"}", data.CSRFToken) }}
+                            
Delete
+                        }
+                        
                         
                             { c.CommentText }
                         
@@ -96,18 +102,25 @@ templ GuestbookView(title string, data CommonData, website models.Website, guest
     if data.IsHtmx {
         @commentForm(form)
     } else {
-        
-            
-                
Guestbook for { website.SiteUrl }
-                
-            
-            
-        
+        
+            
+            
{ title }
+            
+            
+                
+                    
+                        
Guestbook for { website.SiteUrl }
+                        
+                    
+                    
+                
+            
+        
     }
 }
 
diff --git a/ui/views/guestbooks_templ.go b/ui/views/guestbooks_templ.go
index aada8c4..5bb6feb 100644
--- a/ui/views/guestbooks_templ.go
+++ b/ui/views/guestbooks_templ.go
@@ -149,25 +149,63 @@ func GuestbookDashboardCommentsView(title string, data CommonData, website model
 				if templ_7745c5c3_Err != nil {
 					return templ_7745c5c3_Err
 				}
-				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
")
+				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " ")
 				if templ_7745c5c3_Err != nil {
 					return templ_7745c5c3_Err
 				}
-				var templ_7745c5c3_Var8 string
-				templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(c.CommentText)
-				if templ_7745c5c3_Err != nil {
-					return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/guestbooks.templ`, Line: 32, Col: 43}
+				if c.Deleted.IsZero() {
+					commentUrl := fmt.Sprintf("%s/dashboard/guestbook/comments/%s", wUrl(website), shortIdToSlug(c.ShortId))
+					hxHeaders := fmt.Sprintf("{\"X-CSRF-Token\": \"%s\"}", data.CSRFToken)
+					templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "Delete")
+					if templ_7745c5c3_Err != nil {
+						return templ_7745c5c3_Err
+					}
 				}
-				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
+				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
")
 				if templ_7745c5c3_Err != nil {
 					return templ_7745c5c3_Err
 				}
-				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
 ")
+				var templ_7745c5c3_Var10 string
+				templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(c.CommentText)
+				if templ_7745c5c3_Err != nil {
+					return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/guestbooks.templ`, Line: 38, Col: 43}
+				}
+				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
+				if templ_7745c5c3_Err != nil {
+					return templ_7745c5c3_Err
+				}
+				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
")
 				if templ_7745c5c3_Err != nil {
 					return templ_7745c5c3_Err
 				}
 			}
-			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "")
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "")
 			if templ_7745c5c3_Err != nil {
 				return templ_7745c5c3_Err
 			}
@@ -197,108 +235,108 @@ func commentForm(form forms.CommentCreateForm) templ.Component {
 			}()
 		}
 		ctx = templ.InitializeContext(ctx)
-		templ_7745c5c3_Var9 := templ.GetChildren(ctx)
-		if templ_7745c5c3_Var9 == nil {
-			templ_7745c5c3_Var9 = templ.NopComponent
+		templ_7745c5c3_Var11 := templ.GetChildren(ctx)
+		if templ_7745c5c3_Var11 == nil {
+			templ_7745c5c3_Var11 = templ.NopComponent
 		}
 		ctx = templ.ClearChildren(ctx)
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
")
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
 		error, exists := form.FieldErrors["authorName"]
 		if exists {
-			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " ")
-			if templ_7745c5c3_Err != nil {
-				return templ_7745c5c3_Err
-			}
-		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
")
-		if templ_7745c5c3_Err != nil {
-			return templ_7745c5c3_Err
-		}
-		error, exists = form.FieldErrors["authorEmail"]
-		if exists {
-			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " ")
-			if templ_7745c5c3_Err != nil {
-				return templ_7745c5c3_Err
-			}
-		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
")
-		if templ_7745c5c3_Err != nil {
-			return templ_7745c5c3_Err
-		}
-		error, exists = form.FieldErrors["authorSite"]
-		if exists {
-			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " ")
 			if templ_7745c5c3_Err != nil {
 				return templ_7745c5c3_Err
 			}
 		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
Comment: ")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
Email: ")
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
-		error, exists = form.FieldErrors["content"]
+		error, exists = form.FieldErrors["authorEmail"]
 		if exists {
-			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "")
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "")
 			if templ_7745c5c3_Err != nil {
 				return templ_7745c5c3_Err
 			}
 			var templ_7745c5c3_Var13 string
 			templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(error)
 			if templ_7745c5c3_Err != nil {
-				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/guestbooks.templ`, Line: 70, Col: 36}
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/guestbooks.templ`, Line: 60, Col: 36}
 			}
 			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
 			if templ_7745c5c3_Err != nil {
 				return templ_7745c5c3_Err
 			}
-			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " ")
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, " ")
 			if templ_7745c5c3_Err != nil {
 				return templ_7745c5c3_Err
 			}
 		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
Site Url: ")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		error, exists = form.FieldErrors["authorSite"]
+		if exists {
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			var templ_7745c5c3_Var14 string
+			templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(error)
+			if templ_7745c5c3_Err != nil {
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/guestbooks.templ`, Line: 68, Col: 36}
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, " ")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+		}
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
Comment: ")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		error, exists = form.FieldErrors["content"]
+		if exists {
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			var templ_7745c5c3_Var15 string
+			templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(error)
+			if templ_7745c5c3_Err != nil {
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/guestbooks.templ`, Line: 76, Col: 36}
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " ")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+		}
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
")
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
@@ -322,58 +360,58 @@ func GuestbookCommentList(comments []models.GuestbookComment) templ.Component {
 			}()
 		}
 		ctx = templ.InitializeContext(ctx)
-		templ_7745c5c3_Var14 := templ.GetChildren(ctx)
-		if templ_7745c5c3_Var14 == nil {
-			templ_7745c5c3_Var14 = templ.NopComponent
+		templ_7745c5c3_Var16 := templ.GetChildren(ctx)
+		if templ_7745c5c3_Var16 == nil {
+			templ_7745c5c3_Var16 = templ.NopComponent
 		}
 		ctx = templ.ClearChildren(ctx)
 		if len(comments) == 0 {
-			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "No comments yet!
")
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "No comments yet!
")
 			if templ_7745c5c3_Err != nil {
 				return templ_7745c5c3_Err
 			}
 		}
 		for _, c := range comments {
-			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "")
-			if templ_7745c5c3_Err != nil {
-				return templ_7745c5c3_Err
-			}
-			var templ_7745c5c3_Var15 string
-			templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(c.AuthorName)
-			if templ_7745c5c3_Err != nil {
-				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/guestbooks.templ`, Line: 85, Col: 34}
-			}
-			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
-			if templ_7745c5c3_Err != nil {
-				return templ_7745c5c3_Err
-			}
-			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " ")
-			if templ_7745c5c3_Err != nil {
-				return templ_7745c5c3_Err
-			}
-			var templ_7745c5c3_Var16 string
-			templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(c.Created.Format("01-02-2006 03:04PM"))
-			if templ_7745c5c3_Err != nil {
-				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/guestbooks.templ`, Line: 86, Col: 52}
-			}
-			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
-			if templ_7745c5c3_Err != nil {
-				return templ_7745c5c3_Err
-			}
-			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
")
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "
")
 			if templ_7745c5c3_Err != nil {
 				return templ_7745c5c3_Err
 			}
 			var templ_7745c5c3_Var17 string
-			templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(c.CommentText)
+			templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(c.AuthorName)
 			if templ_7745c5c3_Err != nil {
-				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/guestbooks.templ`, Line: 88, Col: 31}
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/guestbooks.templ`, Line: 91, Col: 34}
 			}
 			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
 			if templ_7745c5c3_Err != nil {
 				return templ_7745c5c3_Err
 			}
-			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
")
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, " ")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			var templ_7745c5c3_Var18 string
+			templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(c.Created.Format("01-02-2006 03:04PM"))
+			if templ_7745c5c3_Err != nil {
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/guestbooks.templ`, Line: 92, Col: 52}
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			var templ_7745c5c3_Var19 string
+			templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(c.CommentText)
+			if templ_7745c5c3_Err != nil {
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/guestbooks.templ`, Line: 94, Col: 31}
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
 ")
 			if templ_7745c5c3_Err != nil {
 				return templ_7745c5c3_Err
 			}
@@ -398,9 +436,9 @@ func GuestbookView(title string, data CommonData, website models.Website, guestb
 			}()
 		}
 		ctx = templ.InitializeContext(ctx)
-		templ_7745c5c3_Var18 := templ.GetChildren(ctx)
-		if templ_7745c5c3_Var18 == nil {
-			templ_7745c5c3_Var18 = templ.NopComponent
+		templ_7745c5c3_Var20 := templ.GetChildren(ctx)
+		if templ_7745c5c3_Var20 == nil {
+			templ_7745c5c3_Var20 = templ.NopComponent
 		}
 		ctx = templ.ClearChildren(ctx)
 		postUrl := fmt.Sprintf("/websites/%s/guestbook/comments/create", shortIdToSlug(website.ShortId))
@@ -410,42 +448,55 @@ func GuestbookView(title string, data CommonData, website models.Website, guestb
 				return templ_7745c5c3_Err
 			}
 		} else {
-			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "Guestbook for ")
-			if templ_7745c5c3_Err != nil {
-				return templ_7745c5c3_Err
-			}
-			var templ_7745c5c3_Var19 string
-			templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(website.SiteUrl)
-			if templ_7745c5c3_Err != nil {
-				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/guestbooks.templ`, Line: 101, Col: 51}
-			}
-			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
-			if templ_7745c5c3_Err != nil {
-				return templ_7745c5c3_Err
-			}
-			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "
")
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "