First comment function implementation
This commit is contained in:
parent
e3724a4842
commit
241c962faf
@ -1,9 +1,10 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from flask import Flask, request, jsonify, session
|
from flask import Flask, request, jsonify, session
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.secret_key = "your-secret-key-here-change-in-production" # needed for sessions
|
app.secret_key = "your-secret-key-here-change-in-production" # needed for sessions
|
||||||
@ -333,6 +334,7 @@ def create_blog_post():
|
|||||||
"content": data.get("content", ""),
|
"content": data.get("content", ""),
|
||||||
"journeyId": data.get("journeyId"),
|
"journeyId": data.get("journeyId"),
|
||||||
"image": data.get("image"),
|
"image": data.get("image"),
|
||||||
|
"author_id": user_id,
|
||||||
"created_at": datetime.now().isoformat(),
|
"created_at": datetime.now().isoformat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -351,6 +353,8 @@ def update_blog_post(post_id):
|
|||||||
return jsonify({"error": "Post not found"}), 404
|
return jsonify({"error": "Post not found"}), 404
|
||||||
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
if not get_current_user_id() == data["author_id"]:
|
||||||
|
return jsonify({"error": "Wrong user"})
|
||||||
if "title" in data:
|
if "title" in data:
|
||||||
post["title"] = data["title"]
|
post["title"] = data["title"]
|
||||||
if "content" in data:
|
if "content" in data:
|
||||||
@ -378,6 +382,77 @@ def delete_blog_post(post_id):
|
|||||||
return jsonify({"message": "Post deleted"})
|
return jsonify({"message": "Post deleted"})
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== Comments (stored inside posts) ====================
|
||||||
|
def get_post_by_id(user_id, post_id):
|
||||||
|
posts = load_user_posts(user_id)
|
||||||
|
return next((p for p in posts if p['id'] == post_id), None)
|
||||||
|
|
||||||
|
def save_post(user_id, post):
|
||||||
|
posts = load_user_posts(user_id)
|
||||||
|
for i, p in enumerate(posts):
|
||||||
|
if p['id'] == post['id']:
|
||||||
|
posts[i] = post
|
||||||
|
break
|
||||||
|
save_user_posts(user_id, posts)
|
||||||
|
|
||||||
|
@app.route('/api/posts/<int:post_id>/comments', methods=['GET'])
|
||||||
|
def get_comments(post_id):
|
||||||
|
user_id = session.get('user_id')
|
||||||
|
if not user_id:
|
||||||
|
return jsonify({'error': 'Authentication required'}), 401
|
||||||
|
post = get_post_by_id(user_id, post_id)
|
||||||
|
if not post:
|
||||||
|
return jsonify({'error': 'Post not found'}), 404
|
||||||
|
return jsonify(post.get('comments', []))
|
||||||
|
|
||||||
|
@app.route('/api/posts/<int:post_id>/comments', methods=['POST'])
|
||||||
|
def add_comment(post_id):
|
||||||
|
user_id = session.get('user_id')
|
||||||
|
if not user_id:
|
||||||
|
return jsonify({'error': 'Authentication required'}), 401
|
||||||
|
data = request.get_json()
|
||||||
|
text = data.get('text')
|
||||||
|
if not text:
|
||||||
|
return jsonify({'error': 'Comment text required'}), 400
|
||||||
|
|
||||||
|
post = get_post_by_id(user_id, post_id)
|
||||||
|
if not post:
|
||||||
|
return jsonify({'error': 'Post not found'}), 404
|
||||||
|
|
||||||
|
comment = {
|
||||||
|
'id': int(time.time() * 1000), # simple unique id
|
||||||
|
'author_id': user_id,
|
||||||
|
'author_name': get_user_by_id(user_id)['username'],
|
||||||
|
'text': text,
|
||||||
|
'created_at': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
if 'comments' not in post:
|
||||||
|
post['comments'] = []
|
||||||
|
post['comments'].append(comment)
|
||||||
|
save_post(user_id, post)
|
||||||
|
return jsonify(comment), 201
|
||||||
|
|
||||||
|
@app.route('/api/comments/<int:comment_id>', methods=['DELETE'])
|
||||||
|
def delete_comment(comment_id):
|
||||||
|
user_id = session.get('user_id')
|
||||||
|
if not user_id:
|
||||||
|
return jsonify({'error': 'Authentication required'}), 401
|
||||||
|
|
||||||
|
# Find which post contains this comment
|
||||||
|
posts = load_user_posts(user_id)
|
||||||
|
for post in posts:
|
||||||
|
if 'comments' in post:
|
||||||
|
for i, c in enumerate(post['comments']):
|
||||||
|
if c['id'] == comment_id:
|
||||||
|
# Allow deletion if current user is comment author or post author
|
||||||
|
if c['author_id'] == user_id or post['id'] == post.get('author_id', user_id):
|
||||||
|
del post['comments'][i]
|
||||||
|
save_post(user_id, post)
|
||||||
|
return jsonify({'message': 'Comment deleted'})
|
||||||
|
else:
|
||||||
|
return jsonify({'error': 'Not authorized'}), 403
|
||||||
|
return jsonify({'error': 'Comment not found'}), 404
|
||||||
|
|
||||||
# ==================== Health and root ====================
|
# ==================== Health and root ====================
|
||||||
@app.route("/api/journeys/health", methods=["GET"])
|
@app.route("/api/journeys/health", methods=["GET"])
|
||||||
def health_check():
|
def health_check():
|
||||||
|
|||||||
@ -4,5 +4,11 @@
|
|||||||
"username": "josh",
|
"username": "josh",
|
||||||
"password_hash": "scrypt:32768:8:1$HA70PiOwbBrIwlDq$2ab80bdc08bb3bb4214258566aded836062323380491a7f4c7f2e67bdccb8686367789f57b3c6c5eb3e2f08c8c07186f47f9c89d1e72179ddd3758b509f23fbe",
|
"password_hash": "scrypt:32768:8:1$HA70PiOwbBrIwlDq$2ab80bdc08bb3bb4214258566aded836062323380491a7f4c7f2e67bdccb8686367789f57b3c6c5eb3e2f08c8c07186f47f9c89d1e72179ddd3758b509f23fbe",
|
||||||
"created_at": "2026-03-27T20:32:43.107028"
|
"created_at": "2026-03-27T20:32:43.107028"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"username": "test1",
|
||||||
|
"password_hash": "scrypt:32768:8:1$hPfITQadZq8438bv$38262bf82d93c596a82a1b052a4ba72f8d6729b796ca5273faa7dd47b409112959c4501e77922605a1f3a7ef08e68fa545ce03818eb82e6fb2503cc817c43e2a",
|
||||||
|
"created_at": "2026-03-28T14:13:32.860143"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -5,6 +5,15 @@
|
|||||||
"content": "ksafladjsfk",
|
"content": "ksafladjsfk",
|
||||||
"journeyId": "1",
|
"journeyId": "1",
|
||||||
"image": null,
|
"image": null,
|
||||||
"created_at": "2026-03-27T21:23:39.755057"
|
"created_at": "2026-03-27T21:23:39.755057",
|
||||||
|
"comments": [
|
||||||
|
{
|
||||||
|
"id": 1774703592361,
|
||||||
|
"author_id": 1,
|
||||||
|
"author_name": "josh",
|
||||||
|
"text": "hello",
|
||||||
|
"created_at": "2026-03-28T14:13:12.362078"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
1
backend/data/users/2/journeys.json
Normal file
1
backend/data/users/2/journeys.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
1
backend/data/users/2/posts.json
Normal file
1
backend/data/users/2/posts.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
@ -212,7 +212,7 @@
|
|||||||
<main class="blog-container">
|
<main class="blog-container">
|
||||||
<div class="blog-header">
|
<div class="blog-header">
|
||||||
<h1><i class="fas fa-newspaper"></i> Blog Posts</h1>
|
<h1><i class="fas fa-newspaper"></i> Blog Posts</h1>
|
||||||
<a href="blog-post-edit.html?new" class="btn"><i class="fas fa-plus"></i> New Post</a>
|
<a href="blog-post.html?new" class="btn"><i class="fas fa-plus"></i> New Post</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="posts-grid" class="posts-grid">
|
<div id="posts-grid" class="posts-grid">
|
||||||
<!-- Posts loaded dynamically -->
|
<!-- Posts loaded dynamically -->
|
||||||
@ -247,7 +247,7 @@
|
|||||||
<article class="post-card">
|
<article class="post-card">
|
||||||
${post.image ? `<img class="post-card-image" src="${post.image}" alt="${post.title}">` : '<div class="post-card-image" style="background: var(--surface-3); display: flex; align-items: center; justify-content: center;"><i class="fas fa-image" style="font-size: 3rem; color: var(--gray-5);"></i></div>'}
|
${post.image ? `<img class="post-card-image" src="${post.image}" alt="${post.title}">` : '<div class="post-card-image" style="background: var(--surface-3); display: flex; align-items: center; justify-content: center;"><i class="fas fa-image" style="font-size: 3rem; color: var(--gray-5);"></i></div>'}
|
||||||
<div class="post-card-content">
|
<div class="post-card-content">
|
||||||
<h2 class="post-card-title"><a href="blog-post-edit.html?id=${post.id}">${escapeHtml(post.title)}</a></h2>
|
<h2 class="post-card-title"><a href="blog-post.html?id=${post.id}">${escapeHtml(post.title)}</a></h2>
|
||||||
<div class="post-card-meta">
|
<div class="post-card-meta">
|
||||||
<i class="fas fa-calendar-alt"></i> ${new Date(post.created_at).toLocaleDateString()}
|
<i class="fas fa-calendar-alt"></i> ${new Date(post.created_at).toLocaleDateString()}
|
||||||
${post.journeyId ? `<span style="margin-left: 12px;"><i class="fas fa-route"></i> Journey #${post.journeyId}</span>` : ''}
|
${post.journeyId ? `<span style="margin-left: 12px;"><i class="fas fa-route"></i> Journey #${post.journeyId}</span>` : ''}
|
||||||
|
|||||||
403
blog-post.html
Normal file
403
blog-post.html
Normal file
@ -0,0 +1,403 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Blog Post – Journey Mapper</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/open-props"/>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/open-props/normalize.min.css"/>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
background: var(--gray-0);
|
||||||
|
color: var(--gray-9);
|
||||||
|
line-height: var(--font-lineheight-3);
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.site-header {
|
||||||
|
background: var(--gray-9);
|
||||||
|
padding: var(--size-4) var(--size-6);
|
||||||
|
border-bottom: 1px solid var(--surface-4);
|
||||||
|
}
|
||||||
|
.site-header .container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--size-2);
|
||||||
|
}
|
||||||
|
.site-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--font-size-4);
|
||||||
|
font-weight: var(--font-weight-6);
|
||||||
|
}
|
||||||
|
.site-title a {
|
||||||
|
color: var(--indigo-4);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.site-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--size-4);
|
||||||
|
}
|
||||||
|
.site-nav a {
|
||||||
|
color: var(--gray-2);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: var(--font-weight-5);
|
||||||
|
transition: color 0.2s;
|
||||||
|
padding: var(--size-1) var(--size-2);
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
}
|
||||||
|
.site-nav a:hover,
|
||||||
|
.site-nav a.active {
|
||||||
|
color: var(--indigo-4);
|
||||||
|
background: var(--surface-2);
|
||||||
|
}
|
||||||
|
.user-menu {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--size-2);
|
||||||
|
}
|
||||||
|
.user-menu .username {
|
||||||
|
color: var(--gray-2);
|
||||||
|
font-weight: var(--font-weight-5);
|
||||||
|
}
|
||||||
|
.logout-btn {
|
||||||
|
background: var(--indigo-7);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
padding: var(--size-1) var(--size-3);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--font-size-1);
|
||||||
|
font-weight: var(--font-weight-5);
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.logout-btn:hover {
|
||||||
|
background: var(--indigo-8);
|
||||||
|
}
|
||||||
|
.post-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: var(--size-6) auto;
|
||||||
|
padding: 0 var(--size-4);
|
||||||
|
}
|
||||||
|
.post-card {
|
||||||
|
background: var(--surface-1);
|
||||||
|
border-radius: var(--radius-3);
|
||||||
|
padding: var(--size-6);
|
||||||
|
box-shadow: var(--shadow-2);
|
||||||
|
margin-bottom: var(--size-6);
|
||||||
|
}
|
||||||
|
.post-title {
|
||||||
|
font-size: var(--font-size-6);
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: var(--size-2);
|
||||||
|
}
|
||||||
|
.post-meta {
|
||||||
|
color: var(--gray-6);
|
||||||
|
margin-bottom: var(--size-4);
|
||||||
|
border-bottom: 1px solid var(--surface-4);
|
||||||
|
padding-bottom: var(--size-2);
|
||||||
|
}
|
||||||
|
.post-image {
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
margin: var(--size-4) 0;
|
||||||
|
}
|
||||||
|
.post-content {
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.comments-section {
|
||||||
|
background: var(--surface-1);
|
||||||
|
border-radius: var(--radius-3);
|
||||||
|
padding: var(--size-6);
|
||||||
|
box-shadow: var(--shadow-2);
|
||||||
|
}
|
||||||
|
.comment {
|
||||||
|
border-bottom: 1px solid var(--surface-4);
|
||||||
|
padding: var(--size-4) 0;
|
||||||
|
}
|
||||||
|
.comment:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.comment-meta {
|
||||||
|
font-size: var(--font-size-1);
|
||||||
|
color: var(--gray-6);
|
||||||
|
margin-bottom: var(--size-2);
|
||||||
|
}
|
||||||
|
.comment-text {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.delete-comment {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--red-6);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--font-size-1);
|
||||||
|
margin-left: var(--size-2);
|
||||||
|
}
|
||||||
|
.delete-comment:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.comment-form {
|
||||||
|
margin-top: var(--size-4);
|
||||||
|
}
|
||||||
|
.comment-form textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--size-2);
|
||||||
|
border: 1px solid var(--surface-4);
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
background: var(--surface-2);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: var(--font-size-2);
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: var(--size-2) var(--size-4);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--font-size-2);
|
||||||
|
font-weight: var(--font-weight-5);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--size-2);
|
||||||
|
transition: all 0.2s var(--ease-2);
|
||||||
|
box-shadow: var(--shadow-2);
|
||||||
|
background: var(--indigo-7);
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--gray-7);
|
||||||
|
}
|
||||||
|
.btn-danger {
|
||||||
|
background: var(--red-7);
|
||||||
|
}
|
||||||
|
.btn-sm {
|
||||||
|
padding: var(--size-1) var(--size-2);
|
||||||
|
font-size: var(--font-size-1);
|
||||||
|
}
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: var(--size-4);
|
||||||
|
right: var(--size-4);
|
||||||
|
background: var(--green-7);
|
||||||
|
color: white;
|
||||||
|
padding: var(--size-2) var(--size-4);
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
display: none;
|
||||||
|
z-index: 1100;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="site-header">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="site-title"><a href="map-page.html">Journey Mapper</a></h1>
|
||||||
|
<div style="display: flex; align-items: center; gap: var(--size-4);">
|
||||||
|
<nav class="site-nav">
|
||||||
|
<a href="map-page.html">Map</a>
|
||||||
|
<a href="blog-list.html">Blog</a>
|
||||||
|
</nav>
|
||||||
|
<div class="user-menu" id="user-menu"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="post-container">
|
||||||
|
<div id="post-content"></div>
|
||||||
|
<div id="comments-section" class="comments-section"></div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<div id="toast" class="toast"></div>
|
||||||
|
|
||||||
|
<script src="js/auth.js"></script>
|
||||||
|
<script>
|
||||||
|
// ==================== GLOBALS ====================
|
||||||
|
let currentPost = null;
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const postId = urlParams.get('id');
|
||||||
|
|
||||||
|
// ==================== LOAD POST ====================
|
||||||
|
async function loadPost() {
|
||||||
|
if (!postId) {
|
||||||
|
window.location.href = 'blog-list.html';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/blog-posts/${postId}`, { credentials: 'include' });
|
||||||
|
if (!res.ok) throw new Error('Post not found');
|
||||||
|
currentPost = await res.json();
|
||||||
|
renderPost();
|
||||||
|
loadComments();
|
||||||
|
} catch (err) {
|
||||||
|
showToast('Error loading post', true);
|
||||||
|
setTimeout(() => window.location.href = 'blog-list.html', 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPost() {
|
||||||
|
const container = document.getElementById('post-content');
|
||||||
|
const isAuthor = currentUser && currentUser.id === currentPost.author_id; // we need to store author_id in post
|
||||||
|
// For now we assume post.author_id is stored (we should add it when creating post).
|
||||||
|
// If not, you can compare with journey owner later.
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<article class="post-card">
|
||||||
|
<h1 class="post-title">${escapeHtml(currentPost.title)}</h1>
|
||||||
|
<div class="post-meta">
|
||||||
|
<i class="fas fa-calendar-alt"></i> ${new Date(currentPost.created_at).toLocaleDateString()}
|
||||||
|
${currentPost.journeyId ? `<span style="margin-left: 12px;"><i class="fas fa-route"></i> <a href="map-page.html?journey=${currentPost.journeyId}" style="color: var(--indigo-7);">View Journey</a></span>` : ''}
|
||||||
|
</div>
|
||||||
|
${currentPost.image ? `<img class="post-image" src="${currentPost.image}" alt="${currentPost.title}">` : ''}
|
||||||
|
<div class="post-content">${formatContent(currentPost.content)}</div>
|
||||||
|
${isAuthor ? `
|
||||||
|
<div style="margin-top: var(--size-4); display: flex; gap: var(--size-2);">
|
||||||
|
<a href="blog-post-edit.html?id=${currentPost.id}" class="btn btn-sm"><i class="fas fa-edit"></i> Edit</a>
|
||||||
|
<button id="delete-post-btn" class="btn btn-danger btn-sm"><i class="fas fa-trash"></i> Delete</button>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</article>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (isAuthor) {
|
||||||
|
document.getElementById('delete-post-btn')?.addEventListener('click', deletePost);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatContent(text) {
|
||||||
|
// Simple markdown-like: convert newlines to <br>
|
||||||
|
return escapeHtml(text).replace(/\n/g, '<br>');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deletePost() {
|
||||||
|
if (!confirm('Delete this post permanently?')) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/blog-posts/${currentPost.id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Delete failed');
|
||||||
|
showToast('Post deleted');
|
||||||
|
setTimeout(() => window.location.href = 'blog-list.html', 1000);
|
||||||
|
} catch (err) {
|
||||||
|
showToast('Error deleting post', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== COMMENTS ====================
|
||||||
|
async function loadComments() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/posts/${postId}/comments`, { credentials: 'include' });
|
||||||
|
if (!res.ok) throw new Error('Failed to load comments');
|
||||||
|
const comments = await res.json();
|
||||||
|
renderComments(comments);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderComments(comments) {
|
||||||
|
const container = document.getElementById('comments-section');
|
||||||
|
if (!comments.length) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<h3><i class="fas fa-comments"></i> Comments</h3>
|
||||||
|
<p class="empty-state">No comments yet. Be the first to comment!</p>
|
||||||
|
${currentUser ? getCommentFormHtml() : '<p><a href="login.html">Login</a> to comment.</p>'}
|
||||||
|
`;
|
||||||
|
if (currentUser) attachCommentForm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let commentsHtml = '<h3><i class="fas fa-comments"></i> Comments</h3>';
|
||||||
|
comments.forEach(comment => {
|
||||||
|
const isOwner = currentUser && (currentUser.id === comment.author_id || (currentPost && currentPost.author_id === currentUser.id));
|
||||||
|
commentsHtml += `
|
||||||
|
<div class="comment" data-comment-id="${comment.id}">
|
||||||
|
<div class="comment-meta">
|
||||||
|
<strong>${escapeHtml(comment.author_name)}</strong> – ${new Date(comment.created_at).toLocaleString()}
|
||||||
|
${isOwner ? `<button class="delete-comment" data-id="${comment.id}"><i class="fas fa-trash-alt"></i> Delete</button>` : ''}
|
||||||
|
</div>
|
||||||
|
<p class="comment-text">${escapeHtml(comment.text)}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
container.innerHTML = commentsHtml + (currentUser ? getCommentFormHtml() : '<p><a href="login.html">Login</a> to comment.</p>');
|
||||||
|
|
||||||
|
// Attach delete handlers
|
||||||
|
document.querySelectorAll('.delete-comment').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => deleteComment(parseInt(btn.dataset.id)));
|
||||||
|
});
|
||||||
|
if (currentUser) attachCommentForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCommentFormHtml() {
|
||||||
|
return `
|
||||||
|
<div class="comment-form">
|
||||||
|
<textarea id="comment-text" rows="3" placeholder="Write a comment..."></textarea>
|
||||||
|
<button id="submit-comment" class="btn btn-sm" style="margin-top: var(--size-2);"><i class="fas fa-paper-plane"></i> Post Comment</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachCommentForm() {
|
||||||
|
document.getElementById('submit-comment')?.addEventListener('click', submitComment);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitComment() {
|
||||||
|
const text = document.getElementById('comment-text').value.trim();
|
||||||
|
if (!text) {
|
||||||
|
showToast('Comment cannot be empty', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/posts/${postId}/comments`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ text }),
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to post comment');
|
||||||
|
const newComment = await res.json();
|
||||||
|
showToast('Comment posted');
|
||||||
|
loadComments(); // refresh list
|
||||||
|
} catch (err) {
|
||||||
|
showToast('Error posting comment', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteComment(commentId) {
|
||||||
|
if (!confirm('Delete this comment?')) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/comments/${commentId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Delete failed');
|
||||||
|
showToast('Comment deleted');
|
||||||
|
loadComments();
|
||||||
|
} catch (err) {
|
||||||
|
showToast('Error deleting comment', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== INIT ====================
|
||||||
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
const authenticated = await checkAuthAndRedirect();
|
||||||
|
if (!authenticated) return;
|
||||||
|
updateUserMenu();
|
||||||
|
loadPost();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user