💬 Project Introduction: Discussion Forum Website
This project is a fully functional Discussion Forum Website designed for online communities to engage in conversations, ask questions, share knowledge, and build discussions around various topics. It provides a Reddit-like or Stack Overflow-like experience with categories, threads, posts, and user reputation systems.
The Problem it Solves:
Online communities need a platform where users can post questions, share answers, and engage in discussions. This system provides a structured way to organize conversations by categories, track user contributions, and maintain a healthy community through moderation tools.
Key Features:
- User Features:
- User registration and login system
- Create new discussion threads
- Post replies to existing threads
- Upvote/downvote posts (karma system)
- Mark best answers (solved threads)
- User profiles with post history
- Search functionality
- Subscribe to threads for notifications
- Report inappropriate content
- Admin Features:
- Dashboard with community statistics
- Category management (create, edit, delete)
- User management (ban, promote to moderator)
- Post moderation (edit, delete, feature)
- Reported content review
- Site settings configuration
- Announcement system
Technology Stack:
- Frontend: HTML5, CSS3, JavaScript (Fetch API, Markdown parser)
- Backend: PHP (Object-Oriented PHP with PDO)
- Database: MySQL
- Additional Libraries:
- Parsedown (Markdown parsing)
- PHPMailer (Email notifications)
📁 Project File Structure
discussion-forum/ │ ├── index.php # Homepage - Recent discussions ├── topics.php # Browse topics by category ├── thread.php # View single thread with replies ├── create-thread.php # Create new discussion thread ├── edit-post.php # Edit post ├── search.php # Search results ├── profile.php # User profile ├── login.php # User login ├── register.php # User registration ├── logout.php # Logout script │ ├── admin/ # Admin Panel │ ├── index.php # Admin login │ ├── dashboard.php # Admin dashboard │ ├── categories.php # Manage categories │ ├── users.php # Manage users │ ├── posts.php # Moderate posts │ ├── reports.php # Handle reports │ ├── settings.php # Forum settings │ └── logout.php # Admin logout │ ├── includes/ # Backend logic │ ├── config.php # Database connection │ ├── functions.php # Helper functions │ ├── auth.php # Authentication functions │ ├── session.php # Session management │ └── mailer.php # Email functions │ ├── api/ # AJAX endpoints │ ├── vote.php # Handle upvotes/downvotes │ ├── report.php # Submit reports │ ├── subscribe.php # Thread subscriptions │ └── search.php # Live search │ ├── assets/ # Static assets │ ├── css/ │ │ ├── style.css # Main styles │ │ └── admin.css # Admin styles │ ├── js/ │ │ ├── main.js # Main JavaScript │ │ ├── markdown.js # Markdown preview │ │ └── voting.js # Voting system │ ├── images/ │ │ ├── avatars/ # User avatars │ │ └── default-avatar.png # Default avatar │ └── vendor/ │ └── parsedown/ # Markdown parser │ └── database/ └── forum.sql # Database dump
🗄️ Database Setup (database/forum.sql)
Create a database named discussion_forum and run this SQL.
-- phpMyAdmin SQL Dump -- Database: `discussion_forum` CREATE DATABASE IF NOT EXISTS `discussion_forum`; USE `discussion_forum`; -- -------------------------------------------------------- -- Table structure for table `users` -- -------------------------------------------------------- CREATE TABLE `users` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `email` varchar(100) NOT NULL, `password` varchar(255) NOT NULL, `full_name` varchar(100) DEFAULT NULL, `bio` text DEFAULT NULL, `avatar` varchar(255) DEFAULT 'default-avatar.png', `role` enum('admin','moderator','member') DEFAULT 'member', `karma` int(11) DEFAULT 0, `posts_count` int(11) DEFAULT 0, `joined_at` timestamp NOT NULL DEFAULT current_timestamp(), `last_seen` timestamp NULL DEFAULT NULL, `status` enum('active','banned','inactive') DEFAULT 'active', PRIMARY KEY (`id`), UNIQUE KEY `username` (`username`), UNIQUE KEY `email` (`email`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Default admin (password: admin123) INSERT INTO `users` (`username`, `email`, `password`, `full_name`, `role`) VALUES ('admin', '[email protected]', '$2y$10$YourHashedPasswordHere', 'Administrator', 'admin'); -- Sample moderator (password: mod123) INSERT INTO `users` (`username`, `email`, `password`, `full_name`, `role`) VALUES ('moderator', '[email protected]', '$2y$10$YourHashedPasswordHere', 'Moderator', 'moderator'); -- Sample member (password: member123) INSERT INTO `users` (`username`, `email`, `password`, `full_name`, `role`) VALUES ('john_doe', '[email protected]', '$2y$10$YourHashedPasswordHere', 'John Doe', 'member'); -- -------------------------------------------------------- -- Table structure for table `categories` -- -------------------------------------------------------- CREATE TABLE `categories` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL, `slug` varchar(100) NOT NULL, `description` text DEFAULT NULL, `icon` varchar(50) DEFAULT '📁', `color` varchar(20) DEFAULT '#3498db', `threads_count` int(11) DEFAULT 0, `posts_count` int(11) DEFAULT 0, `last_post_id` int(11) DEFAULT NULL, `last_post_time` timestamp NULL DEFAULT NULL, `order` int(11) DEFAULT 0, `status` enum('active','hidden') DEFAULT 'active', `created_at` timestamp NOT NULL DEFAULT current_timestamp(), PRIMARY KEY (`id`), UNIQUE KEY `slug` (`slug`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Sample categories INSERT INTO `categories` (`name`, `slug`, `description`, `icon`, `color`) VALUES ('General Discussion', 'general', 'General discussions about anything and everything', '💬', '#3498db'), ('Technology', 'technology', 'Discuss latest tech trends, programming, and gadgets', '💻', '#2ecc71'), ('Gaming', 'gaming', 'Video games, gaming news, and discussions', '🎮', '#e74c3c'), ('Science', 'science', 'Scientific discoveries, research, and discussions', '🔬', '#9b59b6'), ('Health & Fitness', 'health', 'Health tips, fitness advice, and wellness', '💪', '#e67e22'), ('Business', 'business', 'Business strategies, entrepreneurship, and finance', '💼', '#f1c40f'); -- -------------------------------------------------------- -- Table structure for table `threads` -- -------------------------------------------------------- CREATE TABLE `threads` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(255) NOT NULL, `slug` varchar(255) NOT NULL, `category_id` int(11) NOT NULL, `user_id` int(11) NOT NULL, `views` int(11) DEFAULT 0, `replies_count` int(11) DEFAULT 0, `last_post_id` int(11) DEFAULT NULL, `last_post_time` timestamp NULL DEFAULT NULL, `is_pinned` tinyint(1) DEFAULT 0, `is_locked` tinyint(1) DEFAULT 0, `status` enum('active','hidden','deleted') DEFAULT 'active', `created_at` timestamp NOT NULL DEFAULT current_timestamp(), `updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp(), PRIMARY KEY (`id`), UNIQUE KEY `slug` (`slug`), KEY `category_id` (`category_id`), KEY `user_id` (`user_id`), KEY `last_post_id` (`last_post_id`), CONSTRAINT `threads_ibfk_1` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE CASCADE, CONSTRAINT `threads_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- -------------------------------------------------------- -- Table structure for table `posts` -- -------------------------------------------------------- CREATE TABLE `posts` ( `id` int(11) NOT NULL AUTO_INCREMENT, `thread_id` int(11) NOT NULL, `user_id` int(11) NOT NULL, `content` longtext NOT NULL, `is_first_post` tinyint(1) DEFAULT 0, `is_best_answer` tinyint(1) DEFAULT 0, `upvotes` int(11) DEFAULT 0, `downvotes` int(11) DEFAULT 0, `score` int(11) DEFAULT 0, `ip_address` varchar(45) DEFAULT NULL, `status` enum('active','hidden','deleted') DEFAULT 'active', `created_at` timestamp NOT NULL DEFAULT current_timestamp(), `updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp(), PRIMARY KEY (`id`), KEY `thread_id` (`thread_id`), KEY `user_id` (`user_id`), CONSTRAINT `posts_ibfk_1` FOREIGN KEY (`thread_id`) REFERENCES `threads` (`id`) ON DELETE CASCADE, CONSTRAINT `posts_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- -------------------------------------------------------- -- Table structure for table `votes` -- -------------------------------------------------------- CREATE TABLE `votes` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `post_id` int(11) NOT NULL, `vote_type` enum('upvote','downvote') NOT NULL, `created_at` timestamp NOT NULL DEFAULT current_timestamp(), PRIMARY KEY (`id`), UNIQUE KEY `user_post` (`user_id`,`post_id`), KEY `post_id` (`post_id`), CONSTRAINT `votes_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, CONSTRAINT `votes_ibfk_2` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- -------------------------------------------------------- -- Table structure for table `subscriptions` -- -------------------------------------------------------- CREATE TABLE `subscriptions` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `thread_id` int(11) NOT NULL, `created_at` timestamp NOT NULL DEFAULT current_timestamp(), PRIMARY KEY (`id`), UNIQUE KEY `user_thread` (`user_id`,`thread_id`), KEY `thread_id` (`thread_id`), CONSTRAINT `subscriptions_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, CONSTRAINT `subscriptions_ibfk_2` FOREIGN KEY (`thread_id`) REFERENCES `threads` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- -------------------------------------------------------- -- Table structure for table `reports` -- -------------------------------------------------------- CREATE TABLE `reports` ( `id` int(11) NOT NULL AUTO_INCREMENT, `post_id` int(11) NOT NULL, `reported_by` int(11) NOT NULL, `reason` enum('spam','harassment','inappropriate','offensive','other') NOT NULL, `details` text DEFAULT NULL, `status` enum('pending','reviewed','resolved') DEFAULT 'pending', `created_at` timestamp NOT NULL DEFAULT current_timestamp(), `resolved_at` timestamp NULL DEFAULT NULL, `resolved_by` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `post_id` (`post_id`), KEY `reported_by` (`reported_by`), KEY `resolved_by` (`resolved_by`), CONSTRAINT `reports_ibfk_1` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE, CONSTRAINT `reports_ibfk_2` FOREIGN KEY (`reported_by`) REFERENCES `users` (`id`) ON DELETE CASCADE, CONSTRAINT `reports_ibfk_3` FOREIGN KEY (`resolved_by`) REFERENCES `users` (`id`) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- -------------------------------------------------------- -- Table structure for table `bookmarks` -- -------------------------------------------------------- CREATE TABLE `bookmarks` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `thread_id` int(11) NOT NULL, `created_at` timestamp NOT NULL DEFAULT current_timestamp(), PRIMARY KEY (`id`), UNIQUE KEY `user_thread` (`user_id`,`thread_id`), KEY `thread_id` (`thread_id`), CONSTRAINT `bookmarks_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, CONSTRAINT `bookmarks_ibfk_2` FOREIGN KEY (`thread_id`) REFERENCES `threads` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- -------------------------------------------------------- -- Table structure for table `notifications` -- -------------------------------------------------------- CREATE TABLE `notifications` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `type` varchar(50) NOT NULL, `subject` varchar(255) NOT NULL, `content` text DEFAULT NULL, `link` varchar(255) DEFAULT NULL, `is_read` tinyint(1) DEFAULT 0, `created_at` timestamp NOT NULL DEFAULT current_timestamp(), PRIMARY KEY (`id`), KEY `user_id` (`user_id`), KEY `is_read` (`is_read`), CONSTRAINT `notifications_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- -------------------------------------------------------- -- Table structure for table `settings` -- -------------------------------------------------------- CREATE TABLE `settings` ( `id` int(11) NOT NULL AUTO_INCREMENT, `setting_key` varchar(100) NOT NULL, `setting_value` text DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `setting_key` (`setting_key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Default settings INSERT INTO `settings` (`setting_key`, `setting_value`) VALUES ('forum_title', 'Discussion Forum'), ('forum_description', 'A place for meaningful discussions'), ('forum_keywords', 'forum, discussion, community'), ('posts_per_page', '20'), ('threads_per_page', '20'), ('allow_registration', '1'), ('email_verification', '0'), ('min_username_length', '3'), ('max_username_length', '20'), ('min_password_length', '6'), ('allow_markdown', '1'), ('enable_karma', '1'), ('karma_per_upvote', '1'), ('karma_per_downvote', '-1'), ('karma_for_best_answer', '5'), ('contact_email', '[email protected]'); COMMIT; 💻 Core PHP Files
1. Database Configuration (includes/config.php)
<?php // Database configuration define('DB_HOST', 'localhost'); define('DB_NAME', 'discussion_forum'); define('DB_USER', 'root'); define('DB_PASS', ''); try { $pdo = new PDO( "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4", DB_USER, DB_PASS, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false ] ); } catch (PDOException $e) { die("Connection failed: " . $e->getMessage()); } // Start session if not started if (session_status() == PHP_SESSION_NONE) { session_start(); } // Load settings $settings = []; $stmt = $pdo->query("SELECT setting_key, setting_value FROM settings"); while ($row = $stmt->fetch()) { $settings[$row['setting_key']] = $row['setting_value']; } // Forum configuration define('FORUM_TITLE', $settings['forum_title'] ?? 'Discussion Forum'); define('FORUM_URL', 'http://localhost/discussion-forum/'); define('POSTS_PER_PAGE', $settings['posts_per_page'] ?? 20); define('THREADS_PER_PAGE', $settings['threads_per_page'] ?? 20); define('ENABLE_KARMA', $settings['enable_karma'] ?? 1); define('CONTACT_EMAIL', $settings['contact_email'] ?? '[email protected]'); // Helper functions function sanitize($data) { $data = trim($data); $data = stripslashes($data); $data = htmlspecialchars($data); return $data; } function redirect($url) { header("Location: $url"); exit; } function isLoggedIn() { return isset($_SESSION['user_id']); } function isAdmin() { return isset($_SESSION['user_role']) && $_SESSION['user_role'] === 'admin'; } function isModerator() { return isset($_SESSION['user_role']) && in_array($_SESSION['user_role'], ['admin', 'moderator']); } function timeAgo($timestamp) { $time_ago = strtotime($timestamp); $current_time = time(); $time_difference = $current_time - $time_ago; $seconds = $time_difference; $minutes = round($seconds / 60); $hours = round($seconds / 3600); $days = round($seconds / 86400); $weeks = round($seconds / 604800); $months = round($seconds / 2629440); $years = round($seconds / 31553280); if ($seconds <= 60) { return "Just now"; } else if ($minutes <= 60) { return ($minutes == 1) ? "1 minute ago" : "$minutes minutes ago"; } else if ($hours <= 24) { return ($hours == 1) ? "1 hour ago" : "$hours hours ago"; } else if ($days <= 7) { return ($days == 1) ? "yesterday" : "$days days ago"; } else if ($weeks <= 4.3) { return ($weeks == 1) ? "1 week ago" : "$weeks weeks ago"; } else if ($months <= 12) { return ($months == 1) ? "1 month ago" : "$months months ago"; } else { return ($years == 1) ? "1 year ago" : "$years years ago"; } } function createSlug($string) { $string = strtolower($string); $string = preg_replace('/[^a-z0-9-]/', '-', $string); $string = preg_replace('/-+/', '-', $string); return trim($string, '-'); } function getGravatar($email, $size = 80) { $hash = md5(strtolower(trim($email))); return "https://www.gravatar.com/avatar/$hash?s=$size&d=identicon"; } function updateUserKarma($user_id, $change) { global $pdo; $pdo->prepare("UPDATE users SET karma = karma + ? WHERE id = ?")->execute([$change, $user_id]); } function sendNotification($user_id, $type, $subject, $content, $link = null) { global $pdo; $stmt = $pdo->prepare("INSERT INTO notifications (user_id, type, subject, content, link) VALUES (?, ?, ?, ?, ?)"); return $stmt->execute([$user_id, $type, $subject, $content, $link]); } function getUnreadNotificationCount($user_id) { global $pdo; $stmt = $pdo->prepare("SELECT COUNT(*) FROM notifications WHERE user_id = ? AND is_read = 0"); $stmt->execute([$user_id]); return $stmt->fetchColumn(); } ?> 2. User Authentication (includes/auth.php)
<?php require_once 'config.php'; class Auth { public static function login($username, $password) { global $pdo; $stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? OR email = ?"); $stmt->execute([$username, $username]); $user = $stmt->fetch(); if ($user && password_verify($password, $user['password'])) { if ($user['status'] === 'banned') { return ['success' => false, 'message' => 'Your account has been banned']; } if ($user['status'] === 'inactive') { return ['success' => false, 'message' => 'Your account is inactive']; } $_SESSION['user_id'] = $user['id']; $_SESSION['user_name'] = $user['username']; $_SESSION['user_role'] = $user['role']; $_SESSION['user_avatar'] = $user['avatar']; $_SESSION['user_karma'] = $user['karma']; // Update last seen $pdo->prepare("UPDATE users SET last_seen = NOW() WHERE id = ?")->execute([$user['id']]); return ['success' => true, 'user' => $user]; } return ['success' => false, 'message' => 'Invalid username or password']; } public static function register($data) { global $pdo; // Validate username if (strlen($data['username']) < $GLOBALS['settings']['min_username_length'] ?? 3) { return ['success' => false, 'message' => 'Username too short']; } if (strlen($data['username']) > $GLOBALS['settings']['max_username_length'] ?? 20) { return ['success' => false, 'message' => 'Username too long']; } if (!preg_match('/^[a-zA-Z0-9_]+$/', $data['username'])) { return ['success' => false, 'message' => 'Username can only contain letters, numbers, and underscores']; } // Check if username exists $stmt = $pdo->prepare("SELECT id FROM users WHERE username = ?"); $stmt->execute([$data['username']]); if ($stmt->fetch()) { return ['success' => false, 'message' => 'Username already taken']; } // Check if email exists $stmt = $pdo->prepare("SELECT id FROM users WHERE email = ?"); $stmt->execute([$data['email']]); if ($stmt->fetch()) { return ['success' => false, 'message' => 'Email already registered']; } // Validate password if (strlen($data['password']) < $GLOBALS['settings']['min_password_length'] ?? 6) { return ['success' => false, 'message' => 'Password too short']; } // Hash password $hashedPassword = password_hash($data['password'], PASSWORD_DEFAULT); // Insert user $stmt = $pdo->prepare(" INSERT INTO users (username, email, password, full_name, bio, avatar) VALUES (?, ?, ?, ?, ?, ?) "); $avatar = 'default-avatar.png'; $success = $stmt->execute([ $data['username'], $data['email'], $hashedPassword, $data['full_name'] ?? null, $data['bio'] ?? null, $avatar ]); if ($success) { return ['success' => true, 'message' => 'Registration successful! Please login.']; } return ['success' => false, 'message' => 'Registration failed']; } public static function logout() { session_destroy(); redirect('login.php'); } public static function checkLogin() { if (!isset($_SESSION['user_id'])) { redirect('login.php'); } } public static function checkModerator() { self::checkLogin(); if (!isModerator()) { redirect('index.php'); } } public static function getCurrentUser() { global $pdo; if (!isset($_SESSION['user_id'])) { return null; } $stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?"); $stmt->execute([$_SESSION['user_id']]); return $stmt->fetch(); } } ?> 3. Homepage (index.php)
<?php require_once 'includes/config.php'; $page = isset($_GET['page']) ? (int)$_GET['page'] : 1; $offset = ($page - 1) * THREADS_PER_PAGE; // Get categories for sidebar $categories = $pdo->query(" SELECT c.*, (SELECT COUNT(*) FROM threads WHERE category_id = c.id) as thread_count FROM categories c WHERE c.status = 'active' ORDER BY c.order, c.name ")->fetchAll(); // Get recent threads $recent_threads = $pdo->prepare(" SELECT t.*, u.username as author, u.avatar as author_avatar, c.name as category_name, c.slug as category_slug, (SELECT COUNT(*) FROM posts WHERE thread_id = t.id) as post_count, (SELECT id FROM posts WHERE thread_id = t.id ORDER BY created_at DESC LIMIT 1) as last_post_id, (SELECT created_at FROM posts WHERE thread_id = t.id ORDER BY created_at DESC LIMIT 1) as last_post_time FROM threads t JOIN users u ON t.user_id = u.id JOIN categories c ON t.category_id = c.id WHERE t.status = 'active' ORDER BY t.last_post_time DESC, t.created_at DESC LIMIT ? OFFSET ? "); $recent_threads->execute([THREADS_PER_PAGE, $offset]); $threads = $recent_threads->fetchAll(); // Get total threads count for pagination $total_threads = $pdo->query("SELECT COUNT(*) FROM threads WHERE status = 'active'")->fetchColumn(); $total_pages = ceil($total_threads / THREADS_PER_PAGE); // Get stats $total_users = $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn(); $total_threads_count = $pdo->query("SELECT COUNT(*) FROM threads")->fetchColumn(); $total_posts_count = $pdo->query("SELECT COUNT(*) FROM posts")->fetchColumn(); $online_users = $pdo->query("SELECT COUNT(*) FROM users WHERE last_seen > DATE_SUB(NOW(), INTERVAL 15 MINUTE)")->fetchColumn(); ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title><?php echo FORUM_TITLE; ?> - Home</title> <link rel="stylesheet" href="assets/css/style.css"> </head> <body> <!-- Header --> <header class="forum-header"> <div class="container"> <div class="header-top"> <div class="logo"> <h1><a href="index.php"><?php echo FORUM_TITLE; ?></a></h1> </div> <div class="search-bar"> <form action="search.php" method="GET"> <input type="text" name="q" placeholder="Search discussions..." id="search-input"> <button type="submit">🔍</button> </form> </div> <div class="user-menu"> <?php if (isLoggedIn()): ?> <div class="user-dropdown"> <img src="assets/images/avatars/<?php echo $_SESSION['user_avatar']; ?>" alt="<?php echo $_SESSION['user_name']; ?>" class="avatar-small"> <span><?php echo $_SESSION['user_name']; ?></span> <span class="karma-badge">⚡ <?php echo $_SESSION['user_karma']; ?></span> <?php $unread = getUnreadNotificationCount($_SESSION['user_id']); ?> <?php if ($unread > 0): ?> <span class="notification-badge"><?php echo $unread; ?></span> <?php endif; ?> <div class="dropdown-menu"> <a href="profile.php">Profile</a> <a href="notifications.php">Notifications</a> <a href="bookmarks.php">Bookmarks</a> <?php if (isModerator()): ?> <a href="admin/dashboard.php">Moderator Panel</a> <?php endif; ?> <a href="logout.php">Logout</a> </div> </div> <?php else: ?> <a href="login.php" class="btn btn-small">Login</a> <a href="register.php" class="btn btn-primary btn-small">Register</a> <?php endif; ?> </div> </div> <nav class="main-nav"> <ul> <li><a href="index.php" class="active">Home</a></li> <li><a href="topics.php">Topics</a></li> <li><a href="recent.php">Recent</a></li> <li><a href="unanswered.php">Unanswered</a></li> <li><a href="create-thread.php" class="btn-create">+ New Discussion</a></li> </ul> </nav> </div> </header> <main class="container"> <div class="forum-layout"> <!-- Main Content --> <div class="forum-main"> <div class="forum-stats"> <span>📊 <?php echo number_format($total_users); ?> users</span> <span>📝 <?php echo number_format($total_threads_count); ?> discussions</span> <span>💬 <?php echo number_format($total_posts_count); ?> posts</span> <span>🟢 <?php echo $online_users; ?> online</span> </div> <div class="thread-list"> <div class="thread-list-header"> <h2>Recent Discussions</h2> <a href="create-thread.php" class="btn btn-primary">Start New Discussion</a> </div> <?php if (empty($threads)): ?> <div class="no-threads"> <p>No discussions yet. Be the first to start one!</p> </div> <?php else: ?> <?php foreach ($threads as $thread): ?> <div class="thread-item"> <div class="thread-stats"> <div class="stat"> <span class="stat-value"><?php echo $thread['post_count']; ?></span> <span class="stat-label">replies</span> </div> <div class="stat"> <span class="stat-value"><?php echo $thread['views']; ?></span> <span class="stat-label">views</span> </div> </div> <div class="thread-content"> <h3> <a href="thread.php?slug=<?php echo $thread['slug']; ?>"> <?php if ($thread['is_pinned']): ?>📌<?php endif; ?> <?php if ($thread['is_locked']): ?>🔒<?php endif; ?> <?php echo $thread['title']; ?> </a> </h3> <div class="thread-meta"> <span class="category-badge" style="background: <?php echo $categories[array_search($thread['category_id'], array_column($categories, 'id'))]['color'] ?? '#3498db'; ?>"> <a href="category.php?slug=<?php echo $thread['category_slug']; ?>"> <?php echo $thread['category_name']; ?> </a> </span> <span class="author"> Started by <a href="profile.php?user=<?php echo $thread['author']; ?>"> <?php echo $thread['author']; ?> </a> </span> <span class="time"> <?php echo timeAgo($thread['created_at']); ?> </span> <?php if ($thread['last_post_time']): ?> <span class="last-post"> Last post <?php echo timeAgo($thread['last_post_time']); ?> </span> <?php endif; ?> </div> </div> </div> <?php endforeach; ?> <!-- Pagination --> <?php if ($total_pages > 1): ?> <div class="pagination"> <?php if ($page > 1): ?> <a href="?page=<?php echo $page - 1; ?>" class="page-link">« Previous</a> <?php endif; ?> <?php for ($i = 1; $i <= $total_pages; $i++): ?> <a href="?page=<?php echo $i; ?>" class="page-link <?php echo $i == $page ? 'active' : ''; ?>"> <?php echo $i; ?> </a> <?php endfor; ?> <?php if ($page < $total_pages): ?> <a href="?page=<?php echo $page + 1; ?>" class="page-link">Next »</a> <?php endif; ?> </div> <?php endif; ?> <?php endif; ?> </div> </div> <!-- Sidebar --> <aside class="forum-sidebar"> <!-- Categories Widget --> <div class="widget"> <h3 class="widget-title">Categories</h3> <ul class="category-list"> <?php foreach ($categories as $cat): ?> <li> <a href="category.php?slug=<?php echo $cat['slug']; ?>"> <span class="cat-icon"><?php echo $cat['icon']; ?></span> <span class="cat-name"><?php echo $cat['name']; ?></span> <span class="cat-count"><?php echo $cat['thread_count']; ?></span> </a> </li> <?php endforeach; ?> </ul> </div> <!-- Statistics Widget --> <div class="widget"> <h3 class="widget-title">Forum Statistics</h3> <ul class="stats-list"> <li>Total Users: <strong><?php echo number_format($total_users); ?></strong></li> <li>Total Discussions: <strong><?php echo number_format($total_threads_count); ?></strong></li> <li>Total Posts: <strong><?php echo number_format($total_posts_count); ?></strong></li> <li>Online Users: <strong><?php echo $online_users; ?></strong></li> <li>Newest User: <strong> <?php $newest = $pdo->query("SELECT username FROM users ORDER BY joined_at DESC LIMIT 1")->fetch(); echo $newest['username']; ?> </strong> </li> </ul> </div> <!-- Active Users Widget --> <div class="widget"> <h3 class="widget-title">Active Users</h3> <div class="active-users"> <?php $active = $pdo->query(" SELECT username, avatar FROM users WHERE last_seen > DATE_SUB(NOW(), INTERVAL 15 MINUTE) ORDER BY last_seen DESC LIMIT 10 ")->fetchAll(); foreach ($active as $user): ?> <a href="profile.php?user=<?php echo $user['username']; ?>" class="active-user" title="<?php echo $user['username']; ?>"> <img src="assets/images/avatars/<?php echo $user['avatar']; ?>" alt="<?php echo $user['username']; ?>"> </a> <?php endforeach; ?> </div> </div> </aside> </div> </main> <!-- Footer --> <footer class="forum-footer"> <div class="container"> <p>© 2024 <?php echo FORUM_TITLE; ?>. All rights reserved.</p> </div> </footer> <script src="assets/js/main.js"></script> </body> </html> 4. Thread View Page (thread.php)
<?php require_once 'includes/config.php'; require_once 'vendor/parsedown/Parsedown.php'; $slug = $_GET['slug'] ?? ''; $page = isset($_GET['page']) ? (int)$_GET['page'] : 1; $offset = ($page - 1) * POSTS_PER_PAGE; if (empty($slug)) { redirect('index.php'); } // Get thread details $stmt = $pdo->prepare(" SELECT t.*, u.username as author, u.avatar as author_avatar, u.karma as author_karma, c.name as category_name, c.slug as category_slug FROM threads t JOIN users u ON t.user_id = u.id JOIN categories c ON t.category_id = c.id WHERE t.slug = ? AND t.status = 'active' "); $stmt->execute([$slug]); $thread = $stmt->fetch(); if (!$thread) { redirect('index.php'); } // Increment view count $pdo->prepare("UPDATE threads SET views = views + 1 WHERE id = ?")->execute([$thread['id']]); // Get posts for this thread $posts_stmt = $pdo->prepare(" SELECT p.*, u.username, u.avatar, u.karma, u.joined_at, u.posts_count, (SELECT COUNT(*) FROM votes WHERE post_id = p.id AND vote_type = 'upvote') as upvotes, (SELECT COUNT(*) FROM votes WHERE post_id = p.id AND vote_type = 'downvote') as downvotes FROM posts p JOIN users u ON p.user_id = u.id WHERE p.thread_id = ? AND p.status = 'active' ORDER BY p.created_at ASC LIMIT ? OFFSET ? "); $posts_stmt->execute([$thread['id'], POSTS_PER_PAGE, $offset]); $posts = $posts_stmt->fetchAll(); // Get total posts count $total_posts = $pdo->prepare("SELECT COUNT(*) FROM posts WHERE thread_id = ?"); $total_posts->execute([$thread['id']]); $total_posts_count = $total_posts->fetchColumn(); $total_pages = ceil($total_posts_count / POSTS_PER_PAGE); // Check if user has voted $user_votes = []; if (isLoggedIn()) { $vote_stmt = $pdo->prepare("SELECT post_id, vote_type FROM votes WHERE user_id = ?"); $vote_stmt->execute([$_SESSION['user_id']]); while ($vote = $vote_stmt->fetch()) { $user_votes[$vote['post_id']] = $vote['vote_type']; } } // Check if user is subscribed $is_subscribed = false; if (isLoggedIn()) { $sub_stmt = $pdo->prepare("SELECT id FROM subscriptions WHERE user_id = ? AND thread_id = ?"); $sub_stmt->execute([$_SESSION['user_id'], $thread['id']]); $is_subscribed = $sub_stmt->fetch() ? true : false; } // Handle reply submission $reply_error = ''; $reply_success = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['submit_reply'])) { if (!isLoggedIn()) { $reply_error = 'You must be logged in to reply'; } elseif ($thread['is_locked']) { $reply_error = 'This thread is locked'; } else { $content = $_POST['content']; if (empty($content)) { $reply_error = 'Please enter a reply'; } else { // Insert reply $stmt = $pdo->prepare(" INSERT INTO posts (thread_id, user_id, content, is_first_post, ip_address, created_at) VALUES (?, ?, ?, 0, ?, NOW()) "); $stmt->execute([ $thread['id'], $_SESSION['user_id'], $content, $_SERVER['REMOTE_ADDR'] ]); $post_id = $pdo->lastInsertId(); // Update thread last post info $pdo->prepare(" UPDATE threads SET replies_count = replies_count + 1, last_post_id = ?, last_post_time = NOW() WHERE id = ? ")->execute([$post_id, $thread['id']]); // Update user post count $pdo->prepare("UPDATE users SET posts_count = posts_count + 1 WHERE id = ?") ->execute([$_SESSION['user_id']]); // Notify thread author if ($thread['user_id'] != $_SESSION['user_id']) { sendNotification( $thread['user_id'], 'reply', 'New reply to your thread', $_SESSION['user_name'] . ' replied to your thread "' . $thread['title'] . '"', 'thread.php?slug=' . $thread['slug'] . '#post-' . $post_id ); } // Notify subscribers $subscribers = $pdo->prepare("SELECT user_id FROM subscriptions WHERE thread_id = ? AND user_id != ?"); $subscribers->execute([$thread['id'], $_SESSION['user_id']]); while ($sub = $subscribers->fetch()) { sendNotification( $sub['user_id'], 'reply', 'New reply in subscribed thread', $_SESSION['user_name'] . ' replied to "' . $thread['title'] . '"', 'thread.php?slug=' . $thread['slug'] . '#post-' . $post_id ); } $reply_success = 'Your reply has been posted'; // Redirect to avoid resubmission redirect('thread.php?slug=' . $thread['slug'] . '&page=' . $total_pages . '#post-' . $post_id); } } } // Initialize Markdown parser $Parsedown = new Parsedown(); ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title><?php echo $thread['title']; ?> - <?php echo FORUM_TITLE; ?></title> <link rel="stylesheet" href="assets/css/style.css"> </head> <body> <!-- Header --> <?php include 'includes/header.php'; ?> <main class="container"> <!-- Breadcrumb --> <div class="breadcrumb"> <a href="index.php">Home</a> » <a href="category.php?slug=<?php echo $thread['category_slug']; ?>"> <?php echo $thread['category_name']; ?> </a> » <span><?php echo $thread['title']; ?></span> </div> <!-- Thread Header --> <div class="thread-header"> <h1 class="thread-title"> <?php if ($thread['is_pinned']): ?>📌<?php endif; ?> <?php if ($thread['is_locked']): ?>🔒<?php endif; ?> <?php echo $thread['title']; ?> </h1> <div class="thread-actions"> <?php if (isLoggedIn()): ?> <button id="subscribe-btn" class="btn <?php echo $is_subscribed ? 'btn-success' : ''; ?>" onclick="toggleSubscribe(<?php echo $thread['id']; ?>)"> <?php echo $is_subscribed ? '✓ Subscribed' : '🔔 Subscribe'; ?> </button> <?php if (isModerator() || $thread['user_id'] == $_SESSION['user_id']): ?> <a href="edit-thread.php?id=<?php echo $thread['id']; ?>" class="btn">Edit</a> <?php endif; ?> <?php if (isModerator()): ?> <button class="btn" onclick="toggleLock(<?php echo $thread['id']; ?>)"> <?php echo $thread['is_locked'] ? 'Unlock' : 'Lock'; ?> </button> <button class="btn" onclick="togglePin(<?php echo $thread['id']; ?>)"> <?php echo $thread['is_pinned'] ? 'Unpin' : 'Pin'; ?> </button> <?php endif; ?> <?php endif; ?> </div> </div> <!-- Posts List --> <div class="posts-list"> <?php foreach ($posts as $index => $post): ?> <div class="post" id="post-<?php echo $post['id']; ?>"> <div class="post-sidebar"> <div class="post-author"> <img src="assets/images/avatars/<?php echo $post['avatar']; ?>" alt="<?php echo $post['username']; ?>" class="avatar-large"> <div class="author-info"> <a href="profile.php?user=<?php echo $post['username']; ?>" class="author-name"> <?php echo $post['username']; ?> </a> <span class="author-karma">⚡ <?php echo $post['karma']; ?></span> <span class="author-title"> <?php if ($post['username'] == $thread['author']) { echo '<span class="op-badge">OP</span>'; } ?> </span> </div> <div class="author-stats"> <span>Joined: <?php echo date('M Y', strtotime($post['joined_at'])); ?></span> <span>Posts: <?php echo $post['posts_count']; ?></span> </div> </div> </div> <div class="post-content"> <div class="post-meta"> <span class="post-date"><?php echo timeAgo($post['created_at']); ?></span> <?php if ($post['is_best_answer']): ?> <span class="best-answer">⭐ Best Answer</span> <?php endif; ?> <?php if ($index === 0): ?> <span class="first-post">First Post</span> <?php endif; ?> </div> <div class="post-body"> <?php echo $Parsedown->text($post['content']); ?> </div> <div class="post-footer"> <div class="post-votes"> <button class="vote-btn upvote <?php echo ($user_votes[$post['id']] ?? '') == 'upvote' ? 'voted' : ''; ?>" onclick="vote(<?php echo $post['id']; ?>, 'upvote')"> ▲ <span class="vote-count"><?php echo $post['upvotes']; ?></span> </button> <button class="vote-btn downvote <?php echo ($user_votes[$post['id']] ?? '') == 'downvote' ? 'voted' : ''; ?>" onclick="vote(<?php echo $post['id']; ?>, 'downvote')"> ▼ <span class="vote-count"><?php echo $post['downvotes']; ?></span> </button> </div> <div class="post-actions"> <a href="#reply-form" onclick="quotePost(<?php echo $post['id']; ?>, '<?php echo $post['username']; ?>')" class="action-link">Quote</a> <?php if (isLoggedIn() && !$post['is_best_answer'] && $thread['user_id'] == $_SESSION['user_id'] && $index > 0): ?> <a href="#" onclick="markBestAnswer(<?php echo $post['id']; ?>)" class="action-link">Mark as Best Answer</a> <?php endif; ?> <?php if (isLoggedIn() && $_SESSION['user_id'] != $post['user_id']): ?> <a href="#" onclick="reportPost(<?php echo $post['id']; ?>)" class="action-link">Report</a> <?php endif; ?> <?php if (isModerator() || $_SESSION['user_id'] == $post['user_id']): ?> <a href="edit-post.php?id=<?php echo $post['id']; ?>" class="action-link">Edit</a> <a href="#" onclick="deletePost(<?php echo $post['id']; ?>)" class="action-link">Delete</a> <?php endif; ?> </div> </div> <?php if ($post['updated_at']): ?> <div class="post-edited"> Last edited <?php echo timeAgo($post['updated_at']); ?> </div> <?php endif; ?> </div> </div> <?php endforeach; ?> <!-- Pagination --> <?php if ($total_pages > 1): ?> <div class="pagination"> <?php if ($page > 1): ?> <a href="?slug=<?php echo $slug; ?>&page=<?php echo $page - 1; ?>" class="page-link">« Previous</a> <?php endif; ?> <?php for ($i = 1; $i <= $total_pages; $i++): ?> <a href="?slug=<?php echo $slug; ?>&page=<?php echo $i; ?>" class="page-link <?php echo $i == $page ? 'active' : ''; ?>"> <?php echo $i; ?> </a> <?php endfor; ?> <?php if ($page < $total_pages): ?> <a href="?slug=<?php echo $slug; ?>&page=<?php echo $page + 1; ?>" class="page-link">Next »</a> <?php endif; ?> </div> <?php endif; ?> </div> <!-- Reply Form --> <?php if (!isLoggedIn()): ?> <div class="login-to-reply"> <p>Please <a href="login.php">login</a> or <a href="register.php">register</a> to reply to this thread.</p> </div> <?php elseif ($thread['is_locked']): ?> <div class="thread-locked"> <p>This thread has been locked. No further replies can be posted.</p> </div> <?php else: ?> <div class="reply-form-container" id="reply-form"> <h3>Post a Reply</h3> <?php if ($reply_error): ?> <div class="alert alert-error"><?php echo $reply_error; ?></div> <?php endif; ?> <?php if ($reply_success): ?> <div class="alert alert-success"><?php echo $reply_success; ?></div> <?php endif; ?> <form method="POST" action="" class="reply-form"> <div class="form-group"> <textarea id="reply-content" name="content" rows="8" placeholder="Write your reply... (Markdown supported)"></textarea> <div class="markdown-preview" id="preview"></div> </div> <div class="form-actions"> <button type="button" class="btn" onclick="togglePreview()">Preview</button> <button type="submit" name="submit_reply" class="btn btn-primary">Post Reply</button> </div> </form> </div> <?php endif; ?> </main> <!-- Footer --> <?php include 'includes/footer.php'; ?> <script src="assets/js/markdown.js"></script> <script src="assets/js/voting.js"></script> <script> function vote(postId, type) { <?php if (!isLoggedIn()): ?> alert('Please login to vote'); return; <?php endif; ?> fetch('api/vote.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: `post_id=${postId}&type=${type}` }) .then(response => response.json()) .then(data => { if (data.success) { location.reload(); } else { alert(data.message); } }); } function toggleSubscribe(threadId) { fetch('api/subscribe.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: `thread_id=${threadId}` }) .then(response => response.json()) .then(data => { if (data.success) { const btn = document.getElementById('subscribe-btn'); if (data.subscribed) { btn.textContent = '✓ Subscribed'; btn.classList.add('btn-success'); } else { btn.textContent = '🔔 Subscribe'; btn.classList.remove('btn-success'); } } }); } function quotePost(postId, username) { // Get post content (simplified - in production you'd fetch this) const textarea = document.getElementById('reply-content'); textarea.value += `> *Quoting ${username}*\n\n`; textarea.focus(); } function togglePreview() { const preview = document.getElementById('preview'); const content = document.getElementById('reply-content').value; if (preview.style.display === 'none') { // Convert markdown to HTML (using simple markdown library) preview.innerHTML = marked.parse(content); preview.style.display = 'block'; } else { preview.style.display = 'none'; } } </script> </body> </html> 5. Create Thread Page (create-thread.php)
<?php require_once 'includes/config.php'; require_once 'includes/auth.php'; Auth::checkLogin(); // Get categories $categories = $pdo->query("SELECT * FROM categories WHERE status = 'active' ORDER BY name")->fetchAll(); $errors = []; $form_data = [ 'title' => '', 'category_id' => '', 'content' => '' ]; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $form_data['title'] = sanitize($_POST['title']); $form_data['category_id'] = (int)$_POST['category_id']; $form_data['content'] = $_POST['content']; // Validate if (empty($form_data['title'])) { $errors['title'] = 'Title is required'; } elseif (strlen($form_data['title']) < 5) { $errors['title'] = 'Title must be at least 5 characters'; } if (empty($form_data['category_id'])) { $errors['category_id'] = 'Please select a category'; } if (empty($form_data['content'])) { $errors['content'] = 'Content is required'; } elseif (strlen($form_data['content']) < 10) { $errors['content'] = 'Content must be at least 10 characters'; } if (empty($errors)) { try { $pdo->beginTransaction(); // Create slug $slug = createSlug($form_data['title']); // Check if slug exists and make it unique $check = $pdo->prepare("SELECT id FROM threads WHERE slug = ?"); $check->execute([$slug]); if ($check->fetch()) { $slug .= '-' . uniqid(); } // Insert thread $stmt = $pdo->prepare(" INSERT INTO threads (title, slug, category_id, user_id, created_at) VALUES (?, ?, ?, ?, NOW()) "); $stmt->execute([ $form_data['title'], $slug, $form_data['category_id'], $_SESSION['user_id'] ]); $thread_id = $pdo->lastInsertId(); // Insert first post $stmt = $pdo->prepare(" INSERT INTO posts (thread_id, user_id, content, is_first_post, ip_address, created_at) VALUES (?, ?, ?, 1, ?, NOW()) "); $stmt->execute([ $thread_id, $_SESSION['user_id'], $form_data['content'], $_SERVER['REMOTE_ADDR'] ]); $post_id = $pdo->lastInsertId(); // Update thread with last post info $pdo->prepare(" UPDATE threads SET last_post_id = ?, last_post_time = NOW() WHERE id = ? ")->execute([$post_id, $thread_id]); // Update category thread count $pdo->prepare(" UPDATE categories SET threads_count = threads_count + 1, posts_count = posts_count + 1, last_post_id = ?, last_post_time = NOW() WHERE id = ? ")->execute([$post_id, $form_data['category_id']]); // Update user post count $pdo->prepare("UPDATE users SET posts_count = posts_count + 1 WHERE id = ?") ->execute([$_SESSION['user_id']]); $pdo->commit(); // Redirect to the new thread redirect('thread.php?slug=' . $slug); } catch (Exception $e) { $pdo->rollBack(); $errors['general'] = 'Failed to create thread: ' . $e->getMessage(); } } } ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Create New Discussion - <?php echo FORUM_TITLE; ?></title> <link rel="stylesheet" href="assets/css/style.css"> </head> <body> <!-- Header --> <?php include 'includes/header.php'; ?> <main class="container"> <div class="create-thread-container"> <h1>Start a New Discussion</h1> <?php if (!empty($errors['general'])): ?> <div class="alert alert-error"><?php echo $errors['general']; ?></div> <?php endif; ?> <form method="POST" action="" class="create-thread-form"> <div class="form-group"> <label for="title">Discussion Title</label> <input type="text" id="title" name="title" value="<?php echo htmlspecialchars($form_data['title']); ?>" placeholder="Enter a descriptive title" required> <?php if (isset($errors['title'])): ?> <span class="error"><?php echo $errors['title']; ?></span> <?php endif; ?> </div> <div class="form-group"> <label for="category">Category</label> <select id="category" name="category_id" required> <option value="">Select a category</option> <?php foreach ($categories as $cat): ?> <option value="<?php echo $cat['id']; ?>" <?php echo $form_data['category_id'] == $cat['id'] ? 'selected' : ''; ?>> <?php echo $cat['icon']; ?> <?php echo $cat['name']; ?> </option> <?php endforeach; ?> </select> <?php if (isset($errors['category_id'])): ?> <span class="error"><?php echo $errors['category_id']; ?></span> <?php endif; ?> </div> <div class="form-group"> <label for="content">Content</label> <textarea id="content" name="content" rows="12" placeholder="Write your discussion content here... (Markdown supported)"><?php echo htmlspecialchars($form_data['content']); ?></textarea> <?php if (isset($errors['content'])): ?> <span class="error"><?php echo $errors['content']; ?></span> <?php endif; ?> <div class="markdown-help"> <p>💡 <strong>Markdown supported:</strong> **bold**, *italic*, [links](url), `code`, lists, etc.</p> </div> </div> <div class="form-actions"> <button type="submit" class="btn btn-primary btn-large">Create Discussion</button> <a href="index.php" class="btn btn-large">Cancel</a> </div> </form> </div> </main> <!-- Footer --> <?php include 'includes/footer.php'; ?> <script src="assets/js/markdown.js"></script> </body> </html> 6. Admin Dashboard (admin/dashboard.php)
<?php require_once '../includes/config.php'; require_once '../includes/auth.php'; Auth::checkModerator(); // Get statistics $totalUsers = $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn(); $totalThreads = $pdo->query("SELECT COUNT(*) FROM threads")->fetchColumn(); $totalPosts = $pdo->query("SELECT COUNT(*) FROM posts")->fetchColumn(); $pendingReports = $pdo->query("SELECT COUNT(*) FROM reports WHERE status = 'pending'")->fetchColumn(); // Get recent users $recentUsers = $pdo->query(" SELECT * FROM users ORDER BY joined_at DESC LIMIT 5 ")->fetchAll(); // Get recent reports $recentReports = $pdo->prepare(" SELECT r.*, p.thread_id, p.content as post_content, u.username as reported_by_name, pu.username as post_author FROM reports r JOIN posts p ON r.post_id = p.id JOIN users u ON r.reported_by = u.id JOIN users pu ON p.user_id = pu.id WHERE r.status = 'pending' ORDER BY r.created_at DESC LIMIT 10 "); $recentReports->execute(); $reports = $recentReports->fetchAll(); // Get popular threads $popularThreads = $pdo->query(" SELECT t.*, u.username, c.name as category_name, (SELECT COUNT(*) FROM posts WHERE thread_id = t.id) as reply_count FROM threads t JOIN users u ON t.user_id = u.id JOIN categories c ON t.category_id = c.id ORDER BY t.views DESC LIMIT 5 ")->fetchAll(); ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Admin Dashboard - <?php echo FORUM_TITLE; ?></title> <link rel="stylesheet" href="../assets/css/admin.css"> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> </head> <body> <div class="admin-wrapper"> <!-- Sidebar --> <aside class="sidebar"> <div class="sidebar-header"> <h2>Forum Admin</h2> <p>Welcome, <?php echo $_SESSION['user_name']; ?></p> </div> <nav class="sidebar-nav"> <ul> <li><a href="dashboard.php" class="active">Dashboard</a></li> <li><a href="categories.php">Categories</a></li> <li><a href="users.php">Users</a></li> <li><a href="posts.php">Posts</a></li> <li><a href="reports.php">Reports <?php if ($pendingReports > 0): ?><span class="badge"><?php echo $pendingReports; ?></span><?php endif; ?></a></li> <li><a href="settings.php">Settings</a></li> <li><a href="logout.php">Logout</a></li> </ul> </nav> </aside> <!-- Main Content --> <main class="admin-main"> <div class="container"> <h1>Dashboard</h1> <!-- Stats Cards --> <div class="stats-grid"> <div class="stat-card"> <div class="stat-icon">👥</div> <div class="stat-details"> <h3><?php echo number_format($totalUsers); ?></h3> <p>Total Users</p> </div> </div> <div class="stat-card"> <div class="stat-icon">📝</div> <div class="stat-details"> <h3><?php echo number_format($totalThreads); ?></h3> <p>Discussions</p> </div> </div> <div class="stat-card"> <div class="stat-icon">💬</div> <div class="stat-details"> <h3><?php echo number_format($totalPosts); ?></h3> <p>Posts</p> </div> </div> <div class="stat-card"> <div class="stat-icon">🚩</div> <div class="stat-details"> <h3><?php echo $pendingReports; ?></h3> <p>Pending Reports</p> </div> </div> </div> <!-- Quick Actions --> <div class="quick-actions"> <a href="categories.php" class="action-card"> <span class="action-icon">📁</span> <span>Manage Categories</span> </a> <a href="users.php" class="action-card"> <span class="action-icon">👥</span> <span>Manage Users</span> </a> <a href="reports.php" class="action-card"> <span class="action-icon">🚩</span> <span>Review Reports</span> <?php if ($pendingReports > 0): ?> <span class="badge"><?php echo $pendingReports; ?></span> <?php endif; ?> </a> <a href="settings.php" class="action-card"> <span class="action-icon">⚙️</span> <span>Forum Settings</span> </a> </div> <div class="dashboard-grid"> <!-- Recent Reports --> <div class="card"> <div class="card-header"> <h3>Recent Reports</h3> <a href="reports.php" class="btn btn-sm">View All</a> </div> <div class="card-body"> <?php if (empty($reports)): ?> <p>No pending reports</p> <?php else: ?> <table class="data-table"> <thead> <tr> <th>Reported Post</th> <th>Author</th> <th>Reported By</th> <th>Reason</th> <th>Actions</th> </tr> </thead> <tbody> <?php foreach ($reports as $report): ?> <tr> <td><?php echo substr($report['post_content'], 0, 50); ?>...</td> <td><?php echo $report['post_author']; ?></td> <td><?php echo $report['reported_by_name']; ?></td> <td><?php echo $report['reason']; ?></td> <td> <a href="view-post.php?id=<?php echo $report['post_id']; ?>" class="btn-small">View</a> <a href="?dismiss=<?php echo $report['id']; ?>" class="btn-small">Dismiss</a> </td> </tr> <?php endforeach; ?> </tbody> </table> <?php endif; ?> </div> </div> <!-- Popular Threads --> <div class="card"> <div class="card-header"> <h3>Popular Threads</h3> </div> <div class="card-body"> <table class="data-table"> <thead> <tr> <th>Title</th> <th>Category</th> <th>Author</th> <th>Views</th> <th>Replies</th> </tr> </thead> <tbody> <?php foreach ($popularThreads as $thread): ?> <tr> <td> <a href="../thread.php?slug=<?php echo $thread['slug']; ?>" target="_blank"> <?php echo $thread['title']; ?> </a> </td> <td><?php echo $thread['category_name']; ?></td> <td><?php echo $thread['username']; ?></td> <td><?php echo number_format($thread['views']); ?></td> <td><?php echo $thread['reply_count']; ?></td> </tr> <?php endforeach; ?> </tbody> </table> </div> </div> <!-- Recent Users --> <div class="card"> <div class="card-header"> <h3>Recent Users</h3> <a href="users.php" class="btn btn-sm">View All</a> </div> <div class="card-body"> <table class="data-table"> <thead> <tr> <th>Username</th> <th>Email</th> <th>Joined</th> <th>Status</th> <th>Actions</th> </tr> </thead> <tbody> <?php foreach ($recentUsers as $user): ?> <tr> <td><?php echo $user['username']; ?></td> <td><?php echo $user['email']; ?></td> <td><?php echo date('M d, Y', strtotime($user['joined_at'])); ?></td> <td> <span class="badge badge-<?php echo $user['status']; ?>"> <?php echo $user['status']; ?> </span> </td> <td> <a href="edit-user.php?id=<?php echo $user['id']; ?>" class="btn-small">Edit</a> </td> </tr> <?php endforeach; ?> </tbody> </table> </div> </div> </div> </div> </main> </div> <script src="../assets/js/admin.js"></script> </body> </html> 7. CSS Styling (assets/css/style.css)
/* Discussion Forum Styles */ :root { --primary-color: #3498db; --secondary-color: #2ecc71; --danger-color: #e74c3c; --warning-color: #f39c12; --dark-color: #2c3e50; --light-color: #ecf0f1; --text-color: #333; --border-color: #ddd; --success-color: #27ae60; --info-color: #3498db; } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; line-height: 1.6; color: var(--text-color); background: #f4f6f9; } .container { max-width: 1200px; margin: 0 auto; padding: 0 20px; } /* Header Styles */ .forum-header { background: #fff; box-shadow: 0 2px 5px rgba(0,0,0,0.1); margin-bottom: 20px; } .header-top { display: flex; justify-content: space-between; align-items: center; padding: 15px 0; border-bottom: 1px solid var(--border-color); } .logo h1 { font-size: 1.8rem; } .logo a { text-decoration: none; color: var(--dark-color); } .search-bar { flex: 0 1 400px; } .search-bar form { display: flex; } .search-bar input { flex: 1; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: 4px 0 0 4px; font-size: 14px; } .search-bar button { padding: 8px 15px; background: var(--primary-color); color: #fff; border: none; border-radius: 0 4px 4px 0; cursor: pointer; } .user-menu { display: flex; align-items: center; gap: 10px; } .user-dropdown { position: relative; display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 5px 10px; border-radius: 4px; } .user-dropdown:hover { background: var(--light-color); } .avatar-small { width: 32px; height: 32px; border-radius: 50%; object-fit: cover; } .karma-badge { background: var(--warning-color); color: #fff; padding: 2px 6px; border-radius: 3px; font-size: 12px; } .notification-badge { background: var(--danger-color); color: #fff; padding: 2px 6px; border-radius: 10px; font-size: 11px; position: absolute; top: -5px; right: -5px; } .dropdown-menu { position: absolute; top: 100%; right: 0; background: #fff; border: 1px solid var(--border-color); border-radius: 4px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); display: none; z-index: 1000; min-width: 150px; } .user-dropdown:hover .dropdown-menu { display: block; } .dropdown-menu a { display: block; padding: 10px 15px; color: var(--text-color); text-decoration: none; border-bottom: 1px solid var(--border-color); } .dropdown-menu a:hover { background: var(--light-color); } .main-nav { padding: 10px 0; } .main-nav ul { list-style: none; display: flex; gap: 20px; } .main-nav a { text-decoration: none; color: var(--text-color); font-weight: 500; padding: 5px 10px; border-radius: 4px; } .main-nav a:hover, .main-nav a.active { background: var(--light-color); color: var(--primary-color); } .btn-create { background: var(--success-color); color: #fff !important; } .btn-create:hover { background: #219a52 !important; color: #fff !important; } /* Forum Layout */ .forum-layout { display: grid; grid-template-columns: 1fr 300px; gap: 20px; } .forum-stats { background: #fff; padding: 15px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); margin-bottom: 20px; display: flex; gap: 30px; justify-content: center; } .forum-stats span { display: flex; align-items: center; gap: 5px; color: #666; } /* Thread List */ .thread-list { background: #fff; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); overflow: hidden; } .thread-list-header { padding: 15px 20px; background: var(--light-color); border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; } .thread-list-header h2 { font-size: 1.3rem; color: var(--dark-color); } .thread-item { display: flex; padding: 15px 20px; border-bottom: 1px solid var(--border-color); transition: background 0.3s; } .thread-item:hover { background: #f9f9f9; } .thread-stats { display: flex; gap: 20px; min-width: 120px; } .stat { text-align: center; } .stat-value { display: block; font-size: 1.2rem; font-weight: bold; color: var(--dark-color); } .stat-label { font-size: 0.8rem; color: #999; } .thread-content { flex: 1; } .thread-content h3 { font-size: 1.1rem; margin-bottom: 8px; } .thread-content h3 a { color: var(--dark-color); text-decoration: none; } .thread-content h3 a:hover { color: var(--primary-color); } .thread-meta { display: flex; gap: 15px; font-size: 0.9rem; color: #666; } .category-badge a { color: #fff; text-decoration: none; } .category-badge a:hover { text-decoration: underline; } .author a { color: var(--primary-color); text-decoration: none; font-weight: 500; } .time, .last-post { color: #999; } /* Thread View */ .thread-header { background: #fff; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); display: flex; justify-content: space-between; align-items: center; } .thread-title { font-size: 2rem; color: var(--dark-color); } .thread-actions { display: flex; gap: 10px; } .posts-list { margin-bottom: 30px; } .post { background: #fff; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); margin-bottom: 20px; display: flex; overflow: hidden; } .post-sidebar { width: 200px; background: #f9f9f9; padding: 20px; border-right: 1px solid var(--border-color); } .post-author { text-align: center; } .avatar-large { width: 100px; height: 100px; border-radius: 50%; object-fit: cover; margin-bottom: 10px; } .author-info { margin-bottom: 10px; } .author-name { display: block; font-size: 1.1rem; font-weight: bold; color: var(--dark-color); text-decoration: none; margin-bottom: 5px; } .author-name:hover { color: var(--primary-color); } .author-karma { display: inline-block; background: var(--warning-color); color: #fff; padding: 2px 8px; border-radius: 3px; font-size: 0.9rem; margin-bottom: 5px; } .op-badge { display: inline-block; background: var(--primary-color); color: #fff; padding: 2px 8px; border-radius: 3px; font-size: 0.8rem; } .author-stats { font-size: 0.8rem; color: #999; } .author-stats span { display: block; margin-bottom: 3px; } .post-content { flex: 1; padding: 20px; } .post-meta { display: flex; gap: 15px; margin-bottom: 15px; font-size: 0.9rem; color: #999; padding-bottom: 10px; border-bottom: 1px solid var(--border-color); } .best-answer { background: var(--success-color); color: #fff; padding: 2px 8px; border-radius: 3px; font-size: 0.8rem; } .first-post { background: var(--info-color); color: #fff; padding: 2px 8px; border-radius: 3px; font-size: 0.8rem; } .post-body { margin-bottom: 20px; line-height: 1.7; } .post-body p { margin-bottom: 15px; } .post-body code { background: #f4f4f4; padding: 2px 5px; border-radius: 3px; font-family: 'Courier New', monospace; } .post-body pre { background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; margin: 15px 0; } .post-body blockquote { border-left: 4px solid var(--primary-color); padding-left: 15px; margin: 15px 0; color: #666; font-style: italic; } .post-footer { display: flex; justify-content: space-between; align-items: center; padding-top: 15px; border-top: 1px solid var(--border-color); } .post-votes { display: flex; gap: 10px; } .vote-btn { display: flex; align-items: center; gap: 5px; padding: 5px 10px; border: 1px solid var(--border-color); border-radius: 4px; background: #fff; cursor: pointer; color: #666; } .vote-btn.voted { background: var(--primary-color); color: #fff; border-color: var(--primary-color); } .vote-btn.upvote.voted { background: var(--success-color); } .vote-btn.downvote.voted { background: var(--danger-color); } .post-actions { display: flex; gap: 10px; } .action-link { color: #999; text-decoration: none; font-size: 0.9rem; } .action-link:hover { color: var(--primary-color); } .post-edited { margin-top: 15px; font-size: 0.8rem; color: #999; font-style: italic; } /* Reply Form */ .reply-form-container { background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); } .reply-form textarea { width: 100%; padding: 15px; border: 1px solid var(--border-color); border-radius: 4px; font-size: 1rem; font-family: inherit; } .markdown-preview { display: none; margin-top: 15px; padding: 15px; background: #f9f9f9; border-radius: 4px; border: 1px solid var(--border-color); } /* Categories Sidebar */ .widget { background: #fff; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); } .widget-title { font-size: 1.2rem; color: var(--dark-color); margin-bottom: 15px; padding-bottom: 8px; border-bottom: 2px solid var(--primary-color); } .category-list { list-style: none; } .category-list li { margin-bottom: 8px; } .category-list a { display: flex; align-items: center; padding: 8px; text-decoration: none; color: var(--text-color); border-radius: 4px; transition: background 0.3s; } .category-list a:hover { background: var(--light-color); } .cat-icon { width: 30px; font-size: 1.2rem; } .cat-name { flex: 1; } .cat-count { background: var(--light-color); padding: 2px 6px; border-radius: 3px; font-size: 0.8rem; color: #666; } .stats-list { list-style: none; } .stats-list li { padding: 8px 0; border-bottom: 1px solid var(--border-color); } .stats-list li:last-child { border-bottom: none; } .active-users { display: flex; flex-wrap: wrap; gap: 10px; } .active-user img { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; } /* Create Thread Form */ .create-thread-container { max-width: 800px; margin: 0 auto; background: #fff; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } .create-thread-container h1 { margin-bottom: 30px; color: var(--dark-color); } .create-thread-form .form-group { margin-bottom: 20px; } .create-thread-form label { display: block; margin-bottom: 8px; font-weight: 500; color: var(--dark-color); } .create-thread-form input[type="text"], .create-thread-form select, .create-thread-form textarea { width: 100%; padding: 12px; border: 1px solid var(--border-color); border-radius: 4px; font-size: 1rem; } .create-thread-form textarea { font-family: inherit; } .markdown-help { margin-top: 5px; font-size: 0.9rem; color: #999; } .form-actions { display: flex; gap: 15px; justify-content: flex-end; } /* Pagination */ .pagination { display: flex; justify-content: center; gap: 5px; margin: 30px 0; } .page-link { padding: 8px 15px; background: #fff; color: var(--text-color); text-decoration: none; border: 1px solid var(--border-color); border-radius: 4px; transition: all 0.3s; } .page-link:hover, .page-link.active { background: var(--primary-color); color: #fff; border-color: var(--primary-color); } /* Breadcrumb */ .breadcrumb { margin-bottom: 20px; font-size: 0.9rem; color: #999; } .breadcrumb a { color: var(--primary-color); text-decoration: none; } .breadcrumb a:hover { text-decoration: underline; } /* Buttons */ .btn { display: inline-block; padding: 8px 15px; background: var(--light-color); color: var(--text-color); text-decoration: none; border: none; border-radius: 4px; cursor: pointer; transition: all 0.3s; font-size: 0.9rem; } .btn:hover { background: #d5dbdb; } .btn-primary { background: var(--primary-color); color: #fff; } .btn-primary:hover { background: #2980b9; } .btn-success { background: var(--success-color); color: #fff; } .btn-success:hover { background: #219a52; } .btn-danger { background: var(--danger-color); color: #fff; } .btn-danger:hover { background: #c0392b; } .btn-small { padding: 5px 10px; font-size: 0.8rem; } .btn-large { padding: 12px 25px; font-size: 1rem; } /* Alerts */ .alert { padding: 15px; border-radius: 4px; margin-bottom: 20px; } .alert-success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; } .alert-error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } /* Footer */ .forum-footer { background: var(--dark-color); color: #fff; text-align: center; padding: 20px 0; margin-top: 40px; } /* Responsive */ @media (max-width: 768px) { .forum-layout { grid-template-columns: 1fr; } .header-top { flex-direction: column; gap: 15px; } .search-bar { width: 100%; } .post { flex-direction: column; } .post-sidebar { width: 100%; border-right: none; border-bottom: 1px solid var(--border-color); } .thread-meta { flex-wrap: wrap; } .thread-stats { min-width: auto; } } 📝 How to Use This Project (Step-by-Step Guide)
Step 1: Install Local Server
- Download and install XAMPP from apachefriends.org
- Launch XAMPP Control Panel
- Start Apache and MySQL services
Step 2: Create Project Folder
- Navigate to
C:\xampp\htdocs\(Windows) or/Applications/XAMPP/htdocs/(Mac) - Create a new folder named
discussion-forum
Step 3: Set Up Database
- Open browser and go to
http://localhost/phpmyadmin - Click on "SQL" tab
- Copy the entire SQL from
database/forum.sql - Paste and click "Go" to create database and tables
Step 4: Create Password Hash
Create a file named hash.php in project root:
<?php echo "admin123 hash: " . password_hash('admin123', PASSWORD_DEFAULT) . "\n"; echo "mod123 hash: " . password_hash('mod123', PASSWORD_DEFAULT) . "\n"; echo "member123 hash: " . password_hash('member123', PASSWORD_DEFAULT); ?> Run it: http://localhost/discussion-forum/hash.php
Copy the hashes and update them in the SQL insert statements for the users.
Step 5: Configure Database Connection
Open includes/config.php and verify:
define('DB_HOST', 'localhost'); define('DB_NAME', 'discussion_forum'); define('DB_USER', 'root'); define('DB_PASS', ''); Step 6: Create Required Directories
Create these folders and set proper permissions:
assets/images/avatars/- for user avatarsassets/images/uploads/- for file uploads (if implementing)
Step 7: Download Third-Party Libraries
- Download Parsedown from github.com/erusev/parsedown and place in
vendor/parsedown/
Step 8: Test the Application
Public Side:
- Open:
http://localhost/discussion-forum/ - Browse categories and threads
- Register a new account
- Create a new discussion
- Post replies
- Vote on posts
Admin Side:
- Open:
http://localhost/discussion-forum/admin/login.php - Login with: Username:
admin, Password:admin123 - Or login as moderator: Username:
moderator, Password:mod123
Step 9: Add Sample Content
- Login to admin panel
- Go to Categories → Add categories if needed
- Go to the main site and create some test discussions
- Try posting replies and voting
- Test the reporting system
Step 10: Customize Forum Settings
- Go to Settings in admin panel
- Update forum title, description
- Configure posting rules and karma settings
- Set email notification preferences
🎯 Features Summary
User Features:
- ✅ User registration and login
- ✅ Create new discussion threads
- ✅ Post replies with Markdown support
- ✅ Vote on posts (upvote/downvote)
- ✅ Karma system for user reputation
- ✅ Subscribe to threads for notifications
- ✅ Bookmark favorite threads
- ✅ User profiles with post history
- ✅ Search functionality
- ✅ Report inappropriate content
Moderator Features:
- ✅ Dashboard with statistics
- ✅ Category management
- ✅ User management (ban, promote)
- ✅ Post moderation (edit, delete, feature)
- ✅ Report review system
- ✅ Thread locking/pinning
- ✅ Mark best answers
Technical Features:
- ✅ Secure password hashing
- ✅ Session management
- ✅ SQL injection prevention
- ✅ XSS protection
- ✅ SEO-friendly URLs (slugs)
- ✅ Pagination
- ✅ Markdown support
- ✅ Voting system with AJAX
- ✅ Notification system
- ✅ Responsive design
🚀 Future Enhancements
- Private Messages: Direct messaging between users
- User Groups: Create private forums for groups
- File Attachments: Upload images and files
- Email Notifications: Real-time email alerts
- Social Login: Login with Google/Facebook
- API: REST API for mobile apps
- Rich Text Editor: WYSIWYG editor option
- User Badges: Achievement system
- Poll Creation: Add polls to threads
- Mentions: @username mentions with notifications
- Thread Tags: Tag system for better organization
- Search Filters: Advanced search options
- Dark Mode: Theme switcher
- Spam Protection: CAPTCHA integration
- Analytics: Detailed usage statistics
- Export Data: Export threads/posts to PDF
- Backup System: Automated database backups
- Multi-language: Internationalization support
- SEO Tools: Meta tags, sitemap generator
- Mobile App: Native Android/iOS apps
This comprehensive Discussion Forum Website provides a complete platform for online communities with powerful features for both users and moderators!