π° Project Introduction: News Portal Website
This project is a fully functional News Portal Website designed for online news publishing. It allows readers to browse news by categories, read articles, leave comments, and search for content. Administrators can manage everything from articles and categories to users and comments through a powerful admin panel.
The Problem it Solves:
Traditional news websites require constant manual updates and complex content management. This system provides an easy-to-use platform for publishers to upload news articles, manage categories, moderate comments, and maintain a professional news website without technical expertise.
Key Features:
- Public User Features:
- Browse news by categories (Politics, Technology, Sports, Entertainment, etc.)
- View featured and latest news on homepage
- Search articles by title, content, or category
- Read full articles with author information and publish date
- Leave comments on articles (with moderation)
- Share articles on social media
- Subscribe to newsletter
- Responsive design for all devices
- Admin Features:
- Dashboard with statistics (total articles, views, comments)
- Article management (create, edit, delete, publish/unpublish)
- Category management (create, edit, delete)
- Comment moderation (approve/reject comments)
- User management (view, edit, delete users)
- Media library for image uploads
- SEO settings (meta titles, descriptions)
- Newsletter management
- Site settings customization
- Backup and restore functionality
Technology Stack:
- Frontend: HTML5, CSS3, JavaScript (Fetch API, Masonry layout)
- Backend: PHP (Object-Oriented PHP with PDO)
- Database: MySQL
- Additional Libraries:
- TinyMCE (Rich text editor)
- Chart.js (Analytics charts)
- PHPMailer (Email notifications)
π Project File Structure
news-portal/ β βββ index.php # Homepage with featured and latest news βββ category.php # Display news by category βββ article.php # Single article page βββ search.php # Search results page βββ about.php # About us page βββ contact.php # Contact page βββ subscribe.php # Newsletter subscription βββ login.php # User login βββ register.php # User registration βββ logout.php # Logout script β βββ admin/ # Admin Panel β βββ index.php # Admin login β βββ dashboard.php # Admin dashboard β βββ articles.php # Manage articles β βββ add-article.php # Add new article β βββ edit-article.php # Edit article β βββ delete-article.php # Delete article β βββ categories.php # Manage categories β βββ add-category.php # Add category β βββ edit-category.php # Edit category β βββ comments.php # Moderate comments β βββ users.php # Manage users β βββ media.php # Media library β βββ newsletter.php # Newsletter management β βββ settings.php # Site settings β βββ backup.php # Backup database β βββ 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 β βββ get-articles.php # Get articles for infinite scroll β βββ search-suggestions.php # Live search suggestions β βββ submit-comment.php # Submit comment via AJAX β βββ subscribe.php # Newsletter subscription β βββ assets/ # Static assets β βββ css/ β β βββ style.css # Main styles β β βββ admin.css # Admin styles β βββ js/ β β βββ main.js # Main JavaScript β β βββ masonry.js # Masonry layout β β βββ admin.js # Admin scripts β βββ images/ β β βββ uploads/ # Article images β β βββ logo.png # Site logo β βββ vendor/ # Third-party libraries β βββ tinymce/ # Rich text editor β βββ chart.js/ # Charts library β βββ database/ βββ news_portal.sql # Database dump
ποΈ Database Setup (database/news_portal.sql)
Create a database named news_portal and run this SQL.
-- phpMyAdmin SQL Dump
-- Database: `news_portal`
CREATE DATABASE IF NOT EXISTS `news_portal`;
USE `news_portal`;
-- --------------------------------------------------------
-- 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) NOT NULL,
`bio` text DEFAULT NULL,
`avatar` varchar(255) DEFAULT 'default-avatar.png',
`role` enum('admin','editor','subscriber') DEFAULT 'subscriber',
`status` enum('active','inactive','banned') DEFAULT 'active',
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
`last_login` timestamp NULL DEFAULT NULL,
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 editor (password: editor123)
INSERT INTO `users` (`username`, `email`, `password`, `full_name`, `role`) VALUES
('editor', '[email protected]', '$2y$10$YourHashedPasswordHere', 'John Editor', 'editor');
-- --------------------------------------------------------
-- 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,
`parent_id` int(11) DEFAULT NULL,
`icon` varchar(50) DEFAULT NULL,
`color` varchar(20) DEFAULT '#3498db',
`menu_order` int(11) DEFAULT 0,
`status` enum('active','inactive') DEFAULT 'active',
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `slug` (`slug`),
KEY `parent_id` (`parent_id`),
CONSTRAINT `categories_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `categories` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Sample categories
INSERT INTO `categories` (`name`, `slug`, `description`, `color`, `menu_order`) VALUES
('Politics', 'politics', 'Latest political news and analysis', '#e74c3c', 1),
('Technology', 'technology', 'Tech news, gadgets, and innovations', '#3498db', 2),
('Sports', 'sports', 'Sports news, scores, and updates', '#27ae60', 3),
('Entertainment', 'entertainment', 'Movies, music, and celebrity news', '#f1c40f', 4),
('Business', 'business', 'Business, finance, and economy', '#9b59b6', 5),
('Health', 'health', 'Health tips, medical news, and wellness', '#e67e22', 6);
-- --------------------------------------------------------
-- Table structure for table `articles`
-- --------------------------------------------------------
CREATE TABLE `articles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL,
`slug` varchar(255) NOT NULL,
`content` longtext NOT NULL,
`excerpt` varchar(500) DEFAULT NULL,
`featured_image` varchar(255) DEFAULT NULL,
`category_id` int(11) NOT NULL,
`author_id` int(11) NOT NULL,
`views` int(11) DEFAULT 0,
`status` enum('draft','published','archived') DEFAULT 'draft',
`is_featured` tinyint(1) DEFAULT 0,
`is_breaking` tinyint(1) DEFAULT 0,
`published_at` datetime DEFAULT NULL,
`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 `author_id` (`author_id`),
KEY `published_at` (`published_at`),
FULLTEXT KEY `search` (`title`,`content`,`excerpt`),
CONSTRAINT `articles_ibfk_1` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE CASCADE,
CONSTRAINT `articles_ibfk_2` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Sample articles
INSERT INTO `articles` (`title`, `slug`, `content`, `excerpt`, `category_id`, `author_id`, `status`, `is_featured`, `published_at`) VALUES
('Breaking: Major Tech Announcement', 'breaking-major-tech-announcement', '<p>A major technology company has announced a groundbreaking new product that promises to revolutionize the industry. The announcement came during their annual developer conference, where CEO John Smith unveiled the new device to an enthusiastic audience.</p><p>The device features cutting-edge technology including AI integration, enhanced security features, and unprecedented processing power. Industry experts predict this will set new standards for the entire tech industry.</p>', 'A major tech company announces revolutionary new product at annual conference', 2, 1, 'published', 1, NOW()),
('Election Results: What It Means', 'election-results-analysis', '<p>The recent election results have significant implications for the country''s future. Political analysts weigh in on what this means for various sectors including economy, healthcare, and foreign policy.</p><p>The winning party has promised sweeping reforms and immediate action on key issues facing the nation. Opposition parties have congratulated the winners while pledging to hold them accountable.</p>', 'Analysis of recent election results and their impact on the nation', 1, 1, 'published', 1, DATE_SUB(NOW(), INTERVAL 1 DAY)),
('Championship Final: Thrilling Victory', 'championship-final-thrilling-victory', '<p>In a nail-biting finish, the underdog team clinched the championship title with a last-minute goal that sent fans into a frenzy. The final score of 3-2 marks one of the most exciting finals in recent memory.</p><p>The winning captain dedicated the victory to their loyal fans, saying "This is for every single person who believed in us." The celebrations continued well into the night across the city.</p>', 'Underdog team wins championship in dramatic last-minute victory', 3, 1, 'published', 1, DATE_SUB(NOW(), INTERVAL 2 DAY));
-- --------------------------------------------------------
-- Table structure for table `comments`
-- --------------------------------------------------------
CREATE TABLE `comments` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`article_id` int(11) NOT NULL,
`user_id` int(11) DEFAULT NULL,
`author_name` varchar(100) DEFAULT NULL,
`author_email` varchar(100) DEFAULT NULL,
`content` text NOT NULL,
`status` enum('pending','approved','spam','trash') DEFAULT 'pending',
`ip_address` varchar(45) DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `article_id` (`article_id`),
KEY `user_id` (`user_id`),
CONSTRAINT `comments_ibfk_1` FOREIGN KEY (`article_id`) REFERENCES `articles` (`id`) ON DELETE CASCADE,
CONSTRAINT `comments_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- --------------------------------------------------------
-- Table structure for table `tags`
-- --------------------------------------------------------
CREATE TABLE `tags` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`slug` varchar(50) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `slug` (`slug`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- --------------------------------------------------------
-- Table structure for table `article_tags`
-- --------------------------------------------------------
CREATE TABLE `article_tags` (
`article_id` int(11) NOT NULL,
`tag_id` int(11) NOT NULL,
PRIMARY KEY (`article_id`,`tag_id`),
KEY `tag_id` (`tag_id`),
CONSTRAINT `article_tags_ibfk_1` FOREIGN KEY (`article_id`) REFERENCES `articles` (`id`) ON DELETE CASCADE,
CONSTRAINT `article_tags_ibfk_2` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- --------------------------------------------------------
-- Table structure for table `subscribers`
-- --------------------------------------------------------
CREATE TABLE `subscribers` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`email` varchar(100) NOT NULL,
`name` varchar(100) DEFAULT NULL,
`status` enum('active','inactive','unsubscribed') DEFAULT 'active',
`subscribed_at` timestamp NOT NULL DEFAULT current_timestamp(),
`unsubscribed_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`)
) 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
('site_title', 'News Portal'),
('site_description', 'Your Trusted Source for Latest News'),
('site_keywords', 'news, breaking news, latest news'),
('site_logo', 'logo.png'),
('site_favicon', 'favicon.ico'),
('contact_email', '[email protected]'),
('contact_phone', '+1 234 567 890'),
('facebook_url', 'https://facebook.com/newsportal'),
('twitter_url', 'https://twitter.com/newsportal'),
('instagram_url', 'https://instagram.com/newsportal'),
('posts_per_page', '10'),
('comment_moderation', '1'),
('allow_registration', '1');
-- --------------------------------------------------------
-- Table structure for table `media`
-- --------------------------------------------------------
CREATE TABLE `media` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`filename` varchar(255) NOT NULL,
`original_name` varchar(255) NOT NULL,
`file_path` varchar(255) NOT NULL,
`file_type` varchar(50) NOT NULL,
`file_size` int(11) NOT NULL,
`uploaded_by` int(11) DEFAULT NULL,
`uploaded_at` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `uploaded_by` (`uploaded_by`),
CONSTRAINT `media_ibfk_1` FOREIGN KEY (`uploaded_by`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
COMMIT;
π» Core PHP Files
1. Database Configuration (includes/config.php)
<?php
// Database configuration
define('DB_HOST', 'localhost');
define('DB_NAME', 'news_portal');
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'];
}
// Site configuration
define('SITE_NAME', $settings['site_title'] ?? 'News Portal');
define('SITE_URL', 'http://localhost/news-portal/');
define('SITE_DESCRIPTION', $settings['site_description'] ?? 'Your Trusted Source for Latest News');
define('CONTACT_EMAIL', $settings['contact_email'] ?? '[email protected]');
define('POSTS_PER_PAGE', $settings['posts_per_page'] ?? 10);
define('COMMENT_MODERATION', $settings['comment_moderation'] ?? 1);
define('UPLOAD_PATH', __DIR__ . '/../assets/images/uploads/');
// 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']) && in_array($_SESSION['user_role'], ['admin', 'editor']);
}
function isSuperAdmin() {
return isset($_SESSION['user_role']) && $_SESSION['user_role'] === 'admin';
}
function formatDate($date, $format = 'F j, Y') {
return date($format, strtotime($date));
}
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 getExcerpt($content, $length = 150) {
$content = strip_tags($content);
if (strlen($content) <= $length) {
return $content;
}
return substr($content, 0, $length) . '...';
}
function incrementViews($article_id) {
global $pdo;
$pdo->prepare("UPDATE articles SET views = views + 1 WHERE id = ?")->execute([$article_id]);
}
?>
2. Homepage (index.php)
<?php
require_once 'includes/config.php';
// Get featured articles
$featured = $pdo->query("
SELECT a.*, u.full_name as author_name, c.name as category_name, c.slug as category_slug
FROM articles a
JOIN users u ON a.author_id = u.id
JOIN categories c ON a.category_id = c.id
WHERE a.status = 'published' AND a.is_featured = 1
ORDER BY a.published_at DESC
LIMIT 3
")->fetchAll();
// Get breaking news
$breaking = $pdo->query("
SELECT a.*, u.full_name as author_name, c.name as category_name
FROM articles a
JOIN users u ON a.author_id = u.id
JOIN categories c ON a.category_id = c.id
WHERE a.status = 'published' AND a.is_breaking = 1
ORDER BY a.published_at DESC
LIMIT 5
")->fetchAll();
// Get latest articles
$latest = $pdo->prepare("
SELECT a.*, u.full_name as author_name, c.name as category_name, c.slug as category_slug
FROM articles a
JOIN users u ON a.author_id = u.id
JOIN categories c ON a.category_id = c.id
WHERE a.status = 'published'
ORDER BY a.published_at DESC
LIMIT ?
");
$latest->execute([POSTS_PER_PAGE]);
$latest_articles = $latest->fetchAll();
// Get popular articles (by views)
$popular = $pdo->query("
SELECT a.*, u.full_name as author_name, c.name as category_name
FROM articles a
JOIN users u ON a.author_id = u.id
JOIN categories c ON a.category_id = c.id
WHERE a.status = 'published'
ORDER BY a.views DESC
LIMIT 5
")->fetchAll();
// Get categories for navigation
$categories = $pdo->query("
SELECT * FROM categories
WHERE status = 'active'
ORDER BY menu_order
")->fetchAll();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo SITE_NAME; ?> - <?php echo SITE_DESCRIPTION; ?></title>
<meta name="description" content="<?php echo SITE_DESCRIPTION; ?>">
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<!-- Header -->
<header class="site-header">
<div class="top-header">
<div class="container">
<div class="header-left">
<span class="current-date"><?php echo date('l, F j, Y'); ?></span>
</div>
<div class="header-right">
<?php if (isLoggedIn()): ?>
<span>Welcome, <?php echo $_SESSION['user_name']; ?></span>
<a href="profile.php">Profile</a>
<?php if (isAdmin()): ?>
<a href="admin/dashboard.php">Admin</a>
<?php endif; ?>
<a href="logout.php">Logout</a>
<?php else: ?>
<a href="login.php">Login</a>
<a href="register.php">Register</a>
<?php endif; ?>
</div>
</div>
</div>
<div class="main-header">
<div class="container">
<div class="logo">
<a href="index.php">
<h1><?php echo SITE_NAME; ?></h1>
</a>
</div>
<div class="search-bar">
<form action="search.php" method="GET">
<input type="text" name="q" placeholder="Search news..." id="search-input" autocomplete="off">
<button type="submit">π</button>
</form>
<div id="search-suggestions" class="search-suggestions"></div>
</div>
</div>
</div>
<nav class="main-nav">
<div class="container">
<ul class="nav-menu">
<li><a href="index.php">Home</a></li>
<?php foreach ($categories as $category): ?>
<li>
<a href="category.php?slug=<?php echo $category['slug']; ?>">
<?php echo $category['name']; ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>
</nav>
<?php if (!empty($breaking)): ?>
<div class="breaking-news">
<div class="container">
<span class="breaking-label">BREAKING</span>
<div class="breaking-ticker">
<?php foreach ($breaking as $news): ?>
<a href="article.php?slug=<?php echo $news['slug']; ?>">
<?php echo $news['title']; ?>
</a>
<?php endforeach; ?>
</div>
</div>
</div>
<?php endif; ?>
</header>
<!-- Main Content -->
<main class="container">
<!-- Featured Articles -->
<?php if (!empty($featured)): ?>
<section class="featured-section">
<h2 class="section-title">Featured Stories</h2>
<div class="featured-grid">
<?php foreach ($featured as $index => $article): ?>
<div class="featured-item featured-<?php echo $index + 1; ?>">
<a href="article.php?slug=<?php echo $article['slug']; ?>">
<div class="featured-image">
<img src="assets/images/uploads/<?php echo $article['featured_image'] ?: 'default.jpg'; ?>"
alt="<?php echo $article['title']; ?>">
<span class="category-badge"><?php echo $article['category_name']; ?></span>
</div>
<div class="featured-content">
<h3><?php echo $article['title']; ?></h3>
<p><?php echo getExcerpt($article['excerpt'] ?: $article['content'], 100); ?></p>
<div class="meta">
<span>By <?php echo $article['author_name']; ?></span>
<span><?php echo timeAgo($article['published_at']); ?></span>
</div>
</div>
</a>
</div>
<?php endforeach; ?>
</div>
</section>
<?php endif; ?>
<div class="content-wrapper">
<!-- Main Content Area -->
<div class="main-content">
<section class="latest-news">
<h2 class="section-title">Latest News</h2>
<div class="news-grid" id="news-grid">
<?php foreach ($latest_articles as $article): ?>
<article class="news-card">
<a href="article.php?slug=<?php echo $article['slug']; ?>">
<div class="card-image">
<img src="assets/images/uploads/<?php echo $article['featured_image'] ?: 'default.jpg'; ?>"
alt="<?php echo $article['title']; ?>">
<span class="category-tag"><?php echo $article['category_name']; ?></span>
</div>
<div class="card-content">
<h3><?php echo $article['title']; ?></h3>
<p><?php echo getExcerpt($article['excerpt'] ?: $article['content'], 120); ?></p>
<div class="card-meta">
<span class="author"><?php echo $article['author_name']; ?></span>
<span class="date"><?php echo timeAgo($article['published_at']); ?></span>
</div>
</div>
</a>
</article>
<?php endforeach; ?>
</div>
<!-- Load More Button -->
<div class="load-more">
<button id="load-more" class="btn" data-page="2">Load More</button>
</div>
</section>
</div>
<!-- Sidebar -->
<aside class="sidebar">
<!-- Popular News -->
<div class="widget popular-widget">
<h3 class="widget-title">Popular News</h3>
<ul class="popular-list">
<?php foreach ($popular as $article): ?>
<li>
<a href="article.php?slug=<?php echo $article['slug']; ?>">
<span class="popular-title"><?php echo $article['title']; ?></span>
<span class="popular-views">ποΈ <?php echo number_format($article['views']); ?> views</span>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>
<!-- Categories -->
<div class="widget categories-widget">
<h3 class="widget-title">Categories</h3>
<ul class="categories-list">
<?php foreach ($categories as $category): ?>
<li>
<a href="category.php?slug=<?php echo $category['slug']; ?>">
<?php echo $category['name']; ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>
<!-- Newsletter -->
<div class="widget newsletter-widget">
<h3 class="widget-title">Newsletter</h3>
<p>Subscribe to get the latest news delivered to your inbox.</p>
<form id="newsletter-form" class="newsletter-form">
<input type="email" name="email" placeholder="Your email address" required>
<button type="submit" class="btn btn-block">Subscribe</button>
</form>
<div id="newsletter-message"></div>
</div>
</aside>
</div>
</main>
<!-- Footer -->
<footer class="site-footer">
<div class="container">
<div class="footer-widgets">
<div class="footer-widget">
<h4>About Us</h4>
<p><?php echo SITE_NAME; ?> is your trusted source for the latest news, analysis, and updates from around the world.</p>
</div>
<div class="footer-widget">
<h4>Quick Links</h4>
<ul>
<li><a href="about.php">About Us</a></li>
<li><a href="contact.php">Contact</a></li>
<li><a href="privacy.php">Privacy Policy</a></li>
<li><a href="terms.php">Terms of Use</a></li>
</ul>
</div>
<div class="footer-widget">
<h4>Follow Us</h4>
<div class="social-links">
<a href="<?php echo $settings['facebook_url'] ?? '#'; ?>" target="_blank">Facebook</a>
<a href="<?php echo $settings['twitter_url'] ?? '#'; ?>" target="_blank">Twitter</a>
<a href="<?php echo $settings['instagram_url'] ?? '#'; ?>" target="_blank">Instagram</a>
</div>
</div>
</div>
<div class="footer-bottom">
<p>© <?php echo date('Y'); ?> <?php echo SITE_NAME; ?>. All rights reserved.</p>
</div>
</div>
</footer>
<script src="assets/js/main.js"></script>
<script>
// Live search suggestions
document.getElementById('search-input').addEventListener('input', function(e) {
const query = e.target.value;
if (query.length < 2) {
document.getElementById('search-suggestions').style.display = 'none';
return;
}
fetch(`api/search-suggestions.php?q=${encodeURIComponent(query)}`)
.then(response => response.json())
.then(data => {
const suggestions = document.getElementById('search-suggestions');
if (data.length > 0) {
suggestions.innerHTML = data.map(item =>
`<a href="article.php?slug=${item.slug}">${item.title}</a>`
).join('');
suggestions.style.display = 'block';
} else {
suggestions.style.display = 'none';
}
});
});
// Newsletter subscription
document.getElementById('newsletter-form').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch('api/subscribe.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
const message = document.getElementById('newsletter-message');
message.className = data.success ? 'success' : 'error';
message.textContent = data.message;
if (data.success) {
this.reset();
}
});
});
</script>
</body>
</html>
3. Article Page (article.php)
<?php
require_once 'includes/config.php';
$slug = $_GET['slug'] ?? '';
if (empty($slug)) {
redirect('index.php');
}
// Get article details
$stmt = $pdo->prepare("
SELECT a.*, u.full_name as author_name, u.bio as author_bio, u.avatar as author_avatar,
c.name as category_name, c.slug as category_slug
FROM articles a
JOIN users u ON a.author_id = u.id
JOIN categories c ON a.category_id = c.id
WHERE a.slug = ? AND a.status = 'published'
");
$stmt->execute([$slug]);
$article = $stmt->fetch();
if (!$article) {
redirect('index.php');
}
// Increment view count
incrementViews($article['id']);
// Get related articles (same category)
$related = $pdo->prepare("
SELECT a.*, u.full_name as author_name
FROM articles a
JOIN users u ON a.author_id = u.id
WHERE a.category_id = ? AND a.id != ? AND a.status = 'published'
ORDER BY a.published_at DESC
LIMIT 3
");
$related->execute([$article['category_id'], $article['id']]);
$related_articles = $related->fetchAll();
// Get comments
$comments = $pdo->prepare("
SELECT c.*, u.full_name as user_name, u.avatar
FROM comments c
LEFT JOIN users u ON c.user_id = u.id
WHERE c.article_id = ? AND c.status = 'approved'
ORDER BY c.created_at DESC
");
$comments->execute([$article['id']]);
$article_comments = $comments->fetchAll();
// Handle comment submission
$comment_error = '';
$comment_success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['submit_comment'])) {
$content = sanitize($_POST['content']);
if (empty($content)) {
$comment_error = 'Please enter a comment';
} else {
if (isLoggedIn()) {
// Logged in user
$stmt = $pdo->prepare("
INSERT INTO comments (article_id, user_id, content, status, ip_address)
VALUES (?, ?, ?, ?, ?)
");
$status = COMMENT_MODERATION ? 'pending' : 'approved';
$stmt->execute([
$article['id'],
$_SESSION['user_id'],
$content,
$status,
$_SERVER['REMOTE_ADDR']
]);
} else {
// Guest comment
$name = sanitize($_POST['name']);
$email = sanitize($_POST['email']);
if (empty($name) || empty($email)) {
$comment_error = 'Name and email are required for guest comments';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$comment_error = 'Invalid email format';
} else {
$stmt = $pdo->prepare("
INSERT INTO comments (article_id, author_name, author_email, content, status, ip_address)
VALUES (?, ?, ?, ?, ?, ?)
");
$status = COMMENT_MODERATION ? 'pending' : 'approved';
$stmt->execute([
$article['id'],
$name,
$email,
$content,
$status,
$_SERVER['REMOTE_ADDR']
]);
}
}
if (!$comment_error) {
$comment_success = COMMENT_MODERATION ?
'Your comment has been submitted and is waiting for approval.' :
'Your comment has been posted.';
}
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo $article['title']; ?> - <?php echo SITE_NAME; ?></title>
<meta name="description" content="<?php echo getExcerpt($article['excerpt'] ?: $article['content'], 160); ?>">
<meta property="og:title" content="<?php echo $article['title']; ?>">
<meta property="og:description" content="<?php echo getExcerpt($article['excerpt'] ?: $article['content'], 160); ?>">
<meta property="og:image" content="<?php echo SITE_URL; ?>assets/images/uploads/<?php echo $article['featured_image'] ?: 'default.jpg'; ?>">
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<!-- Header (same as index.php) -->
<?php include 'includes/header.php'; ?>
<main class="container">
<article class="single-article">
<header class="article-header">
<div class="article-meta-top">
<span class="article-category"><?php echo $article['category_name']; ?></span>
<span class="article-date"><?php echo formatDate($article['published_at']); ?></span>
</div>
<h1 class="article-title"><?php echo $article['title']; ?></h1>
<div class="article-author">
<img src="assets/images/<?php echo $article['author_avatar'] ?: 'default-avatar.png'; ?>"
alt="<?php echo $article['author_name']; ?>" class="author-avatar">
<div class="author-info">
<span class="author-name">By <?php echo $article['author_name']; ?></span>
<?php if ($article['author_bio']): ?>
<p class="author-bio"><?php echo $article['author_bio']; ?></p>
<?php endif; ?>
</div>
</div>
</header>
<?php if ($article['featured_image']): ?>
<div class="article-featured-image">
<img src="assets/images/uploads/<?php echo $article['featured_image']; ?>"
alt="<?php echo $article['title']; ?>">
</div>
<?php endif; ?>
<div class="article-content">
<?php echo $article['content']; ?>
</div>
<footer class="article-footer">
<div class="article-share">
<h4>Share this article:</h4>
<div class="share-buttons">
<a href="https://www.facebook.com/sharer/sharer.php?u=<?php echo urlencode(SITE_URL . 'article.php?slug=' . $article['slug']); ?>"
target="_blank" class="share-facebook">Facebook</a>
<a href="https://twitter.com/intent/tweet?url=<?php echo urlencode(SITE_URL . 'article.php?slug=' . $article['slug']); ?>&text=<?php echo urlencode($article['title']); ?>"
target="_blank" class="share-twitter">Twitter</a>
<a href="https://www.linkedin.com/shareArticle?mini=true&url=<?php echo urlencode(SITE_URL . 'article.php?slug=' . $article['slug']); ?>"
target="_blank" class="share-linkedin">LinkedIn</a>
</div>
</div>
<div class="article-stats">
<span>ποΈ <?php echo number_format($article['views']); ?> views</span>
<span>π¬ <?php echo count($article_comments); ?> comments</span>
</div>
</footer>
</article>
<!-- Comments Section -->
<section class="comments-section">
<h2 class="section-title">Comments (<?php echo count($article_comments); ?>)</h2>
<?php if ($comment_success): ?>
<div class="alert alert-success"><?php echo $comment_success; ?></div>
<?php endif; ?>
<?php if ($comment_error): ?>
<div class="alert alert-error"><?php echo $comment_error; ?></div>
<?php endif; ?>
<!-- Comment Form -->
<div class="comment-form-wrapper">
<h3>Leave a Comment</h3>
<form method="POST" action="" class="comment-form">
<div class="form-group">
<label for="content">Your Comment *</label>
<textarea id="content" name="content" rows="4" required></textarea>
</div>
<?php if (!isLoggedIn()): ?>
<div class="form-row">
<div class="form-group col-md-6">
<label for="name">Name *</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group col-md-6">
<label for="email">Email *</label>
<input type="email" id="email" name="email" required>
</div>
</div>
<?php endif; ?>
<button type="submit" name="submit_comment" class="btn btn-primary">Post Comment</button>
</form>
</div>
<!-- Comments List -->
<?php if (!empty($article_comments)): ?>
<div class="comments-list">
<?php foreach ($article_comments as $comment): ?>
<div class="comment">
<div class="comment-avatar">
<img src="assets/images/<?php echo $comment['avatar'] ?: 'default-avatar.png'; ?>"
alt="<?php echo $comment['user_name'] ?: $comment['author_name']; ?>">
</div>
<div class="comment-content">
<div class="comment-meta">
<span class="comment-author">
<?php echo $comment['user_name'] ?: $comment['author_name']; ?>
</span>
<span class="comment-date">
<?php echo timeAgo($comment['created_at']); ?>
</span>
</div>
<div class="comment-text">
<?php echo nl2br($comment['content']); ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
<!-- Related Articles -->
<?php if (!empty($related_articles)): ?>
<section class="related-articles">
<h2 class="section-title">Related Articles</h2>
<div class="related-grid">
<?php foreach ($related_articles as $related): ?>
<article class="related-card">
<a href="article.php?slug=<?php echo $related['slug']; ?>">
<div class="related-image">
<img src="assets/images/uploads/<?php echo $related['featured_image'] ?: 'default.jpg'; ?>"
alt="<?php echo $related['title']; ?>">
</div>
<div class="related-content">
<h3><?php echo $related['title']; ?></h3>
<div class="related-meta">
<span><?php echo timeAgo($related['published_at']); ?></span>
</div>
</div>
</a>
</article>
<?php endforeach; ?>
</div>
</section>
<?php endif; ?>
</main>
<!-- Footer -->
<?php include 'includes/footer.php'; ?>
<script src="assets/js/main.js"></script>
</body>
</html>
4. Category Page (category.php)
<?php
require_once 'includes/config.php';
$slug = $_GET['slug'] ?? '';
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$offset = ($page - 1) * POSTS_PER_PAGE;
// Get category details
$stmt = $pdo->prepare("SELECT * FROM categories WHERE slug = ? AND status = 'active'");
$stmt->execute([$slug]);
$category = $stmt->fetch();
if (!$category) {
redirect('index.php');
}
// Get total articles count
$count_stmt = $pdo->prepare("
SELECT COUNT(*) FROM articles
WHERE category_id = ? AND status = 'published'
");
$count_stmt->execute([$category['id']]);
$total_articles = $count_stmt->fetchColumn();
$total_pages = ceil($total_articles / POSTS_PER_PAGE);
// Get articles for this category
$articles = $pdo->prepare("
SELECT a.*, u.full_name as author_name
FROM articles a
JOIN users u ON a.author_id = u.id
WHERE a.category_id = ? AND a.status = 'published'
ORDER BY a.published_at DESC
LIMIT ? OFFSET ?
");
$articles->execute([$category['id'], POSTS_PER_PAGE, $offset]);
$category_articles = $articles->fetchAll();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo $category['name']; ?> - <?php echo SITE_NAME; ?></title>
<meta name="description" content="<?php echo $category['description']; ?>">
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<!-- Header -->
<?php include 'includes/header.php'; ?>
<main class="container">
<!-- Category Header -->
<div class="category-header" style="border-bottom-color: <?php echo $category['color']; ?>">
<h1><?php echo $category['name']; ?></h1>
<?php if ($category['description']): ?>
<p><?php echo $category['description']; ?></p>
<?php endif; ?>
</div>
<!-- Articles Grid -->
<div class="category-articles">
<?php if (empty($category_articles)): ?>
<p class="no-articles">No articles found in this category.</p>
<?php else: ?>
<div class="articles-grid">
<?php foreach ($category_articles as $article): ?>
<article class="article-card">
<a href="article.php?slug=<?php echo $article['slug']; ?>">
<div class="card-image">
<img src="assets/images/uploads/<?php echo $article['featured_image'] ?: 'default.jpg'; ?>"
alt="<?php echo $article['title']; ?>">
</div>
<div class="card-content">
<h3><?php echo $article['title']; ?></h3>
<p><?php echo getExcerpt($article['excerpt'] ?: $article['content'], 120); ?></p>
<div class="card-meta">
<span>By <?php echo $article['author_name']; ?></span>
<span><?php echo timeAgo($article['published_at']); ?></span>
</div>
</div>
</a>
</article>
<?php endforeach; ?>
</div>
<!-- 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; ?>
<?php endif; ?>
</div>
</main>
<!-- Footer -->
<?php include 'includes/footer.php'; ?>
</body>
</html>
5. Admin Dashboard (admin/dashboard.php)
<?php
require_once '../includes/config.php';
require_once '../includes/auth.php';
Auth::checkLogin();
if (!isAdmin()) {
redirect('../index.php');
}
// Get statistics
$totalArticles = $pdo->query("SELECT COUNT(*) FROM articles")->fetchColumn();
$publishedArticles = $pdo->query("SELECT COUNT(*) FROM articles WHERE status = 'published'")->fetchColumn();
$totalViews = $pdo->query("SELECT SUM(views) FROM articles")->fetchColumn();
$totalComments = $pdo->query("SELECT COUNT(*) FROM comments")->fetchColumn();
$pendingComments = $pdo->query("SELECT COUNT(*) FROM comments WHERE status = 'pending'")->fetchColumn();
$totalUsers = $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
$totalSubscribers = $pdo->query("SELECT COUNT(*) FROM subscribers WHERE status = 'active'")->fetchColumn();
// Get recent articles
$recentArticles = $pdo->query("
SELECT a.*, u.full_name as author_name, c.name as category_name
FROM articles a
JOIN users u ON a.author_id = u.id
JOIN categories c ON a.category_id = c.id
ORDER BY a.created_at DESC
LIMIT 5
")->fetchAll();
// Get popular articles
$popularArticles = $pdo->query("
SELECT a.*, u.full_name as author_name
FROM articles a
JOIN users u ON a.author_id = u.id
ORDER BY a.views DESC
LIMIT 5
")->fetchAll();
// Get recent comments
$recentComments = $pdo->query("
SELECT c.*, a.title as article_title, a.slug as article_slug
FROM comments c
JOIN articles a ON c.article_id = a.id
ORDER BY c.created_at 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 SITE_NAME; ?></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>News 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="articles.php">Articles</a></li>
<li><a href="categories.php">Categories</a></li>
<li><a href="comments.php">Comments</a></li>
<li><a href="users.php">Users</a></li>
<li><a href="media.php">Media Library</a></li>
<li><a href="newsletter.php">Newsletter</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 $totalArticles; ?></h3>
<p>Total Articles</p>
<small><?php echo $publishedArticles; ?> published</small>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">ποΈ</div>
<div class="stat-details">
<h3><?php echo number_format($totalViews ?: 0); ?></h3>
<p>Total Views</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">π¬</div>
<div class="stat-details">
<h3><?php echo $totalComments; ?></h3>
<p>Comments</p>
<?php if ($pendingComments > 0): ?>
<small class="pending"><?php echo $pendingComments; ?> pending</small>
<?php endif; ?>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">π₯</div>
<div class="stat-details">
<h3><?php echo $totalUsers; ?></h3>
<p>Users</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">π§</div>
<div class="stat-details">
<h3><?php echo $totalSubscribers; ?></h3>
<p>Subscribers</p>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="quick-actions">
<a href="add-article.php" class="action-card">
<span class="action-icon">β</span>
<span>Write New Article</span>
</a>
<a href="media.php" class="action-card">
<span class="action-icon">π·</span>
<span>Upload Media</span>
</a>
<a href="comments.php?filter=pending" class="action-card">
<span class="action-icon">π¬</span>
<span>Moderate Comments</span>
<?php if ($pendingComments > 0): ?>
<span class="badge"><?php echo $pendingComments; ?></span>
<?php endif; ?>
</a>
<a href="newsletter.php" class="action-card">
<span class="action-icon">π§</span>
<span>Send Newsletter</span>
</a>
</div>
<div class="dashboard-grid">
<!-- Recent Articles -->
<div class="card">
<div class="card-header">
<h3>Recent Articles</h3>
<a href="articles.php" class="btn btn-sm">View All</a>
</div>
<div class="card-body">
<table class="data-table">
<thead>
<tr>
<th>Title</th>
<th>Category</th>
<th>Author</th>
<th>Views</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($recentArticles as $article): ?>
<tr>
<td><?php echo $article['title']; ?></td>
<td><?php echo $article['category_name']; ?></td>
<td><?php echo $article['author_name']; ?></td>
<td><?php echo number_format($article['views']); ?></td>
<td>
<span class="badge badge-<?php echo $article['status']; ?>">
<?php echo $article['status']; ?>
</span>
</td>
<td>
<a href="edit-article.php?id=<?php echo $article['id']; ?>" class="btn-small">Edit</a>
<a href="../article.php?slug=<?php echo $article['slug']; ?>" class="btn-small" target="_blank">View</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Popular Articles -->
<div class="card">
<div class="card-header">
<h3>Most Viewed</h3>
</div>
<div class="card-body">
<table class="data-table">
<thead>
<tr>
<th>Title</th>
<th>Views</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($popularArticles as $article): ?>
<tr>
<td><?php echo $article['title']; ?></td>
<td><?php echo number_format($article['views']); ?></td>
<td>
<a href="edit-article.php?id=<?php echo $article['id']; ?>" class="btn-small">Edit</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Recent Comments -->
<div class="card">
<div class="card-header">
<h3>Recent Comments</h3>
<a href="comments.php" class="btn btn-sm">View All</a>
</div>
<div class="card-body">
<table class="data-table">
<thead>
<tr>
<th>Comment</th>
<th>Article</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($recentComments as $comment): ?>
<tr>
<td><?php echo substr($comment['content'], 0, 50); ?>...</td>
<td>
<a href="../article.php?slug=<?php echo $comment['article_slug']; ?>" target="_blank">
<?php echo $comment['article_title']; ?>
</a>
</td>
<td>
<span class="badge badge-<?php echo $comment['status']; ?>">
<?php echo $comment['status']; ?>
</span>
</td>
<td>
<a href="comments.php?action=approve&id=<?php echo $comment['id']; ?>" class="btn-small">Approve</a>
<a href="comments.php?action=delete&id=<?php echo $comment['id']; ?>" class="btn-small btn-danger">Delete</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</main>
</div>
<script src="../assets/js/admin.js"></script>
</body>
</html>
6. Admin - Manage Articles (admin/articles.php)
<?php
require_once '../includes/config.php';
require_once '../includes/auth.php';
Auth::checkLogin();
if (!isAdmin()) {
redirect('../index.php');
}
// Handle article deletion
if (isset($_GET['delete'])) {
$id = (int)$_GET['delete'];
// Delete article and related data
$pdo->beginTransaction();
// Delete comments first (foreign key constraint)
$pdo->prepare("DELETE FROM comments WHERE article_id = ?")->execute([$id]);
// Delete article-tag relationships
$pdo->prepare("DELETE FROM article_tags WHERE article_id = ?")->execute([$id]);
// Delete article
$pdo->prepare("DELETE FROM articles WHERE id = ?")->execute([$id]);
$pdo->commit();
$_SESSION['success'] = 'Article deleted successfully';
redirect('articles.php');
}
// Handle status change
if (isset($_GET['status']) && isset($_GET['id'])) {
$id = (int)$_GET['id'];
$status = $_GET['status'];
if (in_array($status, ['draft', 'published', 'archived'])) {
$pdo->prepare("UPDATE articles SET status = ? WHERE id = ?")->execute([$status, $id]);
$_SESSION['success'] = 'Article status updated';
}
redirect('articles.php');
}
// Get filter parameters
$status_filter = $_GET['status'] ?? '';
$category_filter = isset($_GET['category']) ? (int)$_GET['category'] : 0;
$search = $_GET['search'] ?? '';
// Build query
$query = "
SELECT a.*, u.full_name as author_name, c.name as category_name
FROM articles a
JOIN users u ON a.author_id = u.id
JOIN categories c ON a.category_id = c.id
WHERE 1=1
";
$params = [];
if ($status_filter) {
$query .= " AND a.status = ?";
$params[] = $status_filter;
}
if ($category_filter > 0) {
$query .= " AND a.category_id = ?";
$params[] = $category_filter;
}
if ($search) {
$query .= " AND (a.title LIKE ? OR a.content LIKE ?)";
$params[] = "%$search%";
$params[] = "%$search%";
}
$query .= " ORDER BY a.created_at DESC";
$stmt = $pdo->prepare($query);
$stmt->execute($params);
$articles = $stmt->fetchAll();
// Get categories for filter
$categories = $pdo->query("SELECT * FROM categories ORDER BY name")->fetchAll();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manage Articles - <?php echo SITE_NAME; ?></title>
<link rel="stylesheet" href="../assets/css/admin.css">
</head>
<body>
<div class="admin-wrapper">
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-header">
<h2>News Admin</h2>
<p>Welcome, <?php echo $_SESSION['user_name']; ?></p>
</div>
<nav class="sidebar-nav">
<ul>
<li><a href="dashboard.php">Dashboard</a></li>
<li><a href="articles.php" class="active">Articles</a></li>
<li><a href="categories.php">Categories</a></li>
<li><a href="comments.php">Comments</a></li>
<li><a href="users.php">Users</a></li>
<li><a href="media.php">Media Library</a></li>
<li><a href="newsletter.php">Newsletter</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">
<div class="page-header">
<h1>Manage Articles</h1>
<a href="add-article.php" class="btn btn-primary">+ Write New Article</a>
</div>
<?php if (isset($_SESSION['success'])): ?>
<div class="alert alert-success"><?php echo $_SESSION['success']; unset($_SESSION['success']); ?></div>
<?php endif; ?>
<!-- Filters -->
<div class="filters card">
<form method="GET" action="" class="filter-form">
<div class="form-row">
<div class="form-group">
<input type="text" name="search" placeholder="Search articles..."
value="<?php echo htmlspecialchars($search); ?>">
</div>
<div class="form-group">
<select name="category">
<option value="">All Categories</option>
<?php foreach ($categories as $cat): ?>
<option value="<?php echo $cat['id']; ?>"
<?php echo $category_filter == $cat['id'] ? 'selected' : ''; ?>>
<?php echo $cat['name']; ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<select name="status">
<option value="">All Status</option>
<option value="draft" <?php echo $status_filter == 'draft' ? 'selected' : ''; ?>>Draft</option>
<option value="published" <?php echo $status_filter == 'published' ? 'selected' : ''; ?>>Published</option>
<option value="archived" <?php echo $status_filter == 'archived' ? 'selected' : ''; ?>>Archived</option>
</select>
</div>
<button type="submit" class="btn">Filter</button>
<a href="articles.php" class="btn">Reset</a>
</div>
</form>
</div>
<!-- Articles Table -->
<div class="card">
<div class="card-body">
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Category</th>
<th>Author</th>
<th>Views</th>
<th>Comments</th>
<th>Published</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($articles as $article): ?>
<?php
$comment_count = $pdo->prepare("SELECT COUNT(*) FROM comments WHERE article_id = ?");
$comment_count->execute([$article['id']]);
$comments = $comment_count->fetchColumn();
?>
<tr>
<td>#<?php echo $article['id']; ?></td>
<td>
<strong><?php echo $article['title']; ?></strong>
<?php if ($article['is_featured']): ?>
<span class="badge badge-featured">Featured</span>
<?php endif; ?>
<?php if ($article['is_breaking']): ?>
<span class="badge badge-breaking">Breaking</span>
<?php endif; ?>
</td>
<td><?php echo $article['category_name']; ?></td>
<td><?php echo $article['author_name']; ?></td>
<td><?php echo number_format($article['views']); ?></td>
<td><?php echo $comments; ?></td>
<td><?php echo $article['published_at'] ? formatDate($article['published_at'], 'M d, Y') : '-'; ?></td>
<td>
<span class="badge badge-<?php echo $article['status']; ?>">
<?php echo $article['status']; ?>
</span>
</td>
<td class="actions">
<a href="edit-article.php?id=<?php echo $article['id']; ?>" class="btn-small">Edit</a>
<a href="../article.php?slug=<?php echo $article['slug']; ?>" class="btn-small" target="_blank">View</a>
<?php if ($article['status'] != 'published'): ?>
<a href="?status=published&id=<?php echo $article['id']; ?>" class="btn-small btn-success">Publish</a>
<?php endif; ?>
<?php if ($article['status'] != 'draft'): ?>
<a href="?status=draft&id=<?php echo $article['id']; ?>" class="btn-small btn-warning">Draft</a>
<?php endif; ?>
<a href="?delete=<?php echo $article['id']; ?>"
class="btn-small btn-danger"
onclick="return confirm('Are you sure you want to delete this article? This will also delete all comments.')">Delete</a>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($articles)): ?>
<tr>
<td colspan="9" class="text-center">No articles found</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</main>
</div>
</body>
</html>
7. Admin - Add Article (admin/add-article.php)
<?php
require_once '../includes/config.php';
require_once '../includes/auth.php';
Auth::checkLogin();
if (!isAdmin()) {
redirect('../index.php');
}
// Get categories
$categories = $pdo->query("SELECT * FROM categories WHERE status = 'active' ORDER BY name")->fetchAll();
// Get tags
$tags = $pdo->query("SELECT * FROM tags ORDER BY name")->fetchAll();
$errors = [];
$form_data = [
'title' => '',
'content' => '',
'excerpt' => '',
'category_id' => '',
'featured_image' => '',
'status' => 'draft',
'is_featured' => 0,
'is_breaking' => 0,
'tags' => []
];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Sanitize input
$form_data['title'] = sanitize($_POST['title']);
$form_data['content'] = $_POST['content']; // Allow HTML from TinyMCE
$form_data['excerpt'] = sanitize($_POST['excerpt']);
$form_data['category_id'] = (int)$_POST['category_id'];
$form_data['status'] = $_POST['status'];
$form_data['is_featured'] = isset($_POST['is_featured']) ? 1 : 0;
$form_data['is_breaking'] = isset($_POST['is_breaking']) ? 1 : 0;
$form_data['tags'] = $_POST['tags'] ?? [];
// Validate
if (empty($form_data['title'])) {
$errors['title'] = 'Title is required';
}
if (empty($form_data['content'])) {
$errors['content'] = 'Content is required';
}
if (empty($form_data['category_id'])) {
$errors['category_id'] = 'Category is required';
}
// Handle image upload
if (isset($_FILES['featured_image']) && $_FILES['featured_image']['error'] == 0) {
$allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$max_size = 2 * 1024 * 1024; // 2MB
if (!in_array($_FILES['featured_image']['type'], $allowed)) {
$errors['featured_image'] = 'Only JPG, PNG, GIF, and WEBP images are allowed';
} elseif ($_FILES['featured_image']['size'] > $max_size) {
$errors['featured_image'] = 'Image size must be less than 2MB';
} else {
$extension = pathinfo($_FILES['featured_image']['name'], PATHINFO_EXTENSION);
$filename = uniqid() . '.' . $extension;
$upload_path = '../assets/images/uploads/' . $filename;
if (move_uploaded_file($_FILES['featured_image']['tmp_name'], $upload_path)) {
$form_data['featured_image'] = $filename;
} else {
$errors['featured_image'] = 'Failed to upload image';
}
}
}
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 articles WHERE slug = ?");
$check->execute([$slug]);
if ($check->fetch()) {
$slug .= '-' . uniqid();
}
// Insert article
$stmt = $pdo->prepare("
INSERT INTO articles (title, slug, content, excerpt, featured_image, category_id, author_id,
status, is_featured, is_breaking, published_at, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
");
$published_at = $form_data['status'] == 'published' ? date('Y-m-d H:i:s') : null;
$stmt->execute([
$form_data['title'],
$slug,
$form_data['content'],
$form_data['excerpt'],
$form_data['featured_image'],
$form_data['category_id'],
$_SESSION['user_id'],
$form_data['status'],
$form_data['is_featured'],
$form_data['is_breaking'],
$published_at
]);
$article_id = $pdo->lastInsertId();
// Insert tags
if (!empty($form_data['tags'])) {
$tag_stmt = $pdo->prepare("INSERT INTO article_tags (article_id, tag_id) VALUES (?, ?)");
foreach ($form_data['tags'] as $tag_id) {
$tag_stmt->execute([$article_id, $tag_id]);
}
}
$pdo->commit();
$_SESSION['success'] = 'Article created successfully';
redirect('articles.php');
} catch (Exception $e) {
$pdo->rollBack();
$errors['general'] = 'Failed to create article: ' . $e->getMessage();
}
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Add Article - <?php echo SITE_NAME; ?></title>
<link rel="stylesheet" href="../assets/css/admin.css">
<script src="../assets/vendor/tinymce/tinymce.min.js"></script>
</head>
<body>
<div class="admin-wrapper">
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-header">
<h2>News Admin</h2>
<p>Welcome, <?php echo $_SESSION['user_name']; ?></p>
</div>
<nav class="sidebar-nav">
<ul>
<li><a href="dashboard.php">Dashboard</a></li>
<li><a href="articles.php" class="active">Articles</a></li>
<li><a href="categories.php">Categories</a></li>
<li><a href="comments.php">Comments</a></li>
<li><a href="users.php">Users</a></li>
<li><a href="media.php">Media Library</a></li>
<li><a href="newsletter.php">Newsletter</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">
<div class="page-header">
<h1>Add New Article</h1>
<a href="articles.php" class="btn">β Back to Articles</a>
</div>
<?php if (!empty($errors['general'])): ?>
<div class="alert alert-error"><?php echo $errors['general']; ?></div>
<?php endif; ?>
<div class="card">
<div class="card-body">
<form method="POST" action="" enctype="multipart/form-data" class="admin-form">
<div class="form-row">
<div class="form-group col-md-8">
<label for="title">Title *</label>
<input type="text" id="title" name="title"
value="<?php echo htmlspecialchars($form_data['title']); ?>" required>
<?php if (isset($errors['title'])): ?>
<span class="error"><?php echo $errors['title']; ?></span>
<?php endif; ?>
</div>
<div class="form-group col-md-4">
<label for="category_id">Category *</label>
<select id="category_id" name="category_id" required>
<option value="">Select 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['name']; ?>
</option>
<?php endforeach; ?>
</select>
<?php if (isset($errors['category_id'])): ?>
<span class="error"><?php echo $errors['category_id']; ?></span>
<?php endif; ?>
</div>
</div>
<div class="form-group">
<label for="content">Content *</label>
<textarea id="content" name="content" rows="15"><?php echo $form_data['content']; ?></textarea>
<?php if (isset($errors['content'])): ?>
<span class="error"><?php echo $errors['content']; ?></span>
<?php endif; ?>
</div>
<div class="form-group">
<label for="excerpt">Excerpt (Short summary)</label>
<textarea id="excerpt" name="excerpt" rows="3"><?php echo $form_data['excerpt']; ?></textarea>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="featured_image">Featured Image</label>
<input type="file" id="featured_image" name="featured_image" accept="image/*">
<?php if (isset($errors['featured_image'])): ?>
<span class="error"><?php echo $errors['featured_image']; ?></span>
<?php endif; ?>
<small>Max size: 2MB. Allowed: JPG, PNG, GIF, WEBP</small>
</div>
<div class="form-group col-md-6">
<label for="tags">Tags</label>
<select id="tags" name="tags[]" multiple size="5">
<?php foreach ($tags as $tag): ?>
<option value="<?php echo $tag['id']; ?>"
<?php echo in_array($tag['id'], $form_data['tags']) ? 'selected' : ''; ?>>
<?php echo $tag['name']; ?>
</option>
<?php endforeach; ?>
</select>
<small>Hold Ctrl/Cmd to select multiple tags</small>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-3">
<label>
<input type="checkbox" name="is_featured" value="1"
<?php echo $form_data['is_featured'] ? 'checked' : ''; ?>>
Featured Article
</label>
</div>
<div class="form-group col-md-3">
<label>
<input type="checkbox" name="is_breaking" value="1"
<?php echo $form_data['is_breaking'] ? 'checked' : ''; ?>>
Breaking News
</label>
</div>
<div class="form-group col-md-3">
<label for="status">Status</label>
<select id="status" name="status">
<option value="draft" <?php echo $form_data['status'] == 'draft' ? 'selected' : ''; ?>>Draft</option>
<option value="published" <?php echo $form_data['status'] == 'published' ? 'selected' : ''; ?>>Published</option>
</select>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save Article</button>
<a href="articles.php" class="btn">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</main>
</div>
<script>
// Initialize TinyMCE
tinymce.init({
selector: '#content',
height: 500,
menubar: true,
plugins: [
'advlist autolink lists link image charmap print preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste code help wordcount'
],
toolbar: 'undo redo | formatselect | bold italic backcolor | \
alignleft aligncenter alignright alignjustify | \
bullist numlist outdent indent | removeformat | help',
images_upload_url: 'upload-image.php',
automatic_uploads: true,
file_picker_types: 'image',
relative_urls: false,
remove_script_host: false,
convert_urls: true
});
</script>
</body>
</html>
8. CSS Styling (assets/css/style.css)
/* Main Styles for News Portal */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background: #f4f6f9;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
/* Header Styles */
.site-header {
background: #fff;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.top-header {
background: #2c3e50;
color: #fff;
padding: 0.5rem 0;
font-size: 0.9rem;
}
.top-header .container {
display: flex;
justify-content: space-between;
align-items: center;
}
.top-header a {
color: #fff;
text-decoration: none;
margin-left: 1rem;
}
.top-header a:hover {
color: #3498db;
}
.main-header {
padding: 1rem 0;
}
.main-header .container {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo h1 {
font-size: 2rem;
color: #2c3e50;
}
.logo a {
text-decoration: none;
color: inherit;
}
.search-bar {
position: relative;
width: 300px;
}
.search-bar form {
display: flex;
}
.search-bar input {
flex: 1;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px 0 0 4px;
font-size: 0.9rem;
}
.search-bar button {
padding: 0.5rem 1rem;
background: #3498db;
color: #fff;
border: none;
border-radius: 0 4px 4px 0;
cursor: pointer;
}
.search-bar button:hover {
background: #2980b9;
}
.search-suggestions {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: #fff;
border: 1px solid #ddd;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
z-index: 1000;
display: none;
}
.search-suggestions a {
display: block;
padding: 0.5rem;
color: #333;
text-decoration: none;
border-bottom: 1px solid #eee;
}
.search-suggestions a:hover {
background: #f5f5f5;
}
/* Navigation */
.main-nav {
background: #34495e;
border-top: 1px solid #2c3e50;
}
.nav-menu {
list-style: none;
display: flex;
padding: 0;
}
.nav-menu li {
position: relative;
}
.nav-menu li a {
display: block;
padding: 1rem 1.5rem;
color: #fff;
text-decoration: none;
font-weight: 500;
transition: background 0.3s;
}
.nav-menu li a:hover {
background: #2c3e50;
}
/* Breaking News */
.breaking-news {
background: #e74c3c;
color: #fff;
padding: 0.5rem 0;
}
.breaking-news .container {
display: flex;
align-items: center;
}
.breaking-label {
background: #c0392b;
padding: 0.25rem 1rem;
font-weight: bold;
margin-right: 1rem;
border-radius: 3px;
}
.breaking-ticker {
overflow: hidden;
white-space: nowrap;
}
.breaking-ticker a {
display: inline-block;
color: #fff;
text-decoration: none;
margin-right: 2rem;
animation: ticker 20s linear infinite;
}
.breaking-ticker a:hover {
text-decoration: underline;
}
@keyframes ticker {
0% { transform: translateX(100%); }
100% { transform: translateX(-100%); }
}
/* Featured Section */
.featured-section {
margin: 2rem 0;
}
.section-title {
font-size: 1.8rem;
color: #2c3e50;
margin-bottom: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 3px solid #3498db;
}
.featured-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(2, 250px);
gap: 1rem;
}
.featured-item {
position: relative;
overflow: hidden;
border-radius: 8px;
}
.featured-1 {
grid-column: span 2;
grid-row: span 2;
}
.featured-2, .featured-3 {
grid-column: span 1;
grid-row: span 1;
}
.featured-item a {
display: block;
height: 100%;
text-decoration: none;
color: #fff;
}
.featured-image {
position: relative;
height: 100%;
}
.featured-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.category-badge {
position: absolute;
top: 1rem;
left: 1rem;
background: #3498db;
color: #fff;
padding: 0.25rem 0.75rem;
border-radius: 3px;
font-size: 0.8rem;
font-weight: bold;
z-index: 2;
}
.featured-content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 2rem 1rem 1rem;
background: linear-gradient(transparent, rgba(0,0,0,0.8));
}
.featured-content h3 {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.featured-content p {
font-size: 0.9rem;
opacity: 0.9;
margin-bottom: 0.5rem;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.meta {
font-size: 0.8rem;
opacity: 0.8;
}
.meta span {
margin-right: 1rem;
}
/* Content Layout */
.content-wrapper {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 2rem;
margin: 2rem 0;
}
/* News Grid */
.news-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.news-card {
background: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
transition: transform 0.3s;
}
.news-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.news-card a {
text-decoration: none;
color: inherit;
}
.card-image {
position: relative;
height: 200px;
}
.card-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.category-tag {
position: absolute;
top: 0.5rem;
left: 0.5rem;
background: #3498db;
color: #fff;
padding: 0.25rem 0.5rem;
border-radius: 3px;
font-size: 0.75rem;
font-weight: bold;
}
.card-content {
padding: 1rem;
}
.card-content h3 {
font-size: 1.1rem;
margin-bottom: 0.5rem;
line-height: 1.4;
height: 3rem;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.card-content p {
font-size: 0.9rem;
color: #666;
margin-bottom: 1rem;
height: 2.7rem;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.card-meta {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: #999;
}
/* Sidebar */
.sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.widget {
background: #fff;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.widget-title {
font-size: 1.2rem;
color: #2c3e50;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #3498db;
}
/* Popular Widget */
.popular-list {
list-style: none;
}
.popular-list li {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid #eee;
}
.popular-list li:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.popular-list a {
text-decoration: none;
color: inherit;
display: block;
}
.popular-title {
display: block;
font-weight: 500;
margin-bottom: 0.25rem;
}
.popular-views {
font-size: 0.8rem;
color: #999;
}
/* Categories Widget */
.categories-list {
list-style: none;
}
.categories-list li {
margin-bottom: 0.5rem;
}
.categories-list a {
display: block;
padding: 0.5rem;
background: #f8f9fa;
color: #333;
text-decoration: none;
border-radius: 4px;
transition: background 0.3s;
}
.categories-list a:hover {
background: #3498db;
color: #fff;
}
/* Newsletter Widget */
.newsletter-form input {
width: 100%;
padding: 0.75rem;
margin-bottom: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.newsletter-form .btn {
width: 100%;
}
/* Article Page */
.single-article {
background: #fff;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
margin-bottom: 2rem;
}
.article-header {
margin-bottom: 2rem;
}
.article-meta-top {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.article-category {
background: #3498db;
color: #fff;
padding: 0.25rem 0.75rem;
border-radius: 3px;
font-weight: bold;
}
.article-date {
color: #999;
}
.article-title {
font-size: 2.5rem;
color: #2c3e50;
margin-bottom: 1rem;
line-height: 1.3;
}
.article-author {
display: flex;
align-items: center;
gap: 1rem;
}
.author-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
object-fit: cover;
}
.author-info .author-name {
font-weight: bold;
color: #2c3e50;
}
.author-bio {
font-size: 0.9rem;
color: #666;
}
.article-featured-image {
margin: 2rem -2rem;
}
.article-featured-image img {
width: 100%;
max-height: 500px;
object-fit: cover;
}
.article-content {
font-size: 1.1rem;
line-height: 1.8;
color: #333;
}
.article-content p {
margin-bottom: 1.5rem;
}
.article-content h2,
.article-content h3 {
margin: 2rem 0 1rem;
color: #2c3e50;
}
.article-content img {
max-width: 100%;
height: auto;
border-radius: 8px;
margin: 1.5rem 0;
}
.article-footer {
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.share-buttons {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
}
.share-buttons a {
padding: 0.5rem 1rem;
border-radius: 4px;
color: #fff;
text-decoration: none;
font-size: 0.9rem;
}
.share-facebook { background: #3b5998; }
.share-twitter { background: #1da1f2; }
.share-linkedin { background: #0077b5; }
/* Comments Section */
.comments-section {
background: #fff;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
margin-bottom: 2rem;
}
.comment-form-wrapper {
margin-bottom: 2rem;
}
.comment-form {
background: #f8f9fa;
padding: 1.5rem;
border-radius: 8px;
}
.comments-list {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.comment {
display: flex;
gap: 1rem;
padding: 1rem;
background: #f8f9fa;
border-radius: 8px;
}
.comment-avatar img {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.comment-content {
flex: 1;
}
.comment-meta {
margin-bottom: 0.5rem;
}
.comment-author {
font-weight: bold;
color: #2c3e50;
margin-right: 1rem;
}
.comment-date {
font-size: 0.8rem;
color: #999;
}
.comment-text {
line-height: 1.6;
}
/* Pagination */
.pagination {
display: flex;
justify-content: center;
gap: 0.5rem;
margin: 2rem 0;
}
.page-link {
padding: 0.5rem 1rem;
background: #fff;
color: #333;
text-decoration: none;
border: 1px solid #ddd;
border-radius: 4px;
transition: all 0.3s;
}
.page-link:hover,
.page-link.active {
background: #3498db;
color: #fff;
border-color: #3498db;
}
/* Load More */
.load-more {
text-align: center;
margin: 2rem 0;
}
.load-more .btn {
padding: 0.75rem 2rem;
font-size: 1rem;
}
/* Footer */
.site-footer {
background: #2c3e50;
color: #fff;
padding: 3rem 0 1rem;
margin-top: 3rem;
}
.footer-widgets {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
margin-bottom: 2rem;
}
.footer-widget h4 {
color: #fff;
margin-bottom: 1rem;
}
.footer-widget p,
.footer-widget ul {
color: #bdc3c7;
}
.footer-widget ul {
list-style: none;
}
.footer-widget ul li {
margin-bottom: 0.5rem;
}
.footer-widget a {
color: #bdc3c7;
text-decoration: none;
transition: color 0.3s;
}
.footer-widget a:hover {
color: #fff;
}
.social-links {
display: flex;
gap: 1rem;
}
.footer-bottom {
text-align: center;
padding-top: 2rem;
border-top: 1px solid #34495e;
color: #bdc3c7;
}
/* Responsive */
@media (max-width: 768px) {
.main-header .container {
flex-direction: column;
gap: 1rem;
}
.search-bar {
width: 100%;
}
.featured-grid {
grid-template-columns: 1fr;
grid-template-rows: auto;
}
.featured-1,
.featured-2,
.featured-3 {
grid-column: span 1;
grid-row: span 1;
height: 200px;
}
.content-wrapper {
grid-template-columns: 1fr;
}
.article-title {
font-size: 1.8rem;
}
.article-footer {
flex-direction: column;
gap: 1rem;
text-align: center;
}
.share-buttons {
justify-content: center;
}
.comment {
flex-direction: column;
}
.comment-avatar {
align-self: center;
}
}
@media (max-width: 480px) {
.nav-menu {
flex-wrap: wrap;
}
.nav-menu li {
flex: 1 1 50%;
text-align: center;
}
.news-grid {
grid-template-columns: 1fr;
}
.footer-widgets {
grid-template-columns: 1fr;
text-align: center;
}
.social-links {
justify-content: center;
}
}
π 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
news-portal
Step 3: Set Up Database
- Open browser and go to
http://localhost/phpmyadmin - Click on "SQL" tab
- Copy the entire SQL from
database/news_portal.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 "editor123 hash: " . password_hash('editor123', PASSWORD_DEFAULT);
?>
Run it: http://localhost/news-portal/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', 'news_portal');
define('DB_USER', 'root');
define('DB_PASS', '');
Step 6: Create Required Directories
Create these folders and set proper permissions:
assets/images/uploads/- for article imagesassets/images/- for avatars and default images
Step 7: Download Third-Party Libraries
- Download TinyMCE from tiny.cloud and place in
assets/vendor/tinymce/ - Download Chart.js from chartjs.org and place in
assets/vendor/chart.js/
Step 8: Test the Application
Public Side:
- Open:
http://localhost/news-portal/ - Browse news articles
- Click on categories
- Read full articles
- Leave comments
Admin Side:
- Open:
http://localhost/news-portal/admin/login.php - Login with: Username:
admin, Password:admin123 - Or login as editor: Username:
editor, Password:editor123
Step 9: Add Sample Content via Admin
- Login to admin panel
- Go to Categories β Add categories (Politics, Technology, Sports, etc.)
- Go to Articles β Add new article with content and featured image
- Publish the article
- Check the homepage to see your article
Step 10: Customize Site Settings
- Go to Settings in admin panel
- Update site title, description, contact information
- Add social media links
- Configure comment moderation
- Upload site logo
π― Features Summary
Public User Features:
- β Browse news by categories
- β View featured and breaking news
- β Read full articles with images
- β Search articles
- β Leave comments (with moderation)
- β Subscribe to newsletter
- β Share articles on social media
- β Responsive design
Admin Features:
- β Secure login with role-based access
- β Dashboard with statistics
- β Article management (CRUD)
- β Rich text editor for content
- β Category management
- β Comment moderation
- β User management
- β Media library
- β Newsletter management
- β Site settings customization
Technical Features:
- β Secure password hashing
- β Session management
- β SQL injection prevention
- β XSS protection
- β SEO-friendly URLs (slugs)
- β Pagination
- β Infinite scroll (AJAX)
- β Live search suggestions
- β Responsive design
- β Database relationships
π Future Enhancements
- Multi-language Support: Add language switcher
- Social Login: Login with Facebook/Google
- Push Notifications: Browser push notifications for breaking news
- Video Integration: Embed YouTube/Vimeo videos
- Advertisement Management: Ad spaces management
- Analytics Dashboard: Detailed visitor analytics
- Mobile App: React Native or Flutter app
- RSS Feeds: Generate RSS feeds for categories
- API: REST API for mobile apps
- Email Campaigns: Advanced newsletter campaigns
- User Roles: More granular permissions (author, editor, admin)
- Scheduled Posts: Schedule articles for future publishing
- Image Gallery: Create image galleries within articles
- Related Posts: AI-powered related articles
- SEO Tools: Meta tags, sitemap generator
- Backup & Restore: Automated database backup
- Import/Export: Import articles from CSV/XML
- Custom Fields: Add custom fields to articles
- User Profiles: Public author profiles
- Comment Replies: Nested comments system
This comprehensive News Portal Website provides a complete solution for online news publishing with powerful content management capabilities!