modify the classic board page heavily
parent
95c2fc7c0d
commit
91c7591c19
|
|
@ -1,10 +1,12 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"threadr/models"
|
"threadr/models"
|
||||||
|
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -42,9 +44,12 @@ func LikeHandler(app *App) http.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userAction := likeType // what the user's current vote will be after this
|
||||||
if existingLike != nil {
|
if existingLike != nil {
|
||||||
if existingLike.Type == likeType {
|
if existingLike.Type == likeType {
|
||||||
|
// Toggle off
|
||||||
err = models.DeleteLike(app.DB, postID, userID)
|
err = models.DeleteLike(app.DB, postID, userID)
|
||||||
|
userAction = "" // no active vote
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error deleting like: %v", err)
|
log.Printf("Error deleting like: %v", err)
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
|
@ -72,7 +77,19 @@ func LikeHandler(app *App) http.HandlerFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
// Fetch updated counts
|
||||||
w.Write([]byte("OK"))
|
counts, err := models.GetLikeCountsByPostID(app.DB, postID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error fetching like counts: %v", err)
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"likes": counts.Likes,
|
||||||
|
"dislikes": counts.Dislikes,
|
||||||
|
"userAction": userAction,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -60,7 +60,10 @@ func ThreadHandler(app *App) http.HandlerFunc {
|
||||||
action := r.URL.Query().Get("action")
|
action := r.URL.Query().Get("action")
|
||||||
if action == "submit" {
|
if action == "submit" {
|
||||||
content := r.FormValue("content")
|
content := r.FormValue("content")
|
||||||
replyToStr := r.URL.Query().Get("to")
|
replyToStr := r.FormValue("reply_to")
|
||||||
|
if replyToStr == "" {
|
||||||
|
replyToStr = r.URL.Query().Get("to")
|
||||||
|
}
|
||||||
replyTo := -1
|
replyTo := -1
|
||||||
if replyToStr != "" {
|
if replyToStr != "" {
|
||||||
replyTo, err = strconv.Atoi(replyToStr)
|
replyTo, err = strconv.Atoi(replyToStr)
|
||||||
|
|
@ -109,11 +112,49 @@ func ThreadHandler(app *App) http.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collect post IDs for bulk queries
|
||||||
|
postIDs := make([]int, len(posts))
|
||||||
|
userIDs := make(map[int]bool)
|
||||||
|
for i, p := range posts {
|
||||||
|
postIDs[i] = p.ID
|
||||||
|
userIDs[p.UserID] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch like counts for all posts
|
||||||
|
likeCounts, err := models.GetLikeCountsForPosts(app.DB, postIDs)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error fetching like counts: %v", err)
|
||||||
|
likeCounts = make(map[int]models.LikeCounts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch current user's likes
|
||||||
|
userLikes, err := models.GetUserLikesForPosts(app.DB, userID, postIDs)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error fetching user likes: %v", err)
|
||||||
|
userLikes = make(map[int]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch usernames for post authors
|
||||||
|
usernames := make(map[int]string)
|
||||||
|
for uid := range userIDs {
|
||||||
|
user, err := models.GetUserByID(app.DB, uid)
|
||||||
|
if err != nil || user == nil {
|
||||||
|
usernames[uid] = "Unknown"
|
||||||
|
} else if user.DisplayName != "" {
|
||||||
|
usernames[uid] = user.DisplayName
|
||||||
|
} else {
|
||||||
|
usernames[uid] = user.Username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data := struct {
|
data := struct {
|
||||||
PageData
|
PageData
|
||||||
Thread models.Thread
|
Thread models.Thread
|
||||||
Board models.Board
|
Board models.Board
|
||||||
Posts []models.Post
|
Posts []models.Post
|
||||||
|
LikeCounts map[int]models.LikeCounts
|
||||||
|
UserLikes map[int]string
|
||||||
|
Usernames map[int]string
|
||||||
}{
|
}{
|
||||||
PageData: PageData{
|
PageData: PageData{
|
||||||
Title: "ThreadR - " + thread.Title,
|
Title: "ThreadR - " + thread.Title,
|
||||||
|
|
@ -127,6 +168,9 @@ func ThreadHandler(app *App) http.HandlerFunc {
|
||||||
Thread: *thread,
|
Thread: *thread,
|
||||||
Board: *board,
|
Board: *board,
|
||||||
Posts: posts,
|
Posts: posts,
|
||||||
|
LikeCounts: likeCounts,
|
||||||
|
UserLikes: userLikes,
|
||||||
|
Usernames: usernames,
|
||||||
}
|
}
|
||||||
if err := app.Tmpl.ExecuteTemplate(w, "thread", data); err != nil {
|
if err := app.Tmpl.ExecuteTemplate(w, "thread", data); err != nil {
|
||||||
log.Printf("Error executing template in ThreadHandler: %v", err)
|
log.Printf("Error executing template in ThreadHandler: %v", err)
|
||||||
|
|
|
||||||
|
|
@ -60,3 +60,86 @@ func DeleteLike(db *sql.DB, postID, userID int) error {
|
||||||
_, err := db.Exec(query, postID, userID)
|
_, err := db.Exec(query, postID, userID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LikeCounts struct {
|
||||||
|
Likes int
|
||||||
|
Dislikes int
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLikeCountsByPostID(db *sql.DB, postID int) (LikeCounts, error) {
|
||||||
|
counts := LikeCounts{}
|
||||||
|
query := "SELECT COALESCE(SUM(CASE WHEN type='like' THEN 1 ELSE 0 END),0), COALESCE(SUM(CASE WHEN type='dislike' THEN 1 ELSE 0 END),0) FROM likes WHERE post_id = ?"
|
||||||
|
err := db.QueryRow(query, postID).Scan(&counts.Likes, &counts.Dislikes)
|
||||||
|
return counts, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLikeCountsForPosts(db *sql.DB, postIDs []int) (map[int]LikeCounts, error) {
|
||||||
|
result := make(map[int]LikeCounts)
|
||||||
|
if len(postIDs) == 0 {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
// Build placeholders
|
||||||
|
placeholders := ""
|
||||||
|
args := make([]interface{}, len(postIDs))
|
||||||
|
for i, id := range postIDs {
|
||||||
|
if i > 0 {
|
||||||
|
placeholders += ","
|
||||||
|
}
|
||||||
|
placeholders += "?"
|
||||||
|
args[i] = id
|
||||||
|
}
|
||||||
|
query := "SELECT post_id, type, COUNT(*) FROM likes WHERE post_id IN (" + placeholders + ") GROUP BY post_id, type"
|
||||||
|
rows, err := db.Query(query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var postID int
|
||||||
|
var likeType string
|
||||||
|
var count int
|
||||||
|
if err := rows.Scan(&postID, &likeType, &count); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c := result[postID]
|
||||||
|
if likeType == "like" {
|
||||||
|
c.Likes = count
|
||||||
|
} else {
|
||||||
|
c.Dislikes = count
|
||||||
|
}
|
||||||
|
result[postID] = c
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserLikesForPosts(db *sql.DB, userID int, postIDs []int) (map[int]string, error) {
|
||||||
|
result := make(map[int]string)
|
||||||
|
if len(postIDs) == 0 || userID == 0 {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
placeholders := ""
|
||||||
|
args := make([]interface{}, 0, len(postIDs)+1)
|
||||||
|
args = append(args, userID)
|
||||||
|
for i, id := range postIDs {
|
||||||
|
if i > 0 {
|
||||||
|
placeholders += ","
|
||||||
|
}
|
||||||
|
placeholders += "?"
|
||||||
|
args = append(args, id)
|
||||||
|
}
|
||||||
|
query := "SELECT post_id, type FROM likes WHERE user_id = ? AND post_id IN (" + placeholders + ")"
|
||||||
|
rows, err := db.Query(query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var postID int
|
||||||
|
var likeType string
|
||||||
|
if err := rows.Scan(&postID, &likeType); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result[postID] = likeType
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,51 @@
|
||||||
function initLikeButtons() {
|
function initLikeButtons() {
|
||||||
document.querySelectorAll('form[action*="/like/"]').forEach(form => {
|
document.querySelectorAll('.like-btn, .dislike-btn').forEach(function(btn) {
|
||||||
form.addEventListener('submit', () => {
|
btn.addEventListener('click', function(e) {
|
||||||
const button = form.querySelector('button[type="submit"]');
|
e.preventDefault();
|
||||||
if (button) {
|
var postId = btn.getAttribute('data-post-id');
|
||||||
button.style.opacity = '0.5';
|
var type = btn.getAttribute('data-type');
|
||||||
button.textContent = button.textContent + ' ✓';
|
var basePath = btn.getAttribute('data-base-path');
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
var body = new URLSearchParams();
|
||||||
|
body.append('post_id', postId);
|
||||||
|
body.append('type', type);
|
||||||
|
|
||||||
|
fetch(basePath + '/like/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: body.toString()
|
||||||
|
})
|
||||||
|
.then(function(res) { return res.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
// Find sibling buttons for this post
|
||||||
|
var article = btn.closest('.post-actions');
|
||||||
|
var likeBtn = article.querySelector('.like-btn');
|
||||||
|
var dislikeBtn = article.querySelector('.dislike-btn');
|
||||||
|
|
||||||
|
// Update counts
|
||||||
|
if (likeBtn) {
|
||||||
|
likeBtn.querySelector('.like-count').textContent = data.likes;
|
||||||
}
|
}
|
||||||
|
if (dislikeBtn) {
|
||||||
|
dislikeBtn.querySelector('.dislike-count').textContent = data.dislikes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update active states
|
||||||
|
if (likeBtn) {
|
||||||
|
likeBtn.classList.toggle('active', data.userAction === 'like');
|
||||||
|
}
|
||||||
|
if (dislikeBtn) {
|
||||||
|
dislikeBtn.classList.toggle('active', data.userAction === 'dislike');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
console.error('Like error:', err);
|
||||||
|
})
|
||||||
|
.finally(function() {
|
||||||
|
btn.disabled = false;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
139
static/style.css
139
static/style.css
|
|
@ -333,21 +333,120 @@ p.thread-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-actions a {
|
.post-action-btn {
|
||||||
color: #001858;
|
display: inline-flex;
|
||||||
text-decoration: none;
|
align-items: center;
|
||||||
font-size: 1em;
|
gap: 0.3em;
|
||||||
padding: 0.4em 0.8em;
|
padding: 0.4em 0.8em;
|
||||||
border: 1px solid #001858;
|
border: 1px solid #001858;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: background-color 0.2s ease;
|
background-color: #fef6e4;
|
||||||
|
color: #001858;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin: 0;
|
||||||
|
width: auto;
|
||||||
|
transition: background-color 0.2s ease, transform 0.1s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-actions a:hover {
|
.post-action-btn:hover {
|
||||||
background-color: #8bd3dd;
|
background-color: #8bd3dd;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-action-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-action-btn.like-btn.active {
|
||||||
|
background-color: #8bd3dd;
|
||||||
|
border-color: #001858;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-action-btn.dislike-btn.active {
|
||||||
|
background-color: #f582ae;
|
||||||
|
border-color: #001858;
|
||||||
color: #fef6e4;
|
color: #fef6e4;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.like-count-display, .dislike-count-display {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #001858;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-header-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-id {
|
||||||
|
opacity: 0.6;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-reply-link a {
|
||||||
|
color: #8bd3dd;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-reply-link a:hover {
|
||||||
|
color: #f582ae;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-reply {
|
||||||
|
margin-left: 20px;
|
||||||
|
border-left: 3px solid #8bd3dd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-highlighted {
|
||||||
|
animation: post-highlight-fade 2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes post-highlight-fade {
|
||||||
|
0% { box-shadow: 0 0 0 3px #f582ae; }
|
||||||
|
100% { box-shadow: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#reply-section .reply-indicator {
|
||||||
|
background-color: #001858;
|
||||||
|
color: #fef6e4;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
#reply-section .reply-indicator span {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #fef6e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
#reply-section .reply-indicator button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #fef6e4;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1em;
|
||||||
|
padding: 0 6px;
|
||||||
|
margin: 0;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#reply-section .reply-indicator button:hover {
|
||||||
|
background: none;
|
||||||
|
color: #f582ae;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* New style for highlighted chat messages */
|
/* New style for highlighted chat messages */
|
||||||
|
|
@ -781,14 +880,38 @@ p.thread-info {
|
||||||
.post-content {
|
.post-content {
|
||||||
color: #fef6e4;
|
color: #fef6e4;
|
||||||
}
|
}
|
||||||
.post-actions a {
|
.post-action-btn {
|
||||||
|
background-color: #555;
|
||||||
color: #fef6e4;
|
color: #fef6e4;
|
||||||
border-color: #fef6e4;
|
border-color: #fef6e4;
|
||||||
}
|
}
|
||||||
.post-actions a:hover {
|
.post-action-btn:hover {
|
||||||
background-color: #8bd3dd;
|
background-color: #8bd3dd;
|
||||||
color: #001858;
|
color: #001858;
|
||||||
}
|
}
|
||||||
|
.post-action-btn.like-btn.active {
|
||||||
|
background-color: #8bd3dd;
|
||||||
|
color: #001858;
|
||||||
|
}
|
||||||
|
.post-action-btn.dislike-btn.active {
|
||||||
|
background-color: #f582ae;
|
||||||
|
color: #001858;
|
||||||
|
}
|
||||||
|
.like-count-display, .dislike-count-display {
|
||||||
|
color: #fef6e4;
|
||||||
|
}
|
||||||
|
.post-reply {
|
||||||
|
border-left-color: #8bd3dd;
|
||||||
|
}
|
||||||
|
.post-reply-link a {
|
||||||
|
color: #8bd3dd;
|
||||||
|
}
|
||||||
|
.post-id {
|
||||||
|
color: #fef6e4;
|
||||||
|
}
|
||||||
|
#reply-section .reply-indicator {
|
||||||
|
background-color: #222;
|
||||||
|
}
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
color: #fef6e4;
|
color: #fef6e4;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
<head>
|
<head>
|
||||||
<title>{{.Title}}</title>
|
<title>{{.Title}}</title>
|
||||||
<link rel="stylesheet" href="{{.StaticPath}}/style.css">
|
<link rel="stylesheet" href="{{.StaticPath}}/style.css">
|
||||||
|
<script src="{{.StaticPath}}/likes.js" defer></script>
|
||||||
<script src="{{.StaticPath}}/app.js" defer></script>
|
<script src="{{.StaticPath}}/app.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -11,11 +12,11 @@
|
||||||
<main>
|
<main>
|
||||||
<div class="breadcrumb">
|
<div class="breadcrumb">
|
||||||
<a href="{{.BasePath}}/">Home</a>
|
<a href="{{.BasePath}}/">Home</a>
|
||||||
<span class="breadcrumb-separator">›</span>
|
<span class="breadcrumb-separator">></span>
|
||||||
<a href="{{.BasePath}}/boards/">Boards</a>
|
<a href="{{.BasePath}}/boards/">Boards</a>
|
||||||
<span class="breadcrumb-separator">›</span>
|
<span class="breadcrumb-separator">></span>
|
||||||
<a href="{{.BasePath}}/board/?id={{.Board.ID}}">{{.Board.Name}}</a>
|
<a href="{{.BasePath}}/board/?id={{.Board.ID}}">{{.Board.Name}}</a>
|
||||||
<span class="breadcrumb-separator">›</span>
|
<span class="breadcrumb-separator">></span>
|
||||||
<span class="breadcrumb-current">{{.Thread.Title}}</span>
|
<span class="breadcrumb-current">{{.Thread.Title}}</span>
|
||||||
</div>
|
</div>
|
||||||
<a href="{{.BasePath}}/board/?id={{.Board.ID}}" class="back-button">Back to {{.Board.Name}}</a>
|
<a href="{{.BasePath}}/board/?id={{.Board.ID}}" class="back-button">Back to {{.Board.Name}}</a>
|
||||||
|
|
@ -24,37 +25,44 @@
|
||||||
</header>
|
</header>
|
||||||
<div class="thread-posts">
|
<div class="thread-posts">
|
||||||
{{range .Posts}}
|
{{range .Posts}}
|
||||||
<article id="{{.ID}}" class="post-item" style="margin-left: {{if gt .ReplyTo 0}}20px{{else}}0px{{end}};">
|
<article id="post-{{.ID}}" class="post-item{{if gt .ReplyTo 0}} post-reply{{end}}">
|
||||||
<header>
|
<header>
|
||||||
<h3>{{if .Title}}{{.Title}}{{else}}Post #{{.ID}}{{end}}</h3>
|
<div class="post-header-top">
|
||||||
<p data-timestamp="{{.PostTime.Format "2006-01-02T15:04:05Z07:00"}}">Posted on {{.PostTime.Format "02/01/2006 - 15:04"}}</p>
|
<h3>{{if .Title}}{{.Title}}{{else}}{{index $.Usernames .UserID}}{{end}}</h3>
|
||||||
|
<span class="post-id">#{{.ID}}</span>
|
||||||
|
</div>
|
||||||
|
<p data-timestamp="{{.PostTime.Format "2006-01-02T15:04:05Z07:00"}}">{{.PostTime.Format "02/01/2006 - 15:04"}}</p>
|
||||||
{{if gt .ReplyTo 0}}
|
{{if gt .ReplyTo 0}}
|
||||||
<p>Reply to post <a href="#{{.ReplyTo}}">{{.ReplyTo}}</a></p>
|
<p class="post-reply-link">Replying to <a href="#post-{{.ReplyTo}}">#{{.ReplyTo}}</a></p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</header>
|
</header>
|
||||||
<div class="post-content">{{.Content}}</div>
|
<div class="post-content">{{.Content}}</div>
|
||||||
{{if $.LoggedIn}}
|
|
||||||
<div class="post-actions">
|
<div class="post-actions">
|
||||||
<form method="post" action="{{$.BasePath}}/like/" style="display:inline;">
|
{{if $.LoggedIn}}
|
||||||
<input type="hidden" name="post_id" value="{{.ID}}">
|
<button type="button" class="post-action-btn like-btn{{if eq (index $.UserLikes .ID) "like"}} active{{end}}" data-post-id="{{.ID}}" data-type="like" data-base-path="{{$.BasePath}}">
|
||||||
<input type="hidden" name="type" value="like">
|
<span class="like-count">{{(index $.LikeCounts .ID).Likes}}</span> Like
|
||||||
<button type="submit">Like</button>
|
</button>
|
||||||
</form>
|
<button type="button" class="post-action-btn dislike-btn{{if eq (index $.UserLikes .ID) "dislike"}} active{{end}}" data-post-id="{{.ID}}" data-type="dislike" data-base-path="{{$.BasePath}}">
|
||||||
<form method="post" action="{{$.BasePath}}/like/" style="display:inline;">
|
<span class="dislike-count">{{(index $.LikeCounts .ID).Dislikes}}</span> Dislike
|
||||||
<input type="hidden" name="post_id" value="{{.ID}}">
|
</button>
|
||||||
<input type="hidden" name="type" value="dislike">
|
<button type="button" class="post-action-btn reply-btn" onclick="setReplyTo({{.ID}}, '{{index $.Usernames .UserID}}')">Reply</button>
|
||||||
<button type="submit">Dislike</button>
|
{{else}}
|
||||||
</form>
|
<span class="like-count-display">{{(index $.LikeCounts .ID).Likes}} Likes</span>
|
||||||
<a href="{{$.BasePath}}/thread/?id={{$.Thread.ID}}&action=submit&to={{.ID}}">Reply</a>
|
<span class="dislike-count-display">{{(index $.LikeCounts .ID).Dislikes}} Dislikes</span>
|
||||||
</div>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{if .LoggedIn}}
|
{{if .LoggedIn}}
|
||||||
<section>
|
<section id="reply-section">
|
||||||
<h3>Post a Message</h3>
|
<h3 id="reply-heading">Post a Reply</h3>
|
||||||
<form method="post" action="{{.BasePath}}/thread/?id={{.Thread.ID}}&action=submit">
|
<div id="reply-indicator" class="reply-indicator" style="display:none;">
|
||||||
|
<span id="reply-indicator-text">Replying to...</span>
|
||||||
|
<button type="button" onclick="clearReply()">x</button>
|
||||||
|
</div>
|
||||||
|
<form method="post" action="{{.BasePath}}/thread/?id={{.Thread.ID}}&action=submit" id="reply-form">
|
||||||
|
<input type="hidden" id="reply-to-input" name="reply_to" value="">
|
||||||
<label for="content">Content:</label>
|
<label for="content">Content:</label>
|
||||||
<textarea id="content" name="content" required></textarea><br>
|
<textarea id="content" name="content" required></textarea><br>
|
||||||
<input type="submit" value="Post">
|
<input type="submit" value="Post">
|
||||||
|
|
@ -63,6 +71,37 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</main>
|
</main>
|
||||||
{{template "cookie_banner" .}}
|
{{template "cookie_banner" .}}
|
||||||
|
<script>
|
||||||
|
function setReplyTo(postId, username) {
|
||||||
|
document.getElementById('reply-to-input').value = postId;
|
||||||
|
var indicator = document.getElementById('reply-indicator');
|
||||||
|
indicator.style.display = 'flex';
|
||||||
|
document.getElementById('reply-indicator-text').textContent = 'Replying to ' + username + ' (post #' + postId + ')';
|
||||||
|
document.getElementById('reply-heading').textContent = 'Reply to ' + username;
|
||||||
|
var section = document.getElementById('reply-section');
|
||||||
|
section.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
document.getElementById('content').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearReply() {
|
||||||
|
document.getElementById('reply-to-input').value = '';
|
||||||
|
document.getElementById('reply-indicator').style.display = 'none';
|
||||||
|
document.getElementById('reply-heading').textContent = 'Post a Reply';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smooth scroll to replied post when clicking reply links
|
||||||
|
document.querySelectorAll('.post-reply-link a').forEach(function(link) {
|
||||||
|
link.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var target = document.querySelector(this.getAttribute('href'));
|
||||||
|
if (target) {
|
||||||
|
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
target.classList.add('post-highlighted');
|
||||||
|
setTimeout(function() { target.classList.remove('post-highlighted'); }, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
{{end}}
|
{{end}}
|
||||||
Loading…
Reference in New Issue