Prechádzať zdrojové kódy

Add publication commenting

svalavuo 4 dní pred
rodič
commit
1bf868bce3

+ 306 - 0
admin/comments.php

@@ -0,0 +1,306 @@
+<?php
+// Start session for language preference
+if (session_status() === PHP_SESSION_NONE) {
+    session_start();
+}
+
+require_once '../includes/config.php';
+require_once '../includes/database.php';
+require_once '../includes/auth.php';
+require_once '../includes/comment.php';
+require_once '../includes/translation.php';
+
+// Translation system is auto-initialized when translation.php is included
+
+$auth = new Auth();
+$auth->requireAuth();
+
+$comment = new Comment();
+$user = $auth->getUser();
+
+// Handle actions
+$action = $_GET['action'] ?? '';
+$message = '';
+
+if ($action === 'approve' && isset($_GET['id'])) {
+    $id = (int)$_GET['id'];
+    if ($comment->updateStatus($id, 'approved')) {
+        $message = t('admin_comment_approved_success');
+    } else {
+        $message = t('admin_comment_approve_error');
+    }
+    header('Location: comments.php?message=' . urlencode($message));
+    exit;
+}
+
+if ($action === 'reject' && isset($_GET['id'])) {
+    $id = (int)$_GET['id'];
+    if ($comment->updateStatus($id, 'rejected')) {
+        $message = t('admin_comment_rejected_success');
+    } else {
+        $message = t('admin_comment_reject_error');
+    }
+    header('Location: comments.php?message=' . urlencode($message));
+    exit;
+}
+
+if ($action === 'delete' && isset($_GET['id'])) {
+    $id = (int)$_GET['id'];
+    if ($comment->delete($id)) {
+        $message = t('admin_comment_deleted_success');
+    } else {
+        $message = t('admin_comment_delete_error');
+    }
+    header('Location: comments.php?message=' . urlencode($message));
+    exit;
+}
+
+if ($action === 'reply' && isset($_POST['comment_id']) && isset($_POST['reply_content'])) {
+    $commentId = (int)$_POST['comment_id'];
+    $replyContent = trim($_POST['reply_content']);
+    
+    // Get original comment to get publication ID
+    $originalComment = $comment->getById($commentId);
+    
+    if ($originalComment && !empty($replyContent)) {
+        $replyData = [
+            'publication_id' => $originalComment['publication_id'],
+            'parent_id' => $commentId,
+            'name' => $user['username'],
+            'content' => $replyContent,
+            'replied_by' => $user['id']
+        ];
+        
+        if ($comment->createAdminReply($replyData)) {
+            $message = t('admin_reply_added_success');
+        } else {
+            $message = t('admin_reply_add_error');
+        }
+    } else {
+        $message = t('admin_reply_invalid_data');
+    }
+    header('Location: comments.php?message=' . urlencode($message));
+    exit;
+}
+
+// Get pagination parameters
+$page = max(1, (int)($_GET['page'] ?? 1));
+$limit = 20;
+$offset = ($page - 1) * $limit;
+
+// Get status filter
+$status = $_GET['status'] ?? '';
+
+// Get comments
+$comments = $comment->getAll($status ?: null, $limit, $offset);
+$totalComments = $comment->getCountByStatus($status ?: null);
+$totalPages = ceil($totalComments / $limit);
+
+// Handle message from redirect
+if (isset($_GET['message'])) {
+    $message = htmlspecialchars($_GET['message']);
+}
+?>
+<!DOCTYPE html>
+<html lang="<?php echo Translation::getCurrentLang(); ?>">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title><?php echo t('manage_comments'); ?> - <?php echo SITE_TITLE; ?></title>
+    <link rel="stylesheet" href="../css/style.css">
+    <link rel="stylesheet" href="../css/admin-comments.css">
+</head>
+<body>
+    <div class="admin-layout">
+        <header class="admin-header">
+            <div class="header-content">
+                <h1><a href="/index.php"><?php echo SITE_TITLE; ?></a></h1>
+                <nav class="admin-nav">
+                    <a href="index.php" class="nav-link"><?php echo t('admin_nav_dashboard'); ?></a>
+                    <a href="publications.php" class="nav-link"><?php echo t('admin_nav_publications'); ?></a>
+                    <a href="categories.php" class="nav-link"><?php echo t('admin_nav_categories'); ?></a>
+                    <a href="users.php" class="nav-link"><?php echo t('manage_users'); ?></a>
+                    <a href="comments.php" class="nav-link active"><?php echo t('admin_nav_comments'); ?></a>
+                    <?php if (LDAP_ENABLED): ?>
+                        <a href="ldap-users.php" class="nav-link"><?php echo t('admin_nav_ldap_users'); ?></a>
+                    <?php endif; ?>
+                    <a href="logout.php" class="nav-link"><?php echo t('admin_nav_logout'); ?></a>
+                </nav>
+                <div class="user-info">
+                    <?php echo t('welcome'); ?>, <?php echo htmlspecialchars($user['username']); ?>
+                </div>
+                <?php echo Translation::getLanguageSwitcher('comments.php'); ?>
+            </div>
+        </header>
+
+        <main class="admin-main">
+            <div class="page-header">
+                <h2><?php echo t('manage_comments'); ?></h2>
+                <div class="header-stats">
+                    <span class="stat-item">
+                        <?php echo t('pending_comments'); ?>: <strong><?php echo $comment->getPendingCount(); ?></strong>
+                    </span>
+                    <span class="stat-item">
+                        <?php echo t('total_comments'); ?>: <strong><?php echo $comment->getCountByStatus(); ?></strong>
+                    </span>
+                </div>
+            </div>
+            
+            <?php if ($message): ?>
+                <div class="alert alert-<?php echo strpos($message, 'Error') === false ? 'success' : 'error'; ?>">
+                    <?php echo $message; ?>
+                </div>
+            <?php endif; ?>
+
+            <!-- Status Filter -->
+            <div class="filter-section">
+                <form method="get" class="filter-form">
+                    <label for="status"><?php echo t('filter_by_status'); ?>:</label>
+                    <select name="status" id="status" onchange="this.form.submit()">
+                        <option value=""><?php echo t('all_comments'); ?></option>
+                        <option value="pending" <?php echo $status === 'pending' ? 'selected' : ''; ?>><?php echo t('pending'); ?></option>
+                        <option value="approved" <?php echo $status === 'approved' ? 'selected' : ''; ?>><?php echo t('approved'); ?></option>
+                        <option value="rejected" <?php echo $status === 'rejected' ? 'selected' : ''; ?>><?php echo t('rejected'); ?></option>
+                    </select>
+                </form>
+            </div>
+
+            <!-- Comments List -->
+            <div class="comments-admin-list">
+                <?php if (empty($comments)): ?>
+                    <p class="no-comments"><?php echo t('no_comments_found'); ?></p>
+                <?php else: ?>
+                    <?php foreach ($comments as $comment_item): ?>
+                        <div class="comment-admin-item <?php echo $comment_item['status']; ?>">
+                            <div class="comment-header">
+                                <div class="comment-meta">
+                                    <span class="comment-author">
+                                        <strong><?php echo htmlspecialchars($comment_item['name']); ?></strong>
+                                        <?php if ($comment_item['email']): ?>
+                                            <small>(<?php echo htmlspecialchars($comment_item['email']); ?>)</small>
+                                        <?php endif; ?>
+                                    </span>
+                                    <span class="comment-date">
+                                        <?php echo date('M j, Y g:i A', strtotime($comment_item['created_at'])); ?>
+                                    </span>
+                                    <span class="comment-status status-<?php echo $comment_item['status']; ?>">
+                                        <?php echo t($comment_item['status']); ?>
+                                    </span>
+                                </div>
+                                <div class="comment-actions">
+                                    <?php if ($comment_item['status'] === 'pending'): ?>
+                                        <a href="comments.php?action=approve&id=<?php echo $comment_item['id']; ?>" 
+                                           class="btn btn-sm btn-success"><?php echo t('approve'); ?></a>
+                                    <?php endif; ?>
+                                    <?php if ($comment_item['status'] !== 'rejected'): ?>
+                                        <a href="comments.php?action=reject&id=<?php echo $comment_item['id']; ?>" 
+                                           class="btn btn-sm btn-warning"><?php echo t('reject'); ?></a>
+                                    <?php endif; ?>
+                                    <button type="button" class="btn btn-sm btn-primary reply-btn" 
+                                            data-comment-id="<?php echo $comment_item['id']; ?>">
+                                        <?php echo t('reply'); ?>
+                                    </button>
+                                    <a href="comments.php?action=delete&id=<?php echo $comment_item['id']; ?>" 
+                                       class="btn btn-sm btn-danger" 
+                                       onclick="return confirm('<?php echo t('admin_delete_comment_confirm'); ?>')">
+                                        <?php echo t('delete'); ?>
+                                    </a>
+                                </div>
+                            </div>
+                            
+                            <div class="comment-content">
+                                <p><?php echo nl2br(htmlspecialchars($comment_item['content'])); ?></p>
+                            </div>
+                            
+                            <div class="comment-publication">
+                                <small>
+                                    <?php echo t('on_publication'); ?>: 
+                                    <a href="../public/publication.php?id=<?php echo $comment_item['publication_id']; ?>" target="_blank">
+                                        <?php echo htmlspecialchars($comment_item['publication_title']); ?>
+                                    </a>
+                                </small>
+                            </div>
+                            
+                            <!-- Reply Form (hidden by default) -->
+                            <div class="reply-form-container" id="reply-form-<?php echo $comment_item['id']; ?>" style="display: none;">
+                                <form method="post" class="reply-form">
+                                    <input type="hidden" name="comment_id" value="<?php echo $comment_item['id']; ?>">
+                                    <div class="form-group">
+                                        <label><?php echo t('admin_reply'); ?>:</label>
+                                        <textarea name="reply_content" rows="3" required placeholder="<?php echo t('enter_reply'); ?>"></textarea>
+                                    </div>
+                                    <div class="form-actions">
+                                        <button type="submit" class="btn btn-primary"><?php echo t('submit_reply'); ?></button>
+                                        <button type="button" class="btn btn-secondary cancel-reply"><?php echo t('cancel'); ?></button>
+                                    </div>
+                                </form>
+                            </div>
+                            
+                            <!-- Show replies -->
+                            <?php if ($comment_item['parent_id']): ?>
+                                <div class="reply-indicator">
+                                    <small><?php echo t('reply_to_comment'); ?></small>
+                                </div>
+                            <?php endif; ?>
+                        </div>
+                    <?php endforeach; ?>
+                <?php endif; ?>
+            </div>
+
+            <!-- Pagination -->
+            <?php if ($totalPages > 1): ?>
+                <div class="pagination">
+                    <?php if ($page > 1): ?>
+                        <a href="?page=<?php echo $page - 1; ?><?php echo $status ? '&status=' . $status : ''; ?>" 
+                           class="btn btn-secondary"><?php echo t('previous'); ?></a>
+                    <?php endif; ?>
+                    
+                    <span class="page-info">
+                        <?php echo t('page'); ?> <?php echo $page; ?> <?php echo t('of'); ?> <?php echo $totalPages; ?>
+                    </span>
+                    
+                    <?php if ($page < $totalPages): ?>
+                        <a href="?page=<?php echo $page + 1; ?><?php echo $status ? '&status=' . $status : ''; ?>" 
+                           class="btn btn-secondary"><?php echo t('next'); ?></a>
+                    <?php endif; ?>
+                </div>
+            <?php endif; ?>
+        </main>
+    </div>
+
+    <script>
+        // Admin Comments JavaScript
+        document.addEventListener('DOMContentLoaded', function() {
+            // Handle reply buttons
+            document.querySelectorAll('.reply-btn').forEach(button => {
+                button.addEventListener('click', function() {
+                    const commentId = this.dataset.commentId;
+                    const replyForm = document.getElementById('reply-form-' + commentId);
+                    
+                    // Hide all other reply forms
+                    document.querySelectorAll('.reply-form-container').forEach(form => {
+                        if (form.id !== 'reply-form-' + commentId) {
+                            form.style.display = 'none';
+                        }
+                    });
+                    
+                    // Toggle this reply form
+                    replyForm.style.display = replyForm.style.display === 'none' ? 'block' : 'none';
+                    
+                    // Focus on textarea if showing
+                    if (replyForm.style.display === 'block') {
+                        replyForm.querySelector('textarea').focus();
+                    }
+                });
+            });
+            
+            // Handle cancel reply buttons
+            document.querySelectorAll('.cancel-reply').forEach(button => {
+                button.addEventListener('click', function() {
+                    this.closest('.reply-form-container').style.display = 'none';
+                });
+            });
+        });
+    </script>
+</body>
+</html>

