diff --git a/internal/controller/admin/admin_comment_controller.go b/internal/controller/admin/admin_comment_controller.go new file mode 100644 index 0000000..6a45d0b --- /dev/null +++ b/internal/controller/admin/admin_comment_controller.go @@ -0,0 +1,66 @@ +package admin + +import ( + "net/http" + "strconv" + + "metazone.cc/metalab/internal/common" + "metazone.cc/metalab/internal/model" + + "github.com/gin-gonic/gin" +) + +// AdminCommentController 后台评论管理 +type AdminCommentController struct { + commentService adminCommentUseCase +} + +// NewAdminCommentController 构造函数 +func NewAdminCommentController(cs adminCommentUseCase) *AdminCommentController { + return &AdminCommentController{commentService: cs} +} + +// CommentsPage SSR 页面 +func (ctrl *AdminCommentController) CommentsPage(c *gin.Context) { + c.HTML(http.StatusOK, "admin/comments/index.html", common.BuildAdminPageData(c, gin.H{ + "Title": "评论管理", + "ExtraCSS": "/admin/static/css/comments.css", + })) +} + +// ListComments API 列表 +func (ctrl *AdminCommentController) ListComments(c *gin.Context) { + keyword := c.Query("keyword") + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + + comments, total, err := ctrl.commentService.ListAllComments(keyword, false, page, pageSize) + if err != nil { + common.Error(c, http.StatusInternalServerError, "获取评论列表失败") + return + } + if comments == nil { + comments = []model.AdminCommentRow{} + } + + common.Ok(c, gin.H{ + "items": comments, + "total": total, + "page": page, + "total_pages": common.PageCount(total, pageSize), + }) +} + +// Delete 软删除评论 +func (ctrl *AdminCommentController) Delete(c *gin.Context) { + commentID, err := strconv.ParseUint(c.Param("id"), 10, 64) + if err != nil { + common.Error(c, http.StatusBadRequest, "无效的评论ID") + return + } + if err := ctrl.commentService.Delete(uint(commentID)); err != nil { + common.Error(c, http.StatusBadRequest, err.Error()) + return + } + common.OkMessage(c, "删除成功") +} diff --git a/internal/controller/admin/interfaces.go b/internal/controller/admin/interfaces.go index d29f228..8250fcd 100644 --- a/internal/controller/admin/interfaces.go +++ b/internal/controller/admin/interfaces.go @@ -46,3 +46,9 @@ type adminPostUseCase interface { Unlock(postID uint) error Restore(postID uint) error } + +// adminCommentUseCase AdminCommentController 对 CommentService 的最小依赖(ISP:2 个方法) +type adminCommentUseCase interface { + ListAllComments(keyword string, showDeleted bool, page, pageSize int) ([]model.AdminCommentRow, int64, error) + Delete(commentID uint) error +} diff --git a/internal/controller/comment_controller.go b/internal/controller/comment_controller.go index edcecb1..6cf7170 100644 --- a/internal/controller/comment_controller.go +++ b/internal/controller/comment_controller.go @@ -1,9 +1,17 @@ package controller import ( - "encoding/json" + "bytes" + "image" + "image/jpeg" + "image/png" + "io" "net/http" + "os" + "path/filepath" "strconv" + "strings" + "time" "metazone.cc/metalab/internal/common" "metazone.cc/metalab/internal/model" @@ -177,8 +185,102 @@ func (ctrl *CommentController) SearchUsers(c *gin.Context) { common.Ok(c, users) } -// marshalJSON helper -func marshalJSON(v interface{}) string { - b, _ := json.Marshal(v) - return string(b) +// UploadImage 上传评论图片 POST /api/comments/upload-image +// 需要 LV4+(Exp ≥ 1500),复用帖子图片上传的 WebP 转码逻辑 +func (ctrl *CommentController) UploadImage(c *gin.Context) { + uid, _, ok := common.GetGinUser(c) + if !ok { + common.Error(c, http.StatusUnauthorized, "请先登录") + return + } + + // LV4+ 检查(Exp ≥ 1500) + expVal, exists := c.Get("exp") + if !exists { + common.Error(c, http.StatusForbidden, "无法获取经验值") + return + } + exp := expVal.(int) + if exp < 1500 { + common.Error(c, http.StatusForbidden, "需 Lv4 以上才能上传评论图片") + return + } + + file, header, err := c.Request.FormFile("file") + if err != nil { + common.Error(c, http.StatusBadRequest, "请选择文件") + return + } + defer file.Close() + + // 检查扩展名 + ext := strings.ToLower(filepath.Ext(header.Filename)) + allowedExt := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true} + if !allowedExt[ext] { + common.Error(c, http.StatusBadRequest, "仅支持 jpg/jpeg/png/gif/webp 格式") + return + } + + // 限制 5MB + if header.Size > 5<<20 { + common.Error(c, http.StatusBadRequest, "图片大小不能超过 5MB") + return + } + + data, err := io.ReadAll(file) + if err != nil { + common.Error(c, http.StatusInternalServerError, "读取文件失败") + return + } + + storageDir := "storage/uploads/posts" + if err := os.MkdirAll(storageDir, 0755); err != nil { + common.Error(c, http.StatusInternalServerError, "存储初始化失败") + return + } + + ts := time.Now().UnixMilli() + isJPEGPNG := ext == ".jpg" || ext == ".jpeg" || ext == ".png" + + var savePath, url string + + // JPEG/PNG 转 WebP + if isJPEGPNG { + img, _, err := image.Decode(bytes.NewReader(data)) + if err != nil { + common.Error(c, http.StatusBadRequest, "图片格式无效") + return + } + webpData := encodeWebPBinary(img, len(data)) + if webpData != nil && len(webpData) < len(data) { + filename := strconv.FormatUint(uint64(uid), 10) + "_" + strconv.FormatInt(ts, 10) + ".webp" + savePath = filepath.Join(storageDir, filename) + os.WriteFile(savePath, webpData, 0644) + url = "/uploads/posts/" + filename + } + } + + // 回退存储 + if savePath == "" { + dataOut := data + if isJPEGPNG { + decImg, _, decErr := image.Decode(bytes.NewReader(data)) + if decErr == nil { + var buf bytes.Buffer + if ext == ".png" { + png.Encode(&buf, decImg) + } else { + jpeg.Encode(&buf, decImg, &jpeg.Options{Quality: 92}) + } + dataOut = buf.Bytes() + } + } + filename := strconv.FormatUint(uint64(uid), 10) + "_" + strconv.FormatInt(ts, 10) + ext + savePath = filepath.Join(storageDir, filename) + os.WriteFile(savePath, dataOut, 0644) + url = "/uploads/posts/" + filename + } + + // 返回标准 JSON(前端将 URL 插入评论文本) + common.Ok(c, gin.H{"url": url}) } diff --git a/internal/model/comment.go b/internal/model/comment.go index 36ad642..ac292cd 100644 --- a/internal/model/comment.go +++ b/internal/model/comment.go @@ -40,3 +40,15 @@ type UserSearchResult struct { Username string `json:"username"` Level int `json:"level"` } + +// AdminCommentRow 后台评论列表行 +type AdminCommentRow struct { + ID uint `json:"id"` + PostID uint `json:"post_id"` + PostTitle string `json:"post_title"` + Body string `json:"body"` + AuthorName string `json:"author_name"` + IsDeleted bool `json:"is_deleted"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/internal/repository/comment_repo.go b/internal/repository/comment_repo.go index e965d72..6d84a28 100644 --- a/internal/repository/comment_repo.go +++ b/internal/repository/comment_repo.go @@ -248,3 +248,31 @@ func (r *CommentRepo) FindPostAuthorID(postID uint) (uint, error) { err := r.db.Table("posts").Select("user_id").Where("id = ?", postID).Scan(&authorID).Error return authorID, err } + +// ListAllComments 后台评论列表(支持关键词搜索、分页) +func (r *CommentRepo) ListAllComments(keyword string, showDeleted bool, offset, limit int) ([]model.AdminCommentRow, int64, error) { + var total int64 + q := r.db.Table("comments"). + Joins("LEFT JOIN users ON users.uid = comments.user_id"). + Joins("LEFT JOIN posts ON posts.id = comments.post_id") + + if !showDeleted { + q = q.Where("comments.is_deleted = false") + } + if keyword != "" { + like := "%" + keyword + "%" + q = q.Where("comments.body LIKE ? OR users.username LIKE ? OR posts.title LIKE ?", like, like, like) + } + + if err := q.Count(&total).Error; err != nil { + return nil, 0, err + } + + var rows []model.AdminCommentRow + err := q. + Select("comments.id, comments.post_id, posts.title as post_title, comments.body, users.username as author_name, comments.is_deleted, comments.created_at, comments.updated_at"). + Order("comments.created_at DESC"). + Offset(offset).Limit(limit). + Scan(&rows).Error + return rows, total, err +} diff --git a/internal/router/admin.go b/internal/router/admin.go index 124f446..e055673 100644 --- a/internal/router/admin.go +++ b/internal/router/admin.go @@ -30,6 +30,7 @@ func setupAdminRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) { middleware.RequirePageRole(model.RoleOwner)) adminPages.GET("/energy", d.adminEnergyCtrl.EnergyPage) adminPages.GET("/energy/logs", d.adminEnergyCtrl.EnergyLogPage) + adminPages.GET("/comments", d.adminCommentCtrl.CommentsPage) } // --- 管理后台 API --- @@ -85,4 +86,15 @@ func setupAdminRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) { middleware.RequireMinRole(model.RoleOwner)) adminEnergyAPI.GET("/logs", d.adminEnergyCtrl.ListEnergyLogs) } + + // --- 评论管理 API(admin+) --- + adminCommentAPI := r.Group("/api/admin/comments") + adminCommentAPI.Use(d.authMdw.Required()) + adminCommentAPI.Use(middleware.RequireMinRole(model.RoleAdmin)) + adminCommentAPI.Use(d.authMdw.BannedWriteGuard()) + adminCommentAPI.Use(middleware.CSRF(cfg)) + { + adminCommentAPI.GET("", d.adminCommentCtrl.ListComments) + adminCommentAPI.DELETE("/:id", d.adminCommentCtrl.Delete) + } } diff --git a/internal/router/api.go b/internal/router/api.go index f5881a3..25a48ec 100644 --- a/internal/router/api.go +++ b/internal/router/api.go @@ -116,5 +116,6 @@ func setupAPIRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) { commentsAPI.POST("/comments/:id/reply", d.commentCtrl.CreateReply) commentsAPI.DELETE("/comments/:id", d.commentCtrl.Delete) commentsAPI.GET("/users/search", d.commentCtrl.SearchUsers) + commentsAPI.POST("/comments/upload-image", d.commentCtrl.UploadImage) } } diff --git a/internal/router/deps_core.go b/internal/router/deps_core.go index 985b7fe..9878fc3 100644 --- a/internal/router/deps_core.go +++ b/internal/router/deps_core.go @@ -36,6 +36,7 @@ type dependencies struct { energyService *service.EnergyService commentCtrl *controller.CommentController commentService *service.CommentService + adminCommentCtrl *adminCtrl.AdminCommentController } func buildDepsCore(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSettings, redisClient *redis.Client) ( diff --git a/internal/router/deps_extra.go b/internal/router/deps_extra.go index 4a4d0d3..755ddff 100644 --- a/internal/router/deps_extra.go +++ b/internal/router/deps_extra.go @@ -64,6 +64,9 @@ func buildDeps(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSetting commentService := service.NewCommentService(commentRepo, notificationService) commentCtrl := controller.NewCommentController(commentService) + // 后台评论管理 + adminCommentCtrl := adminCtrl.NewAdminCommentController(commentService) + // 用户空间 spaceService := service.NewSpaceService(userRepo, postRepo) spaceCtrl := controller.NewSpaceController(spaceService) @@ -84,5 +87,6 @@ func buildDeps(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSetting energyService: energyService, commentCtrl: commentCtrl, commentService: commentService, + adminCommentCtrl: adminCommentCtrl, } } diff --git a/internal/service/comment_service.go b/internal/service/comment_service.go index a6db85c..7cbf0d0 100644 --- a/internal/service/comment_service.go +++ b/internal/service/comment_service.go @@ -58,13 +58,13 @@ func (s *CommentService) CreateRoot(userID uint, postID uint, body string) (*mod // 解析 @ 提及 s.parseMentions(comment.ID, body) - // 通知文章作者(评论者 ≠ 作者) + // 通知文章作者(评论者 ≠ 作者),RelatedID 存 postID 以便消息页生成跳转链接 if s.notifier != nil && userID != postAuthorID { commenterUID, _ := s.repo.GetUserUIDByID(userID) s.notifier.Create(postAuthorID, model.NotifyComment, "新评论", fmt.Sprintf("%s 评论了你的文章", commenterUID), - &comment.ID) + &comment.PostID) } return comment, nil @@ -106,13 +106,13 @@ func (s *CommentService) CreateReply(userID uint, parentID uint, body string) (* // 解析 @ 提及 s.parseMentions(comment.ID, body) - // 通知被回复者(不自己回复自己) + // 通知被回复者(不自己回复自己),RelatedID 存 postID 以便消息页生成跳转链接 if s.notifier != nil && parent.UserID != userID { commenterUID, _ := s.repo.GetUserUIDByID(userID) s.notifier.Create(parent.UserID, model.NotifyCommentReply, "新回复", fmt.Sprintf("%s 回复了你的评论", commenterUID), - &comment.ID) + &parent.PostID) } return comment, nil @@ -159,6 +159,17 @@ func (s *CommentService) ListReplies(rootID uint) ([]model.Comment, error) { return list, nil } +// ListAllComments 后台评论列表(分页+搜索) +func (s *CommentService) ListAllComments(keyword string, showDeleted bool, page, pageSize int) ([]model.AdminCommentRow, int64, error) { + p := common.Pagination{Page: page, PageSize: pageSize} + p.DefaultPagination() + rows, total, err := s.repo.ListAllComments(keyword, showDeleted, p.Offset(), p.PageSize) + if rows == nil { + rows = []model.AdminCommentRow{} + } + return rows, total, err +} + // SearchUsers 搜索LV3+用户(用于@艾特) func (s *CommentService) SearchUsers(keyword string) ([]model.UserSearchResult, error) { rows, err := s.repo.SearchUsersByLevel(keyword, 3, 10) diff --git a/internal/service/repository.go b/internal/service/repository.go index 2d1188b..ea050d9 100644 --- a/internal/service/repository.go +++ b/internal/service/repository.go @@ -134,6 +134,7 @@ type commentStore interface { Username string }, error) GetUserUIDByID(userID uint) (string, error) + ListAllComments(keyword string, showDeleted bool, offset, limit int) ([]model.AdminCommentRow, int64, error) } // commentNotifier CommentService 所需的通知接口(ISP) diff --git a/templates/MetaLab-2026/html/messages/index.html b/templates/MetaLab-2026/html/messages/index.html index a90461a..3fec2f5 100644 --- a/templates/MetaLab-2026/html/messages/index.html +++ b/templates/MetaLab-2026/html/messages/index.html @@ -41,6 +41,8 @@ {{$link := ""}} {{if or (eq $m.NotifyType "post_approved") (eq $m.NotifyType "post_rejected")}} {{$link = printf "/posts/%d" (derefUint $m.RelatedID)}} + {{else if or (eq $m.NotifyType "comment") (eq $m.NotifyType "comment_reply")}} + {{$link = printf "/posts/%d#comments" (derefUint $m.RelatedID)}} {{else if or (eq $m.NotifyType "audit_approved") (eq $m.NotifyType "audit_rejected")}} {{$link = "/settings"}} {{end}} diff --git a/templates/MetaLab-2026/html/posts/show.html b/templates/MetaLab-2026/html/posts/show.html index c434002..0510e4b 100644 --- a/templates/MetaLab-2026/html/posts/show.html +++ b/templates/MetaLab-2026/html/posts/show.html @@ -80,7 +80,15 @@ {{if .IsLoggedIn}}
- +
+ + + +
+
{{else}}
@@ -640,6 +648,71 @@ el.textContent = '(' + (current + delta) + ')'; } + // ====== 图片上传 ====== + var imageUploadInput = document.getElementById('commentImageInput'); + var uploadLabel = document.getElementById('commentUploadLabel'); + var uploadPreview = document.getElementById('commentUploadPreview'); + + if (imageUploadInput && uploadLabel) { + uploadLabel.addEventListener('click', function() { + imageUploadInput.click(); + }); + + imageUploadInput.addEventListener('change', function() { + var file = imageUploadInput.files[0]; + if (!file) return; + uploadLabel.style.pointerEvents = 'none'; + uploadLabel.style.opacity = '0.5'; + uploadLabel.textContent = '上传中...'; + + var formData = new FormData(); + formData.append('file', file); + + fetch('/api/comments/upload-image', { + method: 'POST', + headers: { 'X-CSRF-Token': csrfToken }, + body: formData + }) + .then(function(r) { return r.json(); }) + .then(function(d) { + uploadLabel.style.pointerEvents = 'auto'; + uploadLabel.style.opacity = '1'; + uploadLabel.innerHTML = ' 图片'; + + if (d.success && d.data && d.data.url) { + // 插入图片 URL 到文本框当前光标位置 + var imgUrl = '\n![](' + d.data.url + ')\n'; + var input = commentInput; + var start = input.selectionStart; + var end = input.selectionEnd; + input.value = input.value.substring(0, start) + imgUrl + input.value.substring(end); + input.focus(); + input.selectionStart = input.selectionEnd = start + imgUrl.length; + } else { + alert(d.message || '上传失败'); + } + }) + .catch(function() { + uploadLabel.style.pointerEvents = 'auto'; + uploadLabel.style.opacity = '1'; + uploadLabel.innerHTML = ' 图片'; + alert('上传失败'); + }); + + imageUploadInput.value = ''; + }); + } + + // ====== Hash 定位:从消息通知跳转时滚动到评论区 ====== + if (window.location.hash === '#comments') { + setTimeout(function() { + var section = document.getElementById('comments'); + if (section) { + section.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }, 400); + } + // ====== 初始化 ====== loadComments(currentPage); })(); diff --git a/templates/MetaLab-2026/static/css/posts.css b/templates/MetaLab-2026/static/css/posts.css index 5c024b4..1c13ddc 100644 --- a/templates/MetaLab-2026/static/css/posts.css +++ b/templates/MetaLab-2026/static/css/posts.css @@ -756,10 +756,43 @@ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); } -.comment-submit-btn { +.comment-form-actions { + display: flex; + align-items: center; + justify-content: space-between; margin-top: 10px; } +.comment-upload-btn { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 8px 14px; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 13px; + color: #6b7280; + cursor: pointer; + background: #fff; + transition: border-color 0.2s, color 0.2s; +} + +.comment-upload-btn:hover { + border-color: #6366f1; + color: #6366f1; +} + +.comment-upload-preview { + margin-top: 8px; + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.comment-submit-btn { + margin-top: 0; +} + .comment-login-hint { text-align: center; padding: 24px; diff --git a/templates/admin/html/comments/index.html b/templates/admin/html/comments/index.html new file mode 100644 index 0000000..80f2c3b --- /dev/null +++ b/templates/admin/html/comments/index.html @@ -0,0 +1,33 @@ +{{template "admin/layout/base.html" .}} + +
+

