From f9ad3f4dc05252839457579303a4e0a0f94d8b80 Mon Sep 17 00:00:00 2001 From: Thomas Vanbesien Date: Sat, 21 Mar 2026 22:55:13 +0100 Subject: Add likes, comments, email notifications, and pagination to gallery --- src/app/Controllers/GalleryController.php | 89 ++++++++++++++++++++++++++++++- src/app/Mail.php | 10 ++++ src/app/Models/Comment.php | 39 ++++++++++++++ src/app/Models/Like.php | 59 ++++++++++++++++++++ src/app/Views/gallery/index.php | 64 ++++++++++++++-------- src/app/Views/partials/pagination.php | 20 +++++++ 6 files changed, 259 insertions(+), 22 deletions(-) create mode 100644 src/app/Models/Comment.php create mode 100644 src/app/Models/Like.php create mode 100644 src/app/Views/partials/pagination.php (limited to 'src/app') diff --git a/src/app/Controllers/GalleryController.php b/src/app/Controllers/GalleryController.php index 2edcd17..bc76e21 100644 --- a/src/app/Controllers/GalleryController.php +++ b/src/app/Controllers/GalleryController.php @@ -1,20 +1,30 @@ post = new Post(); + $this->like = new Like(); + $this->comment = new Comment(); } public function index(): void @@ -26,7 +36,84 @@ class GalleryController $totalPosts = $this->post->countAll(); $totalPages = max(1, (int) ceil($totalPosts / self::POSTS_PER_PAGE)); + $userId = $_SESSION['user_id'] ?? null; + + foreach ($posts as &$post) { + $post['like_count'] = $this->like->countByPost($post['id']); + $post['user_liked'] = $userId ? $this->like->hasUserLiked($userId, $post['id']) : false; + $post['comments'] = $this->comment->findByPostId($post['id']); + } + unset($post); + $content = __DIR__ . '/../Views/gallery/index.php'; include __DIR__ . '/../Views/layouts/main.php'; } + + public function like(string $id): void + { + if (!isset($_SESSION['user_id'])) { + header('Location: /login'); + return; + } + + if (!Csrf::validate($_POST['csrf_token'] ?? '')) { + Flash::set('error', 'Invalid CSRF token.'); + header('Location: /gallery'); + return; + } + + $this->like->toggle($_SESSION['user_id'], (int) $id); + + $page = (int) ($_POST['page'] ?? 1); + header('Location: /gallery?page=' . $page . '#post-' . $id); + } + + public function comment(string $id): void + { + if (!isset($_SESSION['user_id'])) { + header('Location: /login'); + return; + } + + if (!Csrf::validate($_POST['csrf_token'] ?? '')) { + Flash::set('error', 'Invalid CSRF token.'); + header('Location: /gallery'); + return; + } + + $content = trim($_POST['content'] ?? ''); + + if ($content === '') { + Flash::set('error', 'Comment cannot be empty.'); + $page = (int) ($_POST['page'] ?? 1); + header('Location: /gallery?page=' . $page . '#post-' . $id); + return; + } + + if (\strlen($content) > 500) { + Flash::set('error', 'Comment is too long (max 500 characters).'); + $page = (int) ($_POST['page'] ?? 1); + header('Location: /gallery?page=' . $page . '#post-' . $id); + return; + } + + $this->comment->create($_SESSION['user_id'], (int) $id, $content); + + // Notify the post owner if they have comment notifications enabled + $post = $this->post->findById((int) $id); + if ($post && $post['user_id'] !== $_SESSION['user_id']) { + $user = new User(); + $owner = $user->findById($post['user_id']); + if ($owner && $owner['notify_comments']) { + Mail::sendCommentNotification( + $owner['email'], + $_SESSION['username'], + (int) $id + ); + } + } + + $page = (int) ($_POST['page'] ?? 1); + header('Location: /gallery?page=' . $page . '#post-' . $id); + } } diff --git a/src/app/Mail.php b/src/app/Mail.php index 054c6e0..650c5b3 100644 --- a/src/app/Mail.php +++ b/src/app/Mail.php @@ -28,6 +28,16 @@ class Mail return self::send($to, $subject, $body); } + public static function sendCommentNotification(string $to, string $commenterUsername, int $postId): bool + { + $url = getenv('APP_URL') . '/gallery#post-' . $postId; + $subject = 'Camagru — New comment on your post'; + $body = '

' . htmlspecialchars($commenterUsername) . ' commented on your post.

' + . '

View the comment

'; + + return self::send($to, $subject, $body); + } + public static function sendPasswordReset(string $to, string $token): bool { $url = getenv('APP_URL') . '/reset-password?token=' . urlencode($token); diff --git a/src/app/Models/Comment.php b/src/app/Models/Comment.php new file mode 100644 index 0000000..464cfe2 --- /dev/null +++ b/src/app/Models/Comment.php @@ -0,0 +1,39 @@ +pdo = Database::getInstance()->getPdo(); + } + + public function create(int $userId, int $postId, string $content): int + { + $stmt = $this->pdo->prepare( + 'INSERT INTO comments (user_id, post_id, content) VALUES (:user_id, :post_id, :content)' + ); + $stmt->execute(['user_id' => $userId, 'post_id' => $postId, 'content' => $content]); + return (int) $this->pdo->lastInsertId(); + } + + public function findByPostId(int $postId): array + { + $stmt = $this->pdo->prepare( + 'SELECT comments.*, users.username FROM comments + JOIN users ON comments.user_id = users.id + WHERE comments.post_id = :post_id + ORDER BY comments.created_at ASC' + ); + $stmt->execute(['post_id' => $postId]); + return $stmt->fetchAll(); + } +} diff --git a/src/app/Models/Like.php b/src/app/Models/Like.php new file mode 100644 index 0000000..dc019a0 --- /dev/null +++ b/src/app/Models/Like.php @@ -0,0 +1,59 @@ +pdo = Database::getInstance()->getPdo(); + } + + /** + * Toggle a like: insert if not yet liked, delete if already liked. + * Returns true if the post is now liked, false if unliked. + */ + public function toggle(int $userId, int $postId): bool + { + // INSERT IGNORE silently fails when the UNIQUE(user_id, post_id) constraint + // is violated, meaning the user already liked this post + $stmt = $this->pdo->prepare( + 'INSERT IGNORE INTO likes (user_id, post_id) VALUES (:user_id, :post_id)' + ); + $stmt->execute(['user_id' => $userId, 'post_id' => $postId]); + + if ($stmt->rowCount() > 0) { + return true; + } + + // Row wasn't inserted → already liked → remove the like + $stmt = $this->pdo->prepare( + 'DELETE FROM likes WHERE user_id = :user_id AND post_id = :post_id' + ); + $stmt->execute(['user_id' => $userId, 'post_id' => $postId]); + return false; + } + + public function countByPost(int $postId): int + { + $stmt = $this->pdo->prepare('SELECT COUNT(*) FROM likes WHERE post_id = :post_id'); + $stmt->execute(['post_id' => $postId]); + return (int) $stmt->fetchColumn(); + } + + public function hasUserLiked(int $userId, int $postId): bool + { + $stmt = $this->pdo->prepare( + 'SELECT 1 FROM likes WHERE user_id = :user_id AND post_id = :post_id LIMIT 1' + ); + $stmt->execute(['user_id' => $userId, 'post_id' => $postId]); + return $stmt->fetch() !== false; + } +} diff --git a/src/app/Views/gallery/index.php b/src/app/Views/gallery/index.php index 62fd7f3..5269d74 100644 --- a/src/app/Views/gallery/index.php +++ b/src/app/Views/gallery/index.php @@ -1,6 +1,9 @@ - + diff --git a/src/app/Views/partials/pagination.php b/src/app/Views/partials/pagination.php new file mode 100644 index 0000000..1fcc499 --- /dev/null +++ b/src/app/Views/partials/pagination.php @@ -0,0 +1,20 @@ + + 1): ?> + + -- cgit v1.2.3