+ 1 - 0
admin/publications.php

@@ -74,6 +74,7 @@ $totalPages = ceil($totalPublications / $limit);
                     <a href="index.php" class="nav-link"><?php echo t('admin_nav_dashboard'); ?></a>
                     <a href="index.php" class="nav-link"><?php echo t('admin_nav_dashboard'); ?></a>
                     <a href="publications.php" class="nav-link active"><?php echo t('admin_nav_publications'); ?></a>
                     <a href="publications.php" class="nav-link active"><?php echo t('admin_nav_publications'); ?></a>
                     <a href="categories.php" class="nav-link"><?php echo t('admin_nav_categories'); ?></a>
                     <a href="categories.php" class="nav-link"><?php echo t('admin_nav_categories'); ?></a>
+                    <a href="comments.php" class="nav-link"><?php echo t('admin_nav_comments'); ?></a>
                     <a href="users.php" class="nav-link"><?php echo t('manage_users'); ?></a>
                     <a href="users.php" class="nav-link"><?php echo t('manage_users'); ?></a>
                     <?php if (LDAP_ENABLED): ?>
                     <?php if (LDAP_ENABLED): ?>
                         <a href="ldap-users.php" class="nav-link"><?php echo t('admin_nav_ldap_users'); ?></a>
                         <a href="ldap-users.php" class="nav-link"><?php echo t('admin_nav_ldap_users'); ?></a>

+ 460 - 0
css/admin-comments.css