评论管理

+
+ +
+ + + +
+ +
+ + + + + + + + + + + + +
ID作者评论内容所属文章时间操作
+
+ +
+ + +{{template "admin/layout/footer.html" .}} + diff --git a/templates/admin/html/layout/base.html b/templates/admin/html/layout/base.html index 7bf1b5a..c50a1df 100644 --- a/templates/admin/html/layout/base.html +++ b/templates/admin/html/layout/base.html @@ -51,6 +51,10 @@ 域能日志 + + + 评论管理 + {{if .CanManageSettings}} diff --git a/templates/admin/static/js/comments.js b/templates/admin/static/js/comments.js new file mode 100644 index 0000000..011d193 --- /dev/null +++ b/templates/admin/static/js/comments.js @@ -0,0 +1,132 @@ +(function() { + 'use strict'; + var currentPage = 1; + var currentKeyword = ''; + var totalPages = 1; + var csrfMeta = document.querySelector('meta[name="csrf-token"]'); + var csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : ''; + + var searchInput = document.getElementById('commentSearch'); + var searchBtn = document.getElementById('commentSearchBtn'); + var clearBtn = document.getElementById('commentClearBtn'); + var tbody = document.getElementById('commentTableBody'); + var pagination = document.getElementById('commentPagination'); + var loading = document.getElementById('commentLoading'); + + function api(url, opts) { + opts = opts || {}; + return fetch(url, opts).then(function(r) { return r.json(); }); + } + + function escapeHtml(text) { + if (!text) return ''; + var d = document.createElement('div'); + d.textContent = text; + return d.innerHTML; + } + + function formatTime(iso) { + var d = new Date(iso); + var pad = function(n) { return n < 10 ? '0' + n : n; }; + return d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()); + } + + function loadComments(page) { + loading.style.display = 'block'; + var url = '/api/admin/comments?page=' + page + '&page_size=20'; + if (currentKeyword) url += '&keyword=' + encodeURIComponent(currentKeyword); + + api(url).then(function(d) { + loading.style.display = 'none'; + if (!d.success) return; + totalPages = d.data.total_pages || 1; + tbody.innerHTML = ''; + var items = d.data.items || []; + if (items.length === 0) { + tbody.innerHTML = '暂无评论'; + pagination.innerHTML = ''; + return; + } + for (var i = 0; i < items.length; i++) { + var c = items[i]; + var row = document.createElement('tr'); + var body = escapeHtml(c.body).substring(0, 80) + (c.body && c.body.length > 80 ? '...' : ''); + row.innerHTML = + '' + c.id + '' + + '' + escapeHtml(c.author_name) + '' + + '' + body + '' + + '' + escapeHtml(c.post_title || '文章#' + c.post_id) + '' + + '' + formatTime(c.created_at) + '' + + ''; + tbody.appendChild(row); + } + renderPagination(page); + bindDeleteBtns(); + }).catch(function() { + loading.style.display = 'none'; + tbody.innerHTML = '加载失败'; + }); + } + + function bindDeleteBtns() { + var btns = tbody.querySelectorAll('.comment-delete-btn'); + for (var i = 0; i < btns.length; i++) { + if (btns[i]._bound) continue; + btns[i]._bound = true; + btns[i].addEventListener('click', function() { + var id = this.dataset.id; + if (!confirm('确认删除此评论?')) return; + fetch('/api/admin/comments/' + id, { + method: 'DELETE', + headers: { 'X-CSRF-Token': csrfToken } + }).then(function(r) { return r.json(); }) + .then(function(d) { + if (d.success) { + loadComments(currentPage); + } else { + alert(d.message || '删除失败'); + } + }).catch(function() { alert('请求失败'); }); + }); + } + } + + function renderPagination(page) { + if (totalPages <= 1) { pagination.innerHTML = ''; return; } + var html = ''; + if (page > 1) html += '上一页'; + html += '' + page + ' / ' + totalPages + ''; + if (page < totalPages) html += '下一页'; + pagination.innerHTML = html; + + var links = pagination.querySelectorAll('.admin-page-link'); + for (var j = 0; j < links.length; j++) { + links[j].addEventListener('click', function() { + currentPage = parseInt(this.dataset.page); + loadComments(currentPage); + }); + } + } + + searchBtn.addEventListener('click', function() { + currentKeyword = searchInput.value.trim(); + currentPage = 1; + if (currentKeyword) { clearBtn.style.display = 'inline-block'; } + else { clearBtn.style.display = 'none'; } + loadComments(currentPage); + }); + + clearBtn.addEventListener('click', function() { + currentKeyword = ''; + currentPage = 1; + searchInput.value = ''; + clearBtn.style.display = 'none'; + loadComments(currentPage); + }); + + searchInput.addEventListener('keydown', function(e) { + if (e.key === 'Enter') searchBtn.click(); + }); + + loadComments(currentPage); +})();