@@ -0,0 +1,460 @@
+/* Admin Comments Interface Styles */
+
+.comments-admin-list {
+    margin-top: 1rem;
+}
+
+.comment-admin-item {
+    background: white;
+    border: 1px solid #e9ecef;
+    border-radius: 8px;
+    margin-bottom: 1.5rem;
+    overflow: hidden;
+    transition: box-shadow 0.2s ease;
+}
+
+.comment-admin-item:hover {
+    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.comment-admin-item.pending {
+    border-left: 4px solid #ffc107;
+}
+
+.comment-admin-item.approved {
+    border-left: 4px solid #28a745;
+}
+
+.comment-admin-item.rejected {
+    border-left: 4px solid #dc3545;
+}
+
+.comment-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-start;
+    padding: 1rem 1.5rem;
+    background: #f8f9fa;
+    border-bottom: 1px solid #e9ecef;
+    flex-wrap: wrap;
+    gap: 1rem;
+}
+
+.comment-meta {
+    flex: 1;
+    min-width: 250px;
+}
+
+.comment-author {
+    display: block;
+    font-weight: 600;
+    color: #2c3e50;
+    margin-bottom: 0.25rem;
+}
+
+.comment-author strong {
+    color: #2c3e50;
+}
+
+.comment-author small {
+    color: #6c757d;
+    font-weight: normal;
+}
+
+.comment-date {
+    color: #6c757d;
+    font-size: 0.875rem;
+    display: block;
+    margin-bottom: 0.25rem;
+}
+
+.comment-status {
+    display: inline-block;
+    padding: 0.25rem 0.75rem;
+    border-radius: 12px;
+    font-size: 0.75rem;
+    font-weight: 600;
+    text-transform: uppercase;
+}
+
+.status-pending {
+    background: #fff3cd;
+    color: #856404;
+}
+
+.status-approved {
+    background: #d4edda;
+    color: #155724;
+}
+
+.status-rejected {
+    background: #f8d7da;
+    color: #721c24;
+}
+
+.comment-actions {
+    display: flex;
+    gap: 0.5rem;
+    flex-wrap: wrap;
+    align-items: center;
+}
+
+.comment-actions .btn {
+    padding: 0.375rem 0.75rem;
+    font-size: 0.875rem;
+    border: none;
+    border-radius: 4px;
+    cursor: pointer;
+    transition: background-color 0.2s ease, transform 0.1s ease;
+    text-decoration: none;
+    display: inline-block;
+}
+
+.comment-actions .btn:hover {
+    transform: translateY(-1px);
+}
+
+.btn-success {
+    background: #28a745;
+    color: white;
+}
+
+.btn-success:hover {
+    background: #218838;
+}
+
+.btn-warning {
+    background: #ffc107;
+    color: #212529;
+}
+
+.btn-warning:hover {
+    background: #e0a800;
+}
+
+.btn-primary {
+    background: #007bff;
+    color: white;
+}
+
+.btn-primary:hover {
+    background: #0069d9;
+}
+
+.btn-danger {
+    background: #dc3545;
+    color: white;
+}
+
+.btn-danger:hover {
+    background: #c82333;
+}
+
+.btn-secondary {
+    background: #6c757d;
+    color: white;
+}
+
+.btn-secondary:hover {
+    background: #5a6268;
+}
+
+.comment-content {
+    padding: 1.5rem;
+    color: #495057;
+    line-height: 1.6;
+}
+
+.comment-content p {
+    margin: 0;
+}
+
+.comment-publication {
+    padding: 0 1.5rem 1rem;
+    background: #f8f9fa;
+    border-top: 1px solid #e9ecef;
+}
+
+.comment-publication small {
+    color: #6c757d;
+}
+
+.comment-publication a {
+    color: #007bff;
+    text-decoration: none;
+}
+
+.comment-publication a:hover {
+    text-decoration: underline;
+}
+
+/* Reply Form Styles */
+.reply-form-container {
+    background: #e3f2fd;
+    border-top: 1px solid #bbdefb;
+    padding: 1.5rem;
+}
+
+.reply-form .form-group {
+    margin-bottom: 1rem;
+}
+
+.reply-form label {
+    display: block;
+    margin-bottom: 0.5rem;
+    font-weight: 600;
+    color: #495057;
+}
+
+.reply-form textarea {
+    width: 100%;
+    padding: 0.75rem;
+    border: 1px solid #ced4da;
+    border-radius: 4px;
+    font-size: 0.875rem;
+    resize: vertical;
+    min-height: 80px;
+    font-family: inherit;
+}
+
+.reply-form textarea:focus {
+    outline: none;
+    border-color: #007bff;
+    box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
+}
+
+.reply-form .form-actions {
+    display: flex;
+    gap: 0.75rem;
+    align-items: center;
+}
+
+.reply-indicator {
+    padding: 0.5rem 1.5rem;
+    background: #f8f9fa;
+    border-top: 1px solid #e9ecef;
+    font-style: italic;
+    color: #6c757d;
+}
+
+/* Filter Section */
+.filter-section {
+    background: white;
+    border: 1px solid #e9ecef;
+    border-radius: 8px;
+    padding: 1rem 1.5rem;
+    margin-bottom: 1.5rem;
+}
+
+.filter-form {
+    display: flex;
+    align-items: center;
+    gap: 1rem;
+    flex-wrap: wrap;
+}
+
+.filter-form label {
+    font-weight: 600;
+    color: #495057;
+    margin: 0;
+}
+
+.filter-form select {
+    padding: 0.5rem;
+    border: 1px solid #ced4da;
+    border-radius: 4px;
+    font-size: 0.875rem;
+    background: white;
+    cursor: pointer;
+}
+
+.filter-form select:focus {
+    outline: none;
+    border-color: #007bff;
+    box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
+}
+
+/* Header Stats */
+.header-stats {
+    display: flex;
+    gap: 2rem;
+    align-items: center;
+    flex-wrap: wrap;
+}
+
+.stat-item {
+    font-size: 0.875rem;
+    color: #6c757d;
+}
+
+.stat-item strong {
+    color: #2c3e50;
+    font-weight: 600;
+}
+
+/* Pagination */
+.pagination {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 1.5rem 0;
+    border-top: 1px solid #e9ecef;
+    margin-top: 2rem;
+}
+
+.page-info {
+    color: #6c757d;
+    font-size: 0.875rem;
+}
+
+/* No Comments State */
+.no-comments {
+    text-align: center;
+    padding: 3rem;
+    color: #6c757d;
+    font-style: italic;
+    background: #f8f9fa;
+    border-radius: 8px;
+    border: 1px solid #e9ecef;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+    .comment-header {
+        flex-direction: column;
+        align-items: flex-start;
+    }
+    
+    .comment-actions {
+        width: 100%;
+        justify-content: flex-start;
+    }
+    
+    .header-stats {
+        flex-direction: column;
+        gap: 0.5rem;
+        align-items: flex-start;
+    }
+    
+    .filter-form {
+        flex-direction: column;
+        align-items: flex-start;
+    }
+    
+    .pagination {
+        flex-direction: column;
+        gap: 1rem;
+        text-align: center;
+    }
+    
+    .reply-form .form-actions {
+        flex-direction: column;
+        align-items: stretch;
+    }
+}
+
+/* Button Loading States */
+.btn.loading {
+    position: relative;
+    pointer-events: none;
+    opacity: 0.7;
+}
+
+.btn.loading::after {
+    content: '';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    width: 12px;
+    height: 12px;
+    margin: -6px 0 0 -6px;
+    border: 2px solid transparent;
+    border-top: 2px solid currentColor;
+    border-radius: 50%;
+    animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+    0% { transform: rotate(0deg); }
+    100% { transform: rotate(360deg); }
+}
+
+/* Success/Error States */
+.comment-admin-item.success {
+    border-left-color: #28a745;
+    background: #f8fff8;
+}
+
+.comment-admin-item.error {
+    border-left-color: #dc3545;
+    background: #fff8f8;
+}
+
+/* Bulk Actions */
+.bulk-actions {
+    background: white;
+    border: 1px solid #e9ecef;
+    border-radius: 8px;
+    padding: 1rem 1.5rem;
+    margin-bottom: 1.5rem;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    flex-wrap: wrap;
+    gap: 1rem;
+}
+
+.bulk-actions .form-group {
+    display: flex;
+    align-items: center;
+    gap: 0.5rem;
+}
+
+.bulk-actions select {
+    padding: 0.5rem;
+    border: 1px solid #ced4da;
+    border-radius: 4px;
+    font-size: 0.875rem;
+}
+
+/* Comment Count Badge */
+.comment-count-badge {
+    background: #007bff;
+    color: white;
+    padding: 0.25rem 0.5rem;
+    border-radius: 12px;
+    font-size: 0.75rem;
+    font-weight: 600;
+    margin-left: 0.5rem;
+}
+
+/* Search Box */
+.search-box {
+    background: white;
+    border: 1px solid #e9ecef;
+    border-radius: 8px;
+    padding: 1rem 1.5rem;
+    margin-bottom: 1.5rem;
+}
+
+.search-box form {
+    display: flex;
+    gap: 1rem;
+    align-items: center;
+    flex-wrap: wrap;
+}
+
+.search-box input[type="text"] {
+    flex: 1;
+    min-width: 200px;
+    padding: 0.5rem;
+    border: 1px solid #ced4da;
+    border-radius: 4px;
+    font-size: 0.875rem;
+}
+
+.search-box input[type="text"]:focus {
+    outline: none;
+    border-color: #007bff;
+    box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
+}

+ 346 - 0
css/comments.css

@@ -0,0 +1,346 @@
+/* Comments System Styles */
+
+.comments-section {
+    margin-top: 3rem;
+    padding-top: 2rem;
+    border-top: 1px solid #e0e0e0;
+}
+
+.comments-section h3 {
+    margin-bottom: 1.5rem;
+    color: #333;
+    font-size: 1.5rem;
+}
+
+/* Comments List */
+.comments-list {
+    margin-bottom: 2rem;
+}
+
+.comment {
+    background: #f8f9fa;
+    border: 1px solid #e9ecef;
+    border-radius: 8px;
+    padding: 1.5rem;
+    margin-bottom: 1rem;
+    transition: box-shadow 0.2s ease;
+}
+
+.comment:hover {
+    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.comment.admin-reply {
+    background: #e3f2fd;
+    border-color: #2196f3;
+}
+
+.comment-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 1rem;
+    flex-wrap: wrap;
+    gap: 0.5rem;
+}
+
+.comment-author {
+    font-weight: 600;
+    color: #333;
+}
+
+.comment-author strong {
+    color: #2c3e50;
+}
+
+.admin-badge {
+    background: #2196f3;
+    color: white;
+    padding: 2px 8px;
+    border-radius: 12px;
+    font-size: 0.75rem;
+    margin-left: 0.5rem;
+}
+
+.comment-date {
+    color: #6c757d;
+    font-size: 0.875rem;
+}
+
+.comment-content {
+    color: #495057;
+    line-height: 1.6;
+    margin-bottom: 1rem;
+}
+
+.reply-info {
+    color: #6c757d;
+    font-style: italic;
+    margin-top: 0.5rem;
+}
+
+.no-comments {
+    text-align: center;
+    color: #6c757d;
+    font-style: italic;
+    padding: 2rem;
+    background: #f8f9fa;
+    border-radius: 8px;
+}
+
+/* Comment Form */
+.comment-form-container {
+    background: #ffffff;
+    border: 1px solid #e9ecef;
+    border-radius: 8px;
+    padding: 2rem;
+    margin-top: 2rem;
+}
+
+.comment-form-container h4 {
+    margin-bottom: 1.5rem;
+    color: #333;
+}
+
+.comment-form .form-group {
+    margin-bottom: 1.5rem;
+}
+
+.comment-form label {
+    display: block;
+    margin-bottom: 0.5rem;
+    font-weight: 600;
+    color: #495057;
+}
+
+.comment-form input,
+.comment-form textarea {
+    width: 100%;
+    padding: 0.75rem;
+    border: 1px solid #ced4da;
+    border-radius: 4px;
+    font-size: 1rem;
+    transition: border-color 0.2s ease, box-shadow 0.2s ease;
+}
+
+.comment-form input:focus,
+.comment-form textarea:focus {
+    outline: none;
+    border-color: #2196f3;
+    box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.1);
+}
+
+.comment-form textarea {
+    resize: vertical;
+    min-height: 100px;
+}
+
+.comment-form small {
+    color: #6c757d;
+    font-size: 0.875rem;
+    margin-top: 0.25rem;
+    display: block;
+}
+
+/* Captcha Styles */
+.captcha-container {
+    background: #f8f9fa;
+    border: 1px solid #e9ecef;
+    border-radius: 4px;
+    padding: 1rem;
+    margin-bottom: 1.5rem;
+}
+
+.captcha-question {
+    display: flex;
+    align-items: center;
+    gap: 0.5rem;
+    font-size: 1.1rem;
+    font-weight: 600;
+    margin-bottom: 0.5rem;
+}
+
+.captcha-numbers {
+    color: #2196f3;
+    font-size: 1.2rem;
+}
+
+.captcha-operator {
+    color: #495057;
+    font-size: 1.1rem;
+}
+
+.captcha-equals {
+    color: #495057;
+    font-size: 1.1rem;
+}
+
+.captcha-input {
+    width: 80px;
+    padding: 0.5rem;
+    border: 1px solid #ced4da;
+    border-radius: 4px;
+    text-align: center;
+    font-size: 1rem;
+    font-weight: 600;
+}
+
+.captcha-input:focus {
+    outline: none;
+    border-color: #2196f3;
+    box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.1);
+}
+
+.captcha-help {
+    color: #6c757d;
+    font-size: 0.875rem;
+    font-style: italic;
+}
+
+/* Form Actions */
+.form-actions {
+    display: flex;
+    gap: 1rem;
+    align-items: center;
+}
+
+.form-actions .btn {
+    padding: 0.75rem 1.5rem;
+    border: none;
+    border-radius: 4px;
+    font-size: 1rem;
+    cursor: pointer;
+    transition: background-color 0.2s ease, transform 0.1s ease;
+}
+
+.form-actions .btn:disabled {
+    opacity: 0.6;
+    cursor: not-allowed;
+}
+
+.form-actions .btn:hover:not(:disabled) {
+    transform: translateY(-1px);
+}
+
+.btn-primary {
+    background: #2196f3;
+    color: white;
+}
+
+.btn-primary:hover:not(:disabled) {
+    background: #1976d2;
+}
+
+.btn-secondary {
+    background: #6c757d;
+    color: white;
+}
+
+.btn-secondary:hover {
+    background: #5a6268;
+}
+
+/* Alert Messages */
+.alert {
+    padding: 1rem;
+    border-radius: 4px;
+    margin-bottom: 1rem;
+    border: 1px solid transparent;
+}
+
+.alert-success {
+    background: #d4edda;
+    color: #155724;
+    border-color: #c3e6cb;
+}
+
+.alert-error {
+    background: #f8d7da;
+    color: #721c24;
+    border-color: #f5c6cb;
+}
+
+.alert-warning {
+    background: #fff3cd;
+    color: #856404;
+    border-color: #ffeaa7;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+    .comment-header {
+        flex-direction: column;
+        align-items: flex-start;
+    }
+    
+    .comment-form-container {
+        padding: 1rem;
+    }
+    
+    .form-actions {
+        flex-direction: column;
+        align-items: stretch;
+    }
+    
+    .captcha-question {
+        font-size: 1rem;
+    }
+    
+    .captcha-numbers {
+        font-size: 1.1rem;
+    }
+}
+
+/* Reply Button Styles */
+.reply-btn {
+    background: #28a745;
+    color: white;
+    border: none;
+    padding: 0.25rem 0.75rem;
+    border-radius: 4px;
+    font-size: 0.875rem;
+    cursor: pointer;
+    transition: background-color 0.2s ease;
+}
+
+.reply-btn:hover {
+    background: #218838;
+}
+
+/* Comment Count Animation */
+.comments-section h3 {
+    transition: color 0.3s ease;
+}
+
+.comments-section h3.updated {
+    color: #28a745;
+}
+
+/* Loading State */
+.loading {
+    opacity: 0.6;
+    pointer-events: none;
+}
+
+.loading .btn {
+    position: relative;
+}
+
+.loading .btn::after {
+    content: '';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    width: 16px;
+    height: 16px;
+    margin: -8px 0 0 -8px;
+    border: 2px solid transparent;
+    border-top: 2px solid currentColor;
+    border-radius: 50%;
+    animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+    0% { transform: rotate(0deg); }
+    100% { transform: rotate(360deg); }
+}

+ 124 - 0
includes/captcha.php

@@ -0,0 +1,124 @@
+<?php
+/**
+ * Simple Captcha System for Comment Forms
+ */
+
+class Captcha {
+    private static $sessionKey = 'captcha_code';
+    
+    /**
+     * Generate a simple math captcha
+     */
+    public static function generate() {
+        // Start session if not already started
+        if (session_status() === PHP_SESSION_NONE) {
+            session_start();
+        }
+        
+        // Generate random numbers
+        $num1 = rand(1, 10);
+        $num2 = rand(1, 10);
+        $operators = ['+', '-'];
+        $operator = $operators[array_rand($operators)];
+        
+        // Calculate answer
+        if ($operator === '+') {
+            $answer = $num1 + $num2;
+        } else {
+            $answer = $num1 - $num2;
+        }
+        
+        // Store answer in session
+        $_SESSION[self::$sessionKey] = $answer;
+        
+        // Return the question
+        return [
+            'question' => "$num1 $operator $num2 = ?",
+            'num1' => $num1,
+            'num2' => $num2,
+            'operator' => $operator
+        ];
+    }
+    
+    /**
+     * Verify captcha answer
+     */
+    public static function verify($answer) {
+        // Start session if not already started
+        if (session_status() === PHP_SESSION_NONE) {
+            session_start();
+        }
+        
+        // Check if captcha exists in session
+        if (!isset($_SESSION[self::$sessionKey])) {
+            return false;
+        }
+        
+        // Verify answer
+        $isValid = (int)$answer === $_SESSION[self::$sessionKey];
+        
+        // Clear captcha after verification
+        unset($_SESSION[self::$sessionKey]);
+        
+        return $isValid;
+    }
+    
+    /**
+     * Get HTML for captcha display
+     */
+    public static function getHtml() {
+        $captcha = self::generate();
+        
+        ob_start();
+        ?>
+        <div class="captcha-container">
+            <div class="captcha-question">
+                <span class="captcha-numbers"><?php echo $captcha['num1']; ?></span>
+                <span class="captcha-operator"><?php echo $captcha['operator']; ?></span>
+                <span class="captcha-numbers"><?php echo $captcha['num2']; ?></span>
+                <span class="captcha-equals">=</span>
+                <input type="number" name="captcha_answer" class="captcha-input" required 
+                       placeholder="?" min="-20" max="20" autocomplete="off">
+            </div>
+            <small class="captcha-help"><?php echo t('captcha_help'); ?></small>
+        </div>
+        <?php
+        return ob_get_clean();
+    }
+    
+    /**
+     * Get captcha for AJAX requests
+     */
+    public static function getAjaxCaptcha() {
+        $captcha = self::generate();
+        return [
+            'question' => $captcha['question'],
+            'num1' => $captcha['num1'],
+            'num2' => $captcha['num2'],
+            'operator' => $captcha['operator']
+        ];
+    }
+    
+    /**
+     * Refresh captcha (for AJAX)
+     */
+    public static function refresh() {
+        return self::generate();
+    }
+    
+    /**
+     * Check if captcha is required for current user
+     */
+    public static function isRequired() {
+        // Check if user is logged in as admin
+        if (session_status() === PHP_SESSION_NONE) {
+            session_start();
+        }
+        
+        if (isset($_SESSION['user_id']) && isset($_SESSION['user_role'])) {
+            return $_SESSION['user_role'] !== 'admin';
+        }
+        
+        return true; // Captcha required for non-admin users
+    }
+}

+ 208 - 0
includes/comment.php

@@ -0,0 +1,208 @@
+<?php
+/**
+ * Comment Management Class
+ * Handles all comment-related operations including creation, moderation, and replies
+ */
+
+class Comment {
+    private $db;
+    
+    public function __construct() {
+        $this->db = Database::getInstance();
+    }
+    
+    /**
+     * Create a new comment
+     */
+    public function create($data) {
+        $sql = "INSERT INTO comments (publication_id, parent_id, name, email, content, ip_address, user_agent) 
+                VALUES (?, ?, ?, ?, ?, ?, ?)";
+        
+        return $this->db->execute($sql, [
+            $data['publication_id'],
+            $data['parent_id'] ?? null,
+            $data['name'],
+            $data['email'] ?? null,
+            $data['content'],
+            $_SERVER['REMOTE_ADDR'] ?? null,
+            $_SERVER['HTTP_USER_AGENT'] ?? null
+        ]);
+    }
+    
+    /**
+     * Create an admin reply (bypasses captcha, auto-approved)
+     */
+    public function createAdminReply($data) {
+        $sql = "INSERT INTO comments (publication_id, parent_id, name, content, status, admin_reply, replied_by) 
+                VALUES (?, ?, ?, ?, 'approved', TRUE, ?)";
+        
+        return $this->db->execute($sql, [
+            $data['publication_id'],
+            $data['parent_id'],
+            $data['name'] ?? 'Admin',
+            $data['content'],
+            $data['replied_by']
+        ]);
+    }
+    
+    /**
+     * Get approved comments for a publication
+     */
+    public function getApprovedByPublication($publicationId) {
+        $sql = "SELECT c.*, u.username as replied_by_username 
+                FROM comments c 
+                LEFT JOIN users u ON c.replied_by = u.id 
+                WHERE c.publication_id = ? AND c.status = 'approved' 
+                ORDER BY c.created_at ASC";
+        
+        return $this->db->fetchAll($sql, [$publicationId]);
+    }
+    
+    /**
+     * Get all comments for admin (including pending)
+     */
+    public function getAll($status = null, $limit = 50, $offset = 0) {
+        $sql = "SELECT c.*, p.title as publication_title, u.username as replied_by_username 
+                FROM comments c 
+                LEFT JOIN publications p ON c.publication_id = p.id 
+                LEFT JOIN users u ON c.replied_by = u.id";
+        
+        $params = [];
+        if ($status) {
+            $sql .= " WHERE c.status = ?";
+            $params[] = $status;
+        }
+        
+        $sql .= " ORDER BY c.created_at DESC LIMIT ? OFFSET ?";
+        $params[] = $limit;
+        $params[] = $offset;
+        
+        return $this->db->fetchAll($sql, $params);
+    }
+    
+    /**
+     * Get comment by ID
+     */
+    public function getById($id) {
+        $sql = "SELECT c.*, p.title as publication_title, u.username as replied_by_username 
+                FROM comments c 
+                LEFT JOIN publications p ON c.publication_id = p.id 
+                LEFT JOIN users u ON c.replied_by = u.id 
+                WHERE c.id = ?";
+        
+        return $this->db->fetch($sql, [$id]);
+    }
+    
+    /**
+     * Update comment status (approve/reject)
+     */
+    public function updateStatus($id, $status) {
+        $sql = "UPDATE comments SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?";
+        return $this->db->execute($sql, [$status, $id]);
+    }
+    
+    /**
+     * Delete a comment
+     */
+    public function delete($id) {
+        $sql = "DELETE FROM comments WHERE id = ?";
+        return $this->db->execute($sql, [$id]);
+    }
+    
+    /**
+     * Get comment count by status
+     */
+    public function getCountByStatus($status = null) {
+        if ($status) {
+            $sql = "SELECT COUNT(*) as count FROM comments WHERE status = ?";
+            $result = $this->db->fetch($sql, [$status]);
+        } else {
+            $sql = "SELECT COUNT(*) as count FROM comments";
+            $result = $this->db->fetch($sql);
+        }
+        return $result['count'];
+    }
+    
+    /**
+     * Get comment count for a publication
+     */
+    public function getCountByPublication($publicationId) {
+        $sql = "SELECT COUNT(*) as count FROM comments 
+                WHERE publication_id = ? AND status = 'approved'";
+        $result = $this->db->fetch($sql, [$publicationId]);
+        return $result['count'];
+    }
+    
+    /**
+     * Validate comment data
+     */
+    public function validate($data, $isAdmin = false) {
+        $errors = [];
+        
+        // Common validations
+        if (empty($data['content'])) {
+            $errors[] = 'Comment content is required';
+        } elseif (strlen($data['content']) > 2000) {
+            $errors[] = 'Comment content is too long (max 2000 characters)';
+        }
+        
+        if (empty($data['publication_id'])) {
+            $errors[] = 'Publication ID is required';
+        }
+        
+        // Public user validations (not for admin)
+        if (!$isAdmin) {
+            if (empty($data['name'])) {
+                $errors[] = 'Name is required';
+            } elseif (strlen($data['name']) > 100) {
+                $errors[] = 'Name is too long (max 100 characters)';
+            }
+            
+            if (!empty($data['email']) && !filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
+                $errors[] = 'Invalid email address';
+            }
+        }
+        
+        return $errors;
+    }
+    
+    /**
+     * Check if user can reply to comment
+     */
+    public function canReply($commentId, $userId) {
+        // Admins can reply to any comment
+        if ($userId) {
+            $userSql = "SELECT role FROM users WHERE id = ?";
+            $user = $this->db->fetch($userSql, [$userId]);
+            if ($user && $user['role'] === 'admin') {
+                return true;
+            }
+        }
+        
+        // Check if this is a reply to own comment (for future enhancement)
+        $commentSql = "SELECT user_id FROM comments WHERE id = ?";
+        $comment = $this->db->fetch($commentSql, [$commentId]);
+        
+        return false; // Only admins can reply for now
+    }
+    
+    /**
+     * Get recent comments for admin dashboard
+     */
+    public function getRecent($limit = 10) {
+        $sql = "SELECT c.*, p.title as publication_title 
+                FROM comments c 
+                LEFT JOIN publications p ON c.publication_id = p.id 
+                ORDER BY c.created_at DESC 
+                LIMIT ?";
+        
+        return $this->db->fetchAll($sql, [$limit]);
+    }
+    
+    /**
+     * Get pending comments count
+     */
+    public function getPendingCount() {
+        return $this->getCountByStatus('pending');
+    }
+}

+ 54 - 0
languages/en.php

@@ -63,6 +63,60 @@ return [
     'created' => 'Created {date}',
     'created' => 'Created {date}',
     'results_count' => '{count} publications in this category',
     'results_count' => '{count} publications in this category',
     
     
+    // Comments
+    'comments' => 'Comments',
+    'comment' => 'Comment',
+    'leave_a_comment' => 'Leave a Comment',
+    'name' => 'Name',
+    'email' => 'Email',
+    'email_optional' => 'Email (optional)',
+    'submit_comment' => 'Submit Comment',
+    'comment_submitted_success' => 'Comment submitted successfully! It will be visible after approval.',
+    'comment_submit_error' => 'Error submitting comment. Please try again.',
+    'no_comments_yet' => 'No comments yet. Be the first to comment!',
+    'replying_to' => 'Replying to',
+    'cancel' => 'Cancel',
+    'reply' => 'Reply',
+    'admin' => 'Admin',
+    'replied_by' => 'Replied by',
+    'captcha_help' => 'Solve the math problem to prove you are human',
+    'captcha_invalid' => 'Invalid captcha answer. Please try again.',
+    'submitting' => 'Submitting',
+    'enter_reply' => 'Enter your reply',
+    'submit_reply' => 'Submit Reply',
+    'reply_to_comment' => 'This is a reply to another comment',
+    'on_publication' => 'On publication',
+    
+    // Admin Comments
+    'manage_comments' => 'Manage Comments',
+    'admin_nav_comments' => 'Comments',
+    'pending_comments' => 'Pending Comments',
+    'total_comments' => 'Total Comments',
+    'filter_by_status' => 'Filter by Status',
+    'all_comments' => 'All Comments',
+    'pending' => 'Pending',
+    'approved' => 'Approved',
+    'rejected' => 'Rejected',
+    'approve' => 'Approve',
+    'reject' => 'Reject',
+    'delete' => 'Delete',
+    'admin_reply' => 'Admin Reply',
+    'no_comments_found' => 'No comments found.',
+    'admin_comment_approved_success' => 'Comment approved successfully.',
+    'admin_comment_approve_error' => 'Error approving comment.',
+    'admin_comment_rejected_success' => 'Comment rejected successfully.',
+    'admin_comment_reject_error' => 'Error rejecting comment.',
+    'admin_comment_deleted_success' => 'Comment deleted successfully.',
+    'admin_comment_delete_error' => 'Error deleting comment.',
+    'admin_reply_added_success' => 'Reply added successfully.',
+    'admin_reply_add_error' => 'Error adding reply.',
+    'admin_reply_invalid_data' => 'Invalid reply data.',
+    'admin_delete_comment_confirm' => 'Are you sure you want to delete this comment?',
+    'previous' => 'Previous',
+    'next' => 'Next',
+    'page' => 'Page',
+    'of' => 'of',
+    
     // Search
     // Search
     'search_results' => 'Search Results',
     'search_results' => 'Search Results',
     'search_query' => 'Search for',
     'search_query' => 'Search for',

+ 54 - 0
languages/fi.php

@@ -64,6 +64,60 @@ return [
     'created' => 'Luotu {date}',
     'created' => 'Luotu {date}',
     'results_count' => '{count} julkaisua tässä kategoriassa',
     'results_count' => '{count} julkaisua tässä kategoriassa',
     
     
+    // Comments
+    'comments' => 'Kommentit',
+    'comment' => 'Kommentti',
+    'leave_a_comment' => 'Jätä kommentti',
+    'name' => 'Nimi',
+    'email' => 'Sähköposti',
+    'email_optional' => 'Sähköposti (vapaaehtoinen)',
+    'submit_comment' => 'Lähetä kommentti',
+    'comment_submitted_success' => 'Kommentti lähetetty onnistuneesti! Se näkyy hyväksynnän jälkeen.',
+    'comment_submit_error' => 'Virhe kommentin lähettämisessä. Yritä uudelleen.',
+    'no_comments_yet' => 'Ei kommentteja vielä. Ole ensimmäinen kommentoija!',
+    'replying_to' => 'Vastaa käyttäjälle',
+    'cancel' => 'Peruuta',
+    'reply' => 'Vastaa',
+    'admin' => 'Ylläpitäjä',
+    'replied_by' => 'Vastannut',
+    'captcha_help' => 'Ratkaise matematiikkaongelma todistaaksesi olevasi ihminen',
+    'captcha_invalid' => 'Virheellinen vastaus. Yritä uudelleen.',
+    'submitting' => 'Lähetetään',
+    'enter_reply' => 'Kirjoita vastauksesi',
+    'submit_reply' => 'Lähetä vastaus',
+    'reply_to_comment' => 'Tämä on vastaus toiseen kommenttiin',
+    'on_publication' => 'Julkaisussa',
+    
+    // Admin Comments
+    'manage_comments' => 'Hallinnoi kommentteja',
+    'admin_nav_comments' => 'Kommentit',
+    'pending_comments' => 'Odottavat kommentit',
+    'total_comments' => 'Kaikki kommentit',
+    'filter_by_status' => 'Suodata tilan mukaan',
+    'all_comments' => 'Kaikki kommentit',
+    'pending' => 'Odottaa',
+    'approved' => 'Hyväksytty',
+    'rejected' => 'Hylätty',
+    'approve' => 'Hyväksy',
+    'reject' => 'Hylkää',
+    'delete' => 'Poista',
+    'admin_reply' => 'Ylläpitäjän vastaus',
+    'no_comments_found' => 'Kommentteja ei löytynyt.',
+    'admin_comment_approved_success' => 'Kommentti hyväksytty onnistuneesti.',
+    'admin_comment_approve_error' => 'Virhe kommentin hyväksymisessä.',
+    'admin_comment_rejected_success' => 'Kommentti hylätty onnistuneesti.',
+    'admin_comment_reject_error' => 'Virhe kommentin hylkäämisessä.',
+    'admin_comment_deleted_success' => 'Kommentti poistettu onnistuneesti.',
+    'admin_comment_delete_error' => 'Virhe kommentin poistamisessa.',
+    'admin_reply_added_success' => 'Vastaus lisätty onnistuneesti.',
+    'admin_reply_add_error' => 'Virhe vastauksen lisäämisessä.',
+    'admin_reply_invalid_data' => 'Virheellinen vastausdata.',
+    'admin_delete_comment_confirm' => 'Oletko varma, että haluat poistaa tämän kommentin?',
+    'previous' => 'Edellinen',
+    'next' => 'Seuraava',
+    'page' => 'Sivu',
+    'of' => '/',
+    
     // Search
     // Search
     'search_results' => 'Hakutulokset',
     'search_results' => 'Hakutulokset',
     'search_query' => 'Hakusana',
     'search_query' => 'Hakusana',

+ 210 - 0
public/publication.php

@@ -7,11 +7,14 @@ if (session_status() === PHP_SESSION_NONE) {
 require_once '../includes/config.php';
 require_once '../includes/config.php';
 require_once '../includes/database.php';
 require_once '../includes/database.php';
 require_once '../includes/publication.php';
 require_once '../includes/publication.php';
+require_once '../includes/comment.php';
+require_once '../includes/captcha.php';
 require_once '../includes/translation.php';
 require_once '../includes/translation.php';
 
 
 // Translation system is auto-initialized when translation.php is included
 // Translation system is auto-initialized when translation.php is included
 
 
 $publication = new Publication();
 $publication = new Publication();
+$comment = new Comment();
 
 
 $id = (int)($_GET['id'] ?? 0);
 $id = (int)($_GET['id'] ?? 0);
 $pub = $publication->getById($id);
 $pub = $publication->getById($id);
@@ -35,6 +38,10 @@ if ($pub['categories_array']) {
     // Limit to 3 related publications
     // Limit to 3 related publications
     $relatedPublications = array_slice($relatedPublications, 0, 3);
     $relatedPublications = array_slice($relatedPublications, 0, 3);
 }
 }
+
+// Get approved comments for this publication
+$comments = $comment->getApprovedByPublication($id);
+$commentCount = $comment->getCountByPublication($id);
 ?>
 ?>
 <!DOCTYPE html>
 <!DOCTYPE html>
 <html lang="en">
 <html lang="en">
@@ -43,6 +50,7 @@ if ($pub['categories_array']) {
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title><?php echo htmlspecialchars($pub['title']); ?> - <?php echo SITE_TITLE; ?></title>
     <title><?php echo htmlspecialchars($pub['title']); ?> - <?php echo SITE_TITLE; ?></title>
     <link rel="stylesheet" href="../css/style.css">
     <link rel="stylesheet" href="../css/style.css">
+    <link rel="stylesheet" href="../css/comments.css">
 </head>
 </head>
 <body>
 <body>
     <header class="site-header">
     <header class="site-header">
@@ -94,6 +102,74 @@ if ($pub['categories_array']) {
             </div>
             </div>
         </article>
         </article>
 
 
+        <!-- Comments Section -->
+        <section class="comments-section">
+            <h3><?php echo t('comments'); ?> (<?php echo $commentCount; ?>)</h3>
+            
+            <?php if (!empty($comments)): ?>
+                <div class="comments-list">
+                    <?php foreach ($comments as $comment_item): ?>
+                        <div class="comment <?php echo $comment_item['admin_reply'] ? 'admin-reply' : ''; ?>" 
+                             data-comment-id="<?php echo $comment_item['id']; ?>">
+                            <div class="comment-header">
+                                <div class="comment-author">
+                                    <strong><?php echo htmlspecialchars($comment_item['name']); ?></strong>
+                                    <?php if ($comment_item['admin_reply']): ?>
+                                        <span class="admin-badge"><?php echo t('admin'); ?></span>
+                                    <?php endif; ?>
+                                </div>
+                                <div class="comment-date">
+                                    <?php echo date('M j, Y g:i A', strtotime($comment_item['created_at'])); ?>
+                                </div>
+                            </div>
+                            <div class="comment-content">
+                                <?php echo nl2br(htmlspecialchars($comment_item['content'])); ?>
+                            </div>
+                            <?php if ($comment_item['replied_by_username']): ?>
+                                <div class="reply-info">
+                                    <small><?php echo t('replied_by'); ?> <?php echo htmlspecialchars($comment_item['replied_by_username']); ?></small>
+                                </div>
+                            <?php endif; ?>
+                        </div>
+                    <?php endforeach; ?>
+                </div>
+            <?php else: ?>
+                <p class="no-comments"><?php echo t('no_comments_yet'); ?></p>
+            <?php endif; ?>
+            
+            <!-- Comment Form -->
+            <div class="comment-form-container">
+                <h4><?php echo t('leave_a_comment'); ?></h4>
+                <form id="comment-form" class="comment-form">
+                    <input type="hidden" name="publication_id" value="<?php echo $id; ?>">
+                    <input type="hidden" name="parent_id" value="">
+                    
+                    <div class="form-group">
+                        <label for="comment-name"><?php echo t('name'); ?> *</label>
+                        <input type="text" id="comment-name" name="name" required maxlength="100">
+                    </div>
+                    
+                    <div class="form-group">
+                        <label for="comment-email"><?php echo t('email'); ?></label>
+                        <input type="email" id="comment-email" name="email" maxlength="255">
+                        <small><?php echo t('email_optional'); ?></small>
+                    </div>
+                    
+                    <div class="form-group">
+                        <label for="comment-content"><?php echo t('comment'); ?> *</label>
+                        <textarea id="comment-content" name="content" required maxlength="2000" rows="4"></textarea>
+                    </div>
+                    
+                    <?php echo Captcha::getHtml(); ?>
+                    
+                    <div class="form-actions">
+                        <button type="submit" class="btn btn-primary"><?php echo t('submit_comment'); ?></button>
+                        <button type="button" class="btn btn-secondary cancel-reply" style="display: none;"><?php echo t('cancel'); ?></button>
+                    </div>
+                </form>
+            </div>
+        </section>
+
         <?php if (!empty($relatedPublications)): ?>
         <?php if (!empty($relatedPublications)): ?>
             <section class="related-publications">
             <section class="related-publications">
                 <h2><?php echo t('related_publications'); ?></h2>
                 <h2><?php echo t('related_publications'); ?></h2>
@@ -130,5 +206,139 @@ if ($pub['categories_array']) {
             <p>&copy; <?php echo date('Y'); ?> <?php echo SITE_TITLE; ?>. All rights reserved.</p>
             <p>&copy; <?php echo date('Y'); ?> <?php echo SITE_TITLE; ?>. All rights reserved.</p>
         </div>
         </div>
     </footer>
     </footer>
+<script>
+        // Comment System JavaScript
+        document.addEventListener('DOMContentLoaded', function() {
+            const commentForm = document.getElementById('comment-form');
+            const cancelReplyBtn = document.querySelector('.cancel-reply');
+            const parent_id_input = commentForm.querySelector('input[name="parent_id"]');
+            let isReplying = false;
+            
+            // Handle comment submission
+            commentForm.addEventListener('submit', function(e) {
+                e.preventDefault();
+                
+                const formData = new FormData(commentForm);
+                const submitBtn = commentForm.querySelector('button[type="submit"]');
+                const originalText = submitBtn.textContent;
+                
+                // Disable submit button
+                submitBtn.disabled = true;
+                submitBtn.textContent = '<?php echo t('submitting'); ?>...';
+                
+                // Submit comment via AJAX
+                fetch('submit_comment.php', {
+                    method: 'POST',
+                    body: formData
+                })
+                .then(response => response.json())
+                .then(data => {
+                    if (data.success) {
+                        // Show success message
+                        showMessage(data.message, 'success');
+                        
+                        // Reset form
+                        commentForm.reset();
+                        cancelReply();
+                        
+                        // Update comment count
+                        const commentCountElement = document.querySelector('.comments-section h3');
+                        if (commentCountElement) {
+                            commentCountElement.textContent = '<?php echo t('comments'); ?> (' + data.comment_count + ')';
+                        }
+                        
+                        // Reload comments after a short delay
+                        setTimeout(() => {
+                            window.location.reload();
+                        }, 1500);
+                    } else {
+                        showMessage(data.message, 'error');
+                    }
+                })
+                .catch(error => {
+                    console.error('Error:', error);
+                    showMessage('<?php echo t('comment_submit_error'); ?>', 'error');
+                })
+                .finally(() => {
+                    // Re-enable submit button
+                    submitBtn.disabled = false;
+                    submitBtn.textContent = originalText;
+                });
+            });
+            
+            // Handle reply buttons
+            document.addEventListener('click', function(e) {
+                if (e.target.classList.contains('reply-btn')) {
+                    e.preventDefault();
+                    
+                    const commentId = e.target.dataset.commentId;
+                    const commentElement = document.querySelector(`[data-comment-id="${commentId}"]`);
+                    const commentName = commentElement.querySelector('.comment-author strong').textContent;
+                    
+                    // Set parent ID
+                    parent_id_input.value = commentId;
+                    
+                    // Update form title
+                    const formTitle = document.querySelector('.comment-form-container h4');
+                    formTitle.textContent = '<?php echo t('replying_to'); ?> ' + commentName;
+                    
+                    // Show cancel button
+                    cancelReplyBtn.style.display = 'inline-block';
+                    
+                    // Scroll to form
+                    commentForm.scrollIntoView({ behavior: 'smooth' });
+                    
+                    isReplying = true;
+                }
+            });
+            
+            // Handle cancel reply
+            cancelReplyBtn.addEventListener('click', cancelReply);
+            
+            function cancelReply() {
+                parent_id_input.value = '';
+                const formTitle = document.querySelector('.comment-form-container h4');
+                formTitle.textContent = '<?php echo t('leave_a_comment'); ?>';
+                cancelReplyBtn.style.display = 'none';
+                isReplying = false;
+            }
+            
+            // Show message function
+            function showMessage(message, type) {
+                const messageDiv = document.createElement('div');
+                messageDiv.className = `alert alert-${type}`;
+                messageDiv.textContent = message;
+                
+                const formContainer = document.querySelector('.comment-form-container');
+                formContainer.insertBefore(messageDiv, formContainer.firstChild);
+                
+                // Remove message after 5 seconds
+                setTimeout(() => {
+                    messageDiv.remove();
+                }, 5000);
+            }
+            
+            // Refresh captcha on error
+            function refreshCaptcha() {
+                const captchaContainer = document.querySelector('.captcha-container');
+                if (captchaContainer) {
+                    fetch('submit_comment.php?action=refresh_captcha')
+                        .then(response => response.json())
+                        .then(data => {
+                            if (data.question) {
+                                const questionElement = captchaContainer.querySelector('.captcha-numbers');
+                                const operatorElement = captchaContainer.querySelector('.captcha-operator');
+                                if (questionElement) {
+                                    const parts = data.question.split(' ');
+                                    questionElement.textContent = parts[0] + ' ' + parts[2];
+                                    operatorElement.textContent = parts[1];
+                                }
+                            }
+                        })
+                        .catch(error => console.error('Error refreshing captcha:', error));
+                }
+            }
+        });
+    </script>
 </body>
 </body>
 </html>
 </html>

+ 80 - 0
public/submit_comment.php

@@ -0,0 +1,80 @@
+<?php
+// Start session for captcha verification
+if (session_status() === PHP_SESSION_NONE) {
+    session_start();
+}
+
+require_once '../includes/config.php';
+require_once '../includes/database.php';
+require_once '../includes/comment.php';
+require_once '../includes/captcha.php';
+require_once '../includes/translation.php';
+
+// Translation system is auto-initialized when translation.php is included
+
+header('Content-Type: application/json');
+
+$response = ['success' => false, 'message' => ''];
+
+// Handle captcha refresh
+if ($_GET['action'] === 'refresh_captcha') {
+    $response['success'] = true;
+    $response['question'] = Captcha::getAjaxCaptcha()['question'];
+    echo json_encode($response);
+    exit;
+}
+
+try {
+    // Only accept POST requests
+    if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+        throw new Exception('Invalid request method');
+    }
+    
+    // Get and validate input
+    $publicationId = (int)($_POST['publication_id'] ?? 0);
+    $parent_id = (int)($_POST['parent_id'] ?? 0);
+    $name = trim($_POST['name'] ?? '');
+    $email = trim($_POST['email'] ?? '');
+    $content = trim($_POST['content'] ?? '');
+    $captchaAnswer = $_POST['captcha_answer'] ?? '';
+    
+    // Verify captcha
+    if (!Captcha::verify($captchaAnswer)) {
+        throw new Exception(t('captcha_invalid'));
+    }
+    
+    // Prepare comment data
+    $commentData = [
+        'publication_id' => $publicationId,
+        'parent_id' => $parent_id ?: null,
+        'name' => $name,
+        'email' => $email ?: null,
+        'content' => $content
+    ];
+    
+    // Create comment instance
+    $comment = new Comment();
+    
+    // Validate comment data
+    $errors = $comment->validate($commentData, false);
+    if (!empty($errors)) {
+        throw new Exception(implode(', ', $errors));
+    }
+    
+    // Create comment
+    if ($comment->create($commentData)) {
+        $response['success'] = true;
+        $response['message'] = t('comment_submitted_success');
+        $response['comment_count'] = $comment->getCountByPublication($publicationId);
+    } else {
+        throw new Exception(t('comment_submit_error'));
+    }
+    
+} catch (Exception $e) {
+    $response['message'] = $e->getMessage();
+    
+    // Log error for debugging
+    error_log('Comment submission error: ' . $e->getMessage());
+}
+
+echo json_encode($response);

+ 24 - 0
setup.sql

@@ -68,6 +68,30 @@ CREATE TABLE IF NOT EXISTS images (
 -- Add foreign key constraint for images table
 -- Add foreign key constraint for images table
 ALTER TABLE images ADD CONSTRAINT fk_images_user FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL;
 ALTER TABLE images ADD CONSTRAINT fk_images_user FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL;
 
 
+-- Comments table for publication comments
+CREATE TABLE IF NOT EXISTS comments (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    publication_id INT NOT NULL,
+    parent_id INT NULL DEFAULT NULL,
+    name VARCHAR(100) NOT NULL,
+    email VARCHAR(255),
+    content TEXT NOT NULL,
+    status ENUM('pending', 'approved', 'rejected') DEFAULT 'pending',
+    ip_address VARCHAR(45),
+    user_agent TEXT,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    admin_reply BOOLEAN DEFAULT FALSE,
+    replied_by INT NULL,
+    FOREIGN KEY (publication_id) REFERENCES publications(id) ON DELETE CASCADE,
+    FOREIGN KEY (parent_id) REFERENCES comments(id) ON DELETE CASCADE,
+    FOREIGN KEY (replied_by) REFERENCES users(id) ON DELETE SET NULL,
+    INDEX idx_publication_id (publication_id),
+    INDEX idx_parent_id (parent_id),
+    INDEX idx_status (status),
+    INDEX idx_created_at (created_at)
+);
+
 -- Insert default admin user (password: admin123)
 -- Insert default admin user (password: admin123)
 INSERT INTO users (username, password, email, role, auth_type, status) VALUES 
 INSERT INTO users (username, password, email, role, auth_type, status) VALUES 
 ('admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin@example.com', 'admin', 'local', 'active');
 ('admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin@example.com', 'admin', 'local', 'active');