Blog system enhancements: fix publish/unpublish permissions, add action buttons to blog listings, update gallery to show only published blog images, improve blog card layout and description truncation
17
.htaccess
@@ -59,8 +59,8 @@ RewriteRule ^view_album$ src/pages/gallery/view_album.php [L]
|
|||||||
|
|
||||||
# === EVENTS & BLOG PAGES ===
|
# === EVENTS & BLOG PAGES ===
|
||||||
RewriteRule ^events$ src/pages/events/events.php [L]
|
RewriteRule ^events$ src/pages/events/events.php [L]
|
||||||
RewriteRule ^blog$ src/pages/events/blog.php [L]
|
RewriteRule ^blog$ src/pages/blog/blog.php [L]
|
||||||
RewriteRule ^blog_details$ src/pages/events/blog_details.php [L]
|
RewriteRule ^blog_details$ src/pages/blog/blog_details.php [L]
|
||||||
RewriteRule ^best_of_the_eastern_cape_2024$ src/pages/events/best_of_the_eastern_cape_2024.php [L]
|
RewriteRule ^best_of_the_eastern_cape_2024$ src/pages/events/best_of_the_eastern_cape_2024.php [L]
|
||||||
RewriteRule ^2025_agm_minutes$ src/pages/events/2025_agm_minutes.php [L]
|
RewriteRule ^2025_agm_minutes$ src/pages/events/2025_agm_minutes.php [L]
|
||||||
RewriteRule ^agm_content$ src/pages/events/agm_content.php [L]
|
RewriteRule ^agm_content$ src/pages/events/agm_content.php [L]
|
||||||
@@ -135,6 +135,19 @@ RewriteRule ^get_album_photos$ src/processors/get_album_photos.php [L]
|
|||||||
RewriteRule ^link_membership_user$ src/processors/link_membership_user.php [L]
|
RewriteRule ^link_membership_user$ src/processors/link_membership_user.php [L]
|
||||||
RewriteRule ^unlink_membership_user$ src/processors/unlink_membership_user.php [L]
|
RewriteRule ^unlink_membership_user$ src/processors/unlink_membership_user.php [L]
|
||||||
|
|
||||||
|
# Blog routes
|
||||||
|
RewriteRule ^admin_blogs$ src/pages/blog/admin_blogs.php [L]
|
||||||
|
RewriteRule ^user_blogs$ src/pages/blog/user_blogs.php [L]
|
||||||
|
RewriteRule ^blog_read$ src/pages/blog/blog_read.php [L]
|
||||||
|
RewriteRule ^blog_edit$ src/pages/blog/blog_edit.php [L]
|
||||||
|
RewriteRule ^blog_create$ src/processors/blog/blog_create.php [L]
|
||||||
|
RewriteRule ^blog_delete$ src/processors/blog/blog_delete.php [L]
|
||||||
|
RewriteRule ^publish_blog$ src/processors/blog/publish_blog.php [L]
|
||||||
|
RewriteRule ^blog_unpublish$ src/processors/blog/blog_unpublish.php [L]
|
||||||
|
RewriteRule ^submit_blog$ src/processors/blog/submit_blog.php [L]
|
||||||
|
RewriteRule ^upload_blog_image$ src/processors/blog/upload_blog_image.php [L]
|
||||||
|
RewriteRule ^autosave$ src/processors/blog/autosave.php [L]
|
||||||
|
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
||||||
php_flag display_errors On
|
php_flag display_errors On
|
||||||
|
|||||||
@@ -7124,7 +7124,7 @@ blockquote {
|
|||||||
/* Comments */
|
/* Comments */
|
||||||
.comments {
|
.comments {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid var(--border-color); }
|
/* border: 1px solid var(--border-color); } */
|
||||||
|
|
||||||
.comment-body {
|
.comment-body {
|
||||||
padding: 50px; }
|
padding: 50px; }
|
||||||
|
|||||||
@@ -277,12 +277,12 @@ if ($headerStyle === 'light') {
|
|||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="events">Events</a></li>
|
<li><a href="events">Events</a></li>
|
||||||
<li><a href="blog">Blog</a></li>
|
|
||||||
<?php if ($role === 'admin' || $role === 'superadmin') { ?>
|
<?php if ($role === 'admin' || $role === 'superadmin') { ?>
|
||||||
<li class="dropdown"><a href="#">admin</a>
|
<li class="dropdown"><a href="#">admin</a>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="admin_web_users">Website Users</a></li>
|
<li><a href="admin_web_users">Website Users</a></li>
|
||||||
<li><a href="admin_members">4WDCSA Members</a></li>
|
<li><a href="admin_members">4WDCSA Members</a></li>
|
||||||
|
<li><a href="admin_blogs">Manage Blogs</a></li>
|
||||||
<li><a href="admin_events">Manage Events</a></li>
|
<li><a href="admin_events">Manage Events</a></li>
|
||||||
<li><a href="admin_trips">Manage Trips</a></li>
|
<li><a href="admin_trips">Manage Trips</a></li>
|
||||||
<li><a href="admin_trip_bookings">Trip Bookings</a></li>
|
<li><a href="admin_trip_bookings">Trip Bookings</a></li>
|
||||||
@@ -299,6 +299,7 @@ if ($headerStyle === 'light') {
|
|||||||
<?php if ($is_logged_in) : ?>
|
<?php if ($is_logged_in) : ?>
|
||||||
<li class="dropdown"><a href="#">Members Area</a>
|
<li class="dropdown"><a href="#">Members Area</a>
|
||||||
<ul>
|
<ul>
|
||||||
|
<li><a href="blog">Blog</a></li>
|
||||||
<?php
|
<?php
|
||||||
if (getUserMemberStatus($_SESSION['user_id'])) {
|
if (getUserMemberStatus($_SESSION['user_id'])) {
|
||||||
echo "<li><a href=\"campsites\">Campsites Directory</a></li>";
|
echo "<li><a href=\"campsites\">Campsites Directory</a></li>";
|
||||||
@@ -318,6 +319,7 @@ if ($headerStyle === 'light') {
|
|||||||
<li><a href="account_settings">Account Settings</a></li>
|
<li><a href="account_settings">Account Settings</a></li>
|
||||||
<li><a href="membership_details">Membership</a></li>
|
<li><a href="membership_details">Membership</a></li>
|
||||||
<li><a href="bookings">My Bookings</a></li>
|
<li><a href="bookings">My Bookings</a></li>
|
||||||
|
<li><a href="user_blogs">My Blog Posts</a></li>
|
||||||
<li><a href="submit_pop">Submit P.O.P</a></li>
|
<li><a href="submit_pop">Submit P.O.P</a></li>
|
||||||
<li><a href="logout">Log Out</a></li>
|
<li><a href="logout">Log Out</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
126
index.php
@@ -38,7 +38,7 @@ if (isset($_SESSION['user_id']) && isset($conn) && $conn !== null) {
|
|||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<?php
|
<?php
|
||||||
$bannerFolder = 'assets/images/banners/';
|
$bannerFolder = 'assets/images/banners/';
|
||||||
$bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
$bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
||||||
@@ -190,13 +190,13 @@ if (countUpcomingTrips() > 0) { ?>
|
|||||||
<!-- About Us Area end -->
|
<!-- About Us Area end -->
|
||||||
|
|
||||||
<section class="hotel-area bgc-black py-100 rel z-1">
|
<section class="hotel-area bgc-black py-100 rel z-1">
|
||||||
<div class="countdown-container">
|
<div class="countdown-container">
|
||||||
<h1 style="color: #e5f5e0;" id="countdown">Loading countdown...</h1>
|
<h1 style="color: #e5f5e0;" id="countdown">Loading countdown...</h1>
|
||||||
<a href="events.php" class="theme-btn style-two bgc-secondary" style="margin-top: 20px; background-color: #e90000; padding: 10px 20px; color: white; text-decoration: none; border-radius: 25px;">
|
<a href="events.php" class="theme-btn style-two bgc-secondary" style="margin-top: 20px; background-color: #e90000; padding: 10px 20px; color: white; text-decoration: none; border-radius: 25px;">
|
||||||
<span data-hover="Events">Find out more!</span>
|
<span data-hover="Events">Find out more!</span>
|
||||||
<i class="fal fa-arrow-right"></i>
|
<i class="fal fa-arrow-right"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Features Area start -->
|
<!-- Features Area start -->
|
||||||
@@ -302,8 +302,8 @@ if (countUpcomingTrips() > 0) { ?>
|
|||||||
<!-- <li><i class="fal fa-router"></i> Internet</li> -->
|
<!-- <li><i class="fal fa-router"></i> Internet</li> -->
|
||||||
</ul>
|
</ul>
|
||||||
<div class="destination-footer">
|
<div class="destination-footer">
|
||||||
<span class="price"><span>R <?= getPrice('driver_training', 'member');?></span>/for members</span>
|
<span class="price"><span>R <?= getPrice('driver_training', 'member'); ?></span>/for members</span>
|
||||||
<span class="price"><span>R <?= getPrice('driver_training', 'nonmember');?></span>/for non-members</span>
|
<span class="price"><span>R <?= getPrice('driver_training', 'nonmember'); ?></span>/for non-members</span>
|
||||||
<a href="driver_training.php" class="read-more">Book Now <i class="fal fa-angle-right"></i></a>
|
<a href="driver_training.php" class="read-more">Book Now <i class="fal fa-angle-right"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -327,8 +327,8 @@ if (countUpcomingTrips() > 0) { ?>
|
|||||||
<!-- <li><i class="fal fa-router"></i> Internet</li> -->
|
<!-- <li><i class="fal fa-router"></i> Internet</li> -->
|
||||||
</ul>
|
</ul>
|
||||||
<div class="destination-footer">
|
<div class="destination-footer">
|
||||||
<span class="price"><span>R <?= getPrice('bush_mechanics', 'member');?></span>/for members</span>
|
<span class="price"><span>R <?= getPrice('bush_mechanics', 'member'); ?></span>/for members</span>
|
||||||
<span class="price"><span>R <?= getPrice('bush_mechanics', 'nonmember');?></span>/for non-members</span>
|
<span class="price"><span>R <?= getPrice('bush_mechanics', 'nonmember'); ?></span>/for non-members</span>
|
||||||
<a href="bush_mechanics.php" class="read-more">Book Now <i class="fal fa-angle-right"></i></a>
|
<a href="bush_mechanics.php" class="read-more">Book Now <i class="fal fa-angle-right"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -347,8 +347,8 @@ if (countUpcomingTrips() > 0) { ?>
|
|||||||
<!-- <li><i class="fal fa-router"></i> Internet</li> -->
|
<!-- <li><i class="fal fa-router"></i> Internet</li> -->
|
||||||
</ul>
|
</ul>
|
||||||
<div class="destination-footer">
|
<div class="destination-footer">
|
||||||
<span class="price"><span>R <?= getPrice('rescue_recovery', 'member');?></span>/for members</span>
|
<span class="price"><span>R <?= getPrice('rescue_recovery', 'member'); ?></span>/for members</span>
|
||||||
<span class="price"><span>R <?= getPrice('rescue_recovery', 'nonmember');?></span>/for non-members</span>
|
<span class="price"><span>R <?= getPrice('rescue_recovery', 'nonmember'); ?></span>/for non-members</span>
|
||||||
<a href="rescue_recovery.php" class="read-more">Book Now <i class="fal fa-angle-right"></i></a>
|
<a href="rescue_recovery.php" class="read-more">Book Now <i class="fal fa-angle-right"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -379,68 +379,90 @@ if (countUpcomingTrips() > 0) { ?>
|
|||||||
</div>
|
</div>
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<?php
|
<?php
|
||||||
$sql = "SELECT blog_id, title, date, category, image, description, author, link, members_only FROM blogs WHERE status = 'published' ORDER BY date DESC LIMIT 3";
|
$result = $conn->prepare("
|
||||||
$result = $conn->query($sql);
|
SELECT
|
||||||
|
b.blog_id,
|
||||||
|
b.title,
|
||||||
|
b.description,
|
||||||
|
b.category,
|
||||||
|
b.status,
|
||||||
|
b.date,
|
||||||
|
b.image,
|
||||||
|
b.members_only,
|
||||||
|
CONCAT(u.first_name, ' ', u.last_name) AS author_name,
|
||||||
|
u.email AS author_email,
|
||||||
|
u.profile_pic
|
||||||
|
FROM blogs b
|
||||||
|
JOIN users u ON b.author = u.user_id
|
||||||
|
WHERE b.status = 'published'
|
||||||
|
ORDER BY b.date DESC
|
||||||
|
");
|
||||||
|
|
||||||
if ($result->num_rows > 0) {
|
$result->execute();
|
||||||
|
$posts = $result->get_result();
|
||||||
|
|
||||||
|
if ($posts->num_rows > 0) {
|
||||||
// Loop through each row
|
// Loop through each row
|
||||||
while ($row = $result->fetch_assoc()) {
|
while ($post = $posts->fetch_assoc()):
|
||||||
$blog_id = $row['blog_id'];
|
$blog_id = $post['blog_id'];
|
||||||
$blog_title = $row['title'];
|
$blog_title = $post['title'];
|
||||||
$blog_date = $row['date'];
|
$blog_date = $post['date'];
|
||||||
$blog_category = $row['category'];
|
$blog_category = $post['category'];
|
||||||
$blog_image = $row['image'];
|
$blog_image = $post['image'];
|
||||||
$blog_description = $row['description'];
|
$blog_description = $post['description'];
|
||||||
$blog_author = $row['author'];
|
$members_only = $post['members_only'];
|
||||||
$members_only = $row['members_only'];
|
if ($members_only) {
|
||||||
if($members_only){
|
if (!isset($_SESSION['user_id'])) {
|
||||||
if (!isset($_SESSION['user_id'])){
|
$blog_link = "login";
|
||||||
$blog_link = "login.php";
|
|
||||||
$button_hover = "Members Only";
|
$button_hover = "Members Only";
|
||||||
$icon = "fa-lock";
|
$icon = "fa-lock";
|
||||||
}else{
|
} else {
|
||||||
if (getUserMemberStatus($_SESSION['user_id'])) {
|
if (getUserMemberStatus($_SESSION['user_id'])) {
|
||||||
$blog_link = $row['link'];
|
$blog_link = "blog_read?token=" . encryptData($blog_id, $salt);
|
||||||
$button_hover = "Read More";
|
$button_hover = "Read More";
|
||||||
$icon = "fa-arrow-right";
|
$icon = "fa-arrow-right";
|
||||||
}else{
|
} else {
|
||||||
$blog_link = "membership.php";
|
$blog_link = "membership";
|
||||||
$button_hover = "Members Only";
|
$button_hover = "Members Only";
|
||||||
$icon = "fa-lock";
|
$icon = "fa-lock";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}else{
|
} else {
|
||||||
$blog_link = $row['link'];
|
$blog_link = "blog_read?token=" . encryptData($blog_id, $salt);
|
||||||
$button_hover = "Read More";
|
$button_hover = "Read More";
|
||||||
$icon = "fa-arrow-right";
|
$icon = "fa-arrow-right";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
<div class="col-xl-4 col-md-6">
|
<div class="col-xl-4 col-md-6">
|
||||||
<div class="blog-item" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
<div class="blog-item" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
<div class="content">
|
<div class="content" style="width:100%;">
|
||||||
<a href="#" class="category">' . $blog_category . '</a>
|
|
||||||
<h5><a href="' . $blog_link . '">' . $blog_title . '</a></h5>
|
<div class="destination-header d-flex align-items-start gap-3">
|
||||||
<ul class="blog-meta">
|
|
||||||
<li><i class="far fa-calendar-alt"></i> <a href="#">' . $blog_date . '</a></li>
|
<img src="' . $post["profile_pic"] . '" alt="Author" class="rounded-circle border" width="60" height="60">
|
||||||
<li><i class="far fa-user"></i>' . getFullName($blog_author) . '</li>
|
<div>
|
||||||
</ul>
|
<span class="badge bg-dark mb-1">' . strtoupper($post["category"]) . '</span>
|
||||||
|
<h5 class="mb-0">' . $post["title"] . '</h5>
|
||||||
|
<small class="text-muted">' . $post["author_name"] . '</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p style="max-height: 60px; overflow: hidden;">' . $post["description"] . '</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="image">
|
<div class="image">
|
||||||
<img style="border-radius:20px;" src="assets/images/blog/' . $blog_id . '/' . $blog_image . '" alt="Blog List">
|
<img style="aspect-ratio: 4 / 3; object-fit: cover; object-position: center; border-radius:20px; width: 100%; display: block;" src="' . $blog_image . '" alt="Blog List">
|
||||||
</div>
|
</div>
|
||||||
<a style="width:100%;" href="' . $blog_link . '" class="theme-btn">
|
<a style="width:100%;" href="' . $blog_link . '" class="theme-btn">
|
||||||
<span style="width:100%;" data-hover="'.$button_hover.'">Read More</span>
|
<span style="width:100%;" data-hover="' . $button_hover . '">Read More</span>
|
||||||
<i class="fal '.$icon.'"></i>
|
<i class="fal ' . $icon . '"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>';
|
</div>';
|
||||||
|
endwhile;
|
||||||
|
} else {
|
||||||
|
echo "<p>No blog posts available.</p>";
|
||||||
}
|
}
|
||||||
// Close connection
|
|
||||||
$conn->close();
|
?>
|
||||||
} ?>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -538,8 +560,8 @@ if (countUpcomingTrips() > 0) { ?>
|
|||||||
</div>
|
</div>
|
||||||
<!--End pagewrapper-->
|
<!--End pagewrapper-->
|
||||||
<?php if ($indemnityPending): ?>
|
<?php if ($indemnityPending): ?>
|
||||||
<!-- Bootstrap Modal -->
|
<!-- Bootstrap Modal -->
|
||||||
<div class="modal fade" id="indemnityModal" tabindex="-1" aria-labelledby="indemnityModalLabel" aria-hidden="true">
|
<div class="modal fade" id="indemnityModal" tabindex="-1" aria-labelledby="indemnityModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
<div class="modal-content border-secondary">
|
<div class="modal-content border-secondary">
|
||||||
<div class="modal-header bg-secondary text-white">
|
<div class="modal-header bg-secondary text-white">
|
||||||
@@ -552,15 +574,15 @@ if (countUpcomingTrips() > 0) { ?>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Show modal when page loads
|
// Show modal when page loads
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
var indemnityModal = new bootstrap.Modal(document.getElementById('indemnityModal'));
|
var indemnityModal = new bootstrap.Modal(document.getElementById('indemnityModal'));
|
||||||
indemnityModal.show();
|
indemnityModal.show();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
|
||||||
@@ -588,7 +610,7 @@ if (countUpcomingTrips() > 0) { ?>
|
|||||||
<script src="assets/js/script.js"></script>
|
<script src="assets/js/script.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Set your target date and time
|
// Set your target date and time
|
||||||
const targetDate = new Date("<?php echo getNextOpenDayDate();?>T08:00:00"); // yyyy-mm-ddThh:mm:ss
|
const targetDate = new Date("<?php echo getNextOpenDayDate(); ?>T08:00:00"); // yyyy-mm-ddThh:mm:ss
|
||||||
|
|
||||||
function updateCountdown() {
|
function updateCountdown() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|||||||
@@ -2064,7 +2064,7 @@ function getCommentCount($page_id) {
|
|||||||
|
|
||||||
// Prepare statement to avoid SQL injection
|
// Prepare statement to avoid SQL injection
|
||||||
$stmt = $conn->prepare("SELECT COUNT(*) FROM comments WHERE page_id = ?");
|
$stmt = $conn->prepare("SELECT COUNT(*) FROM comments WHERE page_id = ?");
|
||||||
$stmt->bind_param("i", $page_id);
|
$stmt->bind_param("s", $page_id);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
|
|
||||||
// Get result
|
// Get result
|
||||||
|
|||||||
165
src/pages/blog/admin_blogs.php
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<?php
|
||||||
|
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||||
|
require_once($rootPath . "/src/config/env.php");
|
||||||
|
require_once($rootPath . "/src/config/connection.php");
|
||||||
|
require_once($rootPath . "/src/config/functions.php");
|
||||||
|
require_once($rootPath . "/header.php");
|
||||||
|
checkAdmin();
|
||||||
|
checkUserSession();
|
||||||
|
|
||||||
|
$pageTitle = 'Manage Blog Posts';
|
||||||
|
$breadcrumbs = [['Home' => 'index']];
|
||||||
|
require_once($rootPath . '/components/banner.php');
|
||||||
|
|
||||||
|
$result = $conn->prepare("
|
||||||
|
SELECT
|
||||||
|
b.blog_id,
|
||||||
|
b.title,
|
||||||
|
b.description,
|
||||||
|
b.status,
|
||||||
|
b.date,
|
||||||
|
b.image,
|
||||||
|
CONCAT(u.first_name, ' ', u.last_name) AS author_name,
|
||||||
|
u.email AS author_email,
|
||||||
|
u.profile_pic
|
||||||
|
FROM blogs b
|
||||||
|
JOIN users u ON b.author = u.user_id
|
||||||
|
WHERE b.status = 'published'
|
||||||
|
ORDER BY b.date DESC
|
||||||
|
");
|
||||||
|
|
||||||
|
$result->execute();
|
||||||
|
$posts = $result->get_result();
|
||||||
|
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.image {
|
||||||
|
width: 400px;
|
||||||
|
/* Set your desired width */
|
||||||
|
height: 350px;
|
||||||
|
/* Set your desired height */
|
||||||
|
overflow: hidden;
|
||||||
|
/* Hide any overflow */
|
||||||
|
display: block;
|
||||||
|
/* Ensure proper block behavior */
|
||||||
|
}
|
||||||
|
|
||||||
|
.image img {
|
||||||
|
width: 100%;
|
||||||
|
/* Image scales to fill the container */
|
||||||
|
height: 100%;
|
||||||
|
/* Image scales to fill the container */
|
||||||
|
object-fit: cover;
|
||||||
|
/* Fills the container while maintaining aspect ratio */
|
||||||
|
object-position: top;
|
||||||
|
/* Aligns the top of the image with the top of the container */
|
||||||
|
display: block;
|
||||||
|
/* Prevents inline whitespace issues */
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$bannerFolder = 'assets/images/banners/';
|
||||||
|
$bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- Blog List Area start -->
|
||||||
|
<section class="blog-list-page py-100 rel z-1">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
|
||||||
|
<h2>Manage Blog Posts</h2>
|
||||||
|
<?php if (isset($_SESSION['message'])): ?>
|
||||||
|
<div class="alert alert-warning message-box">
|
||||||
|
<?php echo $_SESSION['message']; ?>
|
||||||
|
<span class="close-btn" onclick="this.parentElement.style.display='none'">×</span>
|
||||||
|
</div>
|
||||||
|
<?php unset($_SESSION['message']); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<a href="blog_create.php">+ New Post</a>
|
||||||
|
|
||||||
|
<?php while ($post = $posts->fetch_assoc()):
|
||||||
|
// Determine cover image - use provided image or fallback placeholder
|
||||||
|
$coverImage = $post["image"] ? $post["image"] : 'assets/images/placeholder.jpg';
|
||||||
|
// Output the HTML structure with dynamic data
|
||||||
|
echo '
|
||||||
|
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="image" style="width:200px;height:200px;">
|
||||||
|
<img src="' . htmlspecialchars($coverImage) . '" alt="' . htmlspecialchars($post["title"]) . '">
|
||||||
|
</div>
|
||||||
|
<div class="content" style="width:100%;">
|
||||||
|
<div class="destination-header d-flex align-items-start gap-3">
|
||||||
|
<img src="' . $post["profile_pic"] . '" alt="Author" class="rounded-circle border" width="80" height="80">
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-dark mb-1">' . strtoupper($post["status"]) . '</span>
|
||||||
|
<h5 class="mb-0">' . $post["title"] . '</h5>
|
||||||
|
<small class="text-muted">' . $post["author_name"] . '</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>' . $post["description"] . '</p>
|
||||||
|
<div class="destination-footer">
|
||||||
|
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;">
|
||||||
|
<a href="blog_edit.php?token='.encryptData($post["blog_id"], $salt).'" data-bs-toggle="tooltip" data-bs-placement="top" title="Edit"><span class="material-icons">edit</span></a>
|
||||||
|
<a href="blog_read.php?token='.encryptData($post["blog_id"], $salt).'" data-bs-toggle="tooltip" data-bs-placement="top" title="Preview"><span class="material-icons">visibility</span></a>
|
||||||
|
<button type="button" class="publish-btn" data-blog-id="' . $post["blog_id"] . '" data-status="' . $post["status"] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="' . ($post["status"] == "published" ? "Unpublish" : "Publish") . '" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">' . ($post["status"] == "published" ? "cloud_off" : "cloud_upload") . '</span></button>
|
||||||
|
<a href="blog_delete.php?token='.encryptData($post["blog_id"], $salt).'" data-bs-toggle="tooltip" data-bs-placement="top" title="Delete"><span class="material-icons">delete</span></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
';
|
||||||
|
endwhile; ?>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Blog List Area end -->
|
||||||
|
<script>
|
||||||
|
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||||
|
tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el));
|
||||||
|
|
||||||
|
// Handle publish/unpublish button clicks
|
||||||
|
document.querySelectorAll('.publish-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const blogId = this.dataset.blogId;
|
||||||
|
const status = this.dataset.status;
|
||||||
|
const action = status === 'published' ? 'unpublish' : 'publish';
|
||||||
|
const endpoint = status === 'published' ? 'blog_unpublish' : 'publish_blog';
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('id', blogId);
|
||||||
|
|
||||||
|
fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
alert(action.charAt(0).toUpperCase() + action.slice(1) + ' successful!');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert(action + ' failed.');
|
||||||
|
console.error('Error:', response.statusText);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Error:', err);
|
||||||
|
alert(action + ' failed due to network error.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<?php include_once($rootPath . '/components/insta_footer.php'); ?>
|
||||||
241
src/pages/blog/blog.php
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
<?php
|
||||||
|
$headerStyle = 'light';
|
||||||
|
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||||
|
include_once($rootPath . '/header.php');
|
||||||
|
?>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.image {
|
||||||
|
width: 400px;
|
||||||
|
/* Set your desired width */
|
||||||
|
height: 350px;
|
||||||
|
/* Set your desired height */
|
||||||
|
overflow: hidden;
|
||||||
|
/* Hide any overflow */
|
||||||
|
display: block;
|
||||||
|
/* Ensure proper block behavior */
|
||||||
|
}
|
||||||
|
|
||||||
|
.image img {
|
||||||
|
width: 100%;
|
||||||
|
/* Image scales to fill the container */
|
||||||
|
height: 100%;
|
||||||
|
/* Image scales to fill the container */
|
||||||
|
object-fit: cover;
|
||||||
|
/* Fills the container while maintaining aspect ratio */
|
||||||
|
object-position: top;
|
||||||
|
/* Aligns the top of the image with the top of the container */
|
||||||
|
display: block;
|
||||||
|
/* Prevents inline whitespace issues */
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
</style><?php
|
||||||
|
$pageTitle = 'Blogs';
|
||||||
|
$breadcrumbs = [['Home' => 'index.php']];
|
||||||
|
require_once($rootPath . '/components/banner.php');
|
||||||
|
?>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Blog List Area start -->
|
||||||
|
<section class="blog-list-page py-100 rel z-1">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<?php
|
||||||
|
// Query to retrieve data from the trips table
|
||||||
|
$result = $conn->prepare("
|
||||||
|
SELECT
|
||||||
|
b.blog_id,
|
||||||
|
b.title,
|
||||||
|
b.description,
|
||||||
|
b.category,
|
||||||
|
b.status,
|
||||||
|
b.date,
|
||||||
|
b.image,
|
||||||
|
b.members_only,
|
||||||
|
CONCAT(u.first_name, ' ', u.last_name) AS author_name,
|
||||||
|
u.email AS author_email,
|
||||||
|
u.profile_pic
|
||||||
|
FROM blogs b
|
||||||
|
JOIN users u ON b.author = u.user_id
|
||||||
|
WHERE b.status = 'published'
|
||||||
|
ORDER BY b.date DESC
|
||||||
|
");
|
||||||
|
|
||||||
|
$result->execute();
|
||||||
|
$posts = $result->get_result();
|
||||||
|
|
||||||
|
if ($posts->num_rows > 0) {
|
||||||
|
// Loop through each row
|
||||||
|
while ($post = $posts->fetch_assoc()):
|
||||||
|
$blog_id = $post['blog_id'];
|
||||||
|
$blog_title = $post['title'];
|
||||||
|
$blog_date = $post['date'];
|
||||||
|
$blog_category = $post['category'];
|
||||||
|
$blog_image = $post['image'];
|
||||||
|
$blog_description = $post['description'];
|
||||||
|
$members_only = $post['members_only'];
|
||||||
|
if ($members_only) {
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
$blog_link = "login";
|
||||||
|
$button_hover = "Members Only";
|
||||||
|
$icon = "fa-lock";
|
||||||
|
} else {
|
||||||
|
if (getUserMemberStatus($_SESSION['user_id'])) {
|
||||||
|
$blog_link = "blog_read?token=" . encryptData($blog_id, $salt);
|
||||||
|
$button_hover = "Read More";
|
||||||
|
$icon = "fa-arrow-right";
|
||||||
|
} else {
|
||||||
|
$blog_link = "membership";
|
||||||
|
$button_hover = "Members Only";
|
||||||
|
$icon = "fa-lock";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$blog_link = "blog_read?token=" . encryptData($blog_id, $salt);
|
||||||
|
$button_hover = "Read More";
|
||||||
|
$icon = "fa-arrow-right";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output the HTML structure with dynamic data
|
||||||
|
echo '
|
||||||
|
<div class="blog-item style-three" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="image" style="border-radius:20px; width:300px;height: 250px;margin-right:0px;">
|
||||||
|
<img src="' . htmlspecialchars($blog_image) . '" alt="' . htmlspecialchars($post["title"]) . '">
|
||||||
|
</div>
|
||||||
|
<div style="padding: 10px; height: 100%; width:100%;">
|
||||||
|
<div class="destination-header d-flex align-items-start gap-3" style="width:100%; align-items: flex-start;">
|
||||||
|
<img src="' . $post["profile_pic"] . '" alt="Author" class="rounded-circle border" width="60" height="60">
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-dark mb-1">' . strtoupper($post["category"]) . '</span>
|
||||||
|
<h5 class="mb-0">' . $post["title"] . '</h5>
|
||||||
|
<small class="text-muted">' . $post["author_name"] . '</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>' . $post["description"] . '</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
';
|
||||||
|
endwhile;
|
||||||
|
} else {
|
||||||
|
echo '<p>No blog posts found.</p>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- <ul class="pagination pt-15 flex-wrap" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<span class="page-link"><i class="far fa-chevron-left"></i></span>
|
||||||
|
</li>
|
||||||
|
<li class="page-item active">
|
||||||
|
<span class="page-link">
|
||||||
|
1
|
||||||
|
<span class="sr-only">(current)</span>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="page-item"><a class="page-link" href="#">2</a></li>
|
||||||
|
<li class="page-item"><a class="page-link" href="#">3</a></li>
|
||||||
|
<li class="page-item"><a class="page-link" href="#">...</a></li>
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="#"><i class="far fa-chevron-right"></i></a>
|
||||||
|
</li>
|
||||||
|
</ul> -->
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4 col-md-8 col-sm-10 rmt-75">
|
||||||
|
<div class="blog-sidebar">
|
||||||
|
|
||||||
|
<div class="widget widget-search" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<form action="#" class="default-search-form" onsubmit="return false;">
|
||||||
|
<input type="text" id="blog-search" placeholder="Search" required="">
|
||||||
|
<button type="submit" class="searchbutton far fa-search"></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="widget widget-gallery" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<h5 class="widget-title">Gallery</h5>
|
||||||
|
<div class="gallery">
|
||||||
|
<?php
|
||||||
|
// Get IDs of published blogs
|
||||||
|
$published_blogs = $conn->query("SELECT blog_id FROM blogs WHERE status = 'published'");
|
||||||
|
$blog_ids = [];
|
||||||
|
while ($blog = $published_blogs->fetch_assoc()) {
|
||||||
|
$blog_ids[] = $blog['blog_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display images from published blogs only
|
||||||
|
if (!empty($blog_ids)) {
|
||||||
|
foreach ($blog_ids as $bid) {
|
||||||
|
$folder = $rootPath . '/uploads/blogs/' . $bid . '/';
|
||||||
|
if (is_dir($folder)) {
|
||||||
|
$files = glob($folder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
||||||
|
if (!empty($files)) {
|
||||||
|
foreach ($files as $file) {
|
||||||
|
// Skip cover images
|
||||||
|
if (basename($file) !== 'cover.' . pathinfo($file, PATHINFO_EXTENSION)) {
|
||||||
|
$relativePath = '/uploads/blogs/' . $bid . '/' . basename($file);
|
||||||
|
echo '<a href="' . $relativePath . '" style="width: 110px; height: 110px; overflow: hidden; display: inline-block; margin: 2px;">';
|
||||||
|
echo '<img src="' . $relativePath . '" alt="Gallery" style="width: 100%; height: 100%; object-fit: cover; display: block;">';
|
||||||
|
echo '</a>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <div class="widget widget-cta" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="content text-white">
|
||||||
|
<span class="h6">Explore The World</span>
|
||||||
|
<h3>Become a Member</h3>
|
||||||
|
<a href="membership" class="theme-btn style-two bgc-secondary">
|
||||||
|
<span data-hover="Explore Now">Join Now</span>
|
||||||
|
<i class="fal fa-arrow-right"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="image">
|
||||||
|
<img src="assets/images/logos/weblogo.png" alt="CTA">
|
||||||
|
</div>
|
||||||
|
<div class="cta-shape"><img src="assets/images/widgets/cta-shape.png" alt="Shape"></div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Blog List Area end -->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const searchInput = document.getElementById('blog-search');
|
||||||
|
const blogItems = document.querySelectorAll('.blog-item');
|
||||||
|
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.addEventListener('keyup', function() {
|
||||||
|
const searchTerm = this.value.toLowerCase();
|
||||||
|
|
||||||
|
blogItems.forEach(function(item) {
|
||||||
|
const title = item.querySelector('h5').textContent.toLowerCase();
|
||||||
|
const category = item.querySelector('.category').textContent.toLowerCase();
|
||||||
|
const description = item.querySelector('p').textContent.toLowerCase();
|
||||||
|
const author = item.querySelector('.blog-meta li:nth-child(2)').textContent.toLowerCase();
|
||||||
|
|
||||||
|
if (title.includes(searchTerm) || category.includes(searchTerm) || description.includes(searchTerm) || author.includes(searchTerm)) {
|
||||||
|
item.style.display = '';
|
||||||
|
} else {
|
||||||
|
item.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php include_once(dirname(dirname(dirname(__DIR__))) . '/components/insta_footer.php'); ?>
|
||||||
533
src/pages/blog/blog_details.php
Normal file
@@ -0,0 +1,533 @@
|
|||||||
|
<?php
|
||||||
|
$headerStyle = 'light';
|
||||||
|
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||||
|
include_once($rootPath . '/header.php');
|
||||||
|
?>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.image {
|
||||||
|
width: 400px;
|
||||||
|
/* Set your desired width */
|
||||||
|
height: 350px;
|
||||||
|
/* Set your desired height */
|
||||||
|
overflow: hidden;
|
||||||
|
/* Hide any overflow */
|
||||||
|
display: block;
|
||||||
|
/* Ensure proper block behavior */
|
||||||
|
}
|
||||||
|
|
||||||
|
.image img {
|
||||||
|
width: 100%;
|
||||||
|
/* Image scales to fill the container */
|
||||||
|
height: 100%;
|
||||||
|
/* Image scales to fill the container */
|
||||||
|
object-fit: cover;
|
||||||
|
/* Fills the container while maintaining aspect ratio */
|
||||||
|
object-position: top;
|
||||||
|
/* Aligns the top of the image with the top of the container */
|
||||||
|
display: block;
|
||||||
|
/* Prevents inline whitespace issues */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$pageTitle = 'Blog Details';
|
||||||
|
$breadcrumbs = [['Home' => 'index.php'], ['Blogs' => 'blog.php']];
|
||||||
|
require_once($rootPath . '/components/banner.php');
|
||||||
|
?>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Blog Detaisl Area start -->
|
||||||
|
<section class="blog-detaisl-page py-100 rel z-1">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<div class="blog-details-content" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<a href="blog.html" class="category">Travel</a>
|
||||||
|
<ul class="blog-meta mb-30">
|
||||||
|
<li><img src="assets/images/pp/default.png" alt="Admin"> <a href="#">John Runciman</a></li>
|
||||||
|
<li><i class="far fa-calendar-alt"></i> <a href="#">25 Feb 2024</a></li>
|
||||||
|
<li><i class="far fa-comments"></i> <a href="#">Comments (5)</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>Every year, Noelene and I organise a trip through the Eastern Cape, with the highlight being traversing Baviaanskloof. Each trip has been slightly different to the previous one, with this trip, in my opinion, being the best one!</p>
|
||||||
|
|
||||||
|
<p>The idea was to meet up at the village at the mouth of the Bushman’s River, Boesmansriviermond, near Kenton-on-Sea. Mike and Clara arrived a few days early and we enjoyed a ride up the Bushman’s River in our little boat and walks on the beach.</p>
|
||||||
|
|
||||||
|
<p>The rest of the group—Roy and Naome, Doug and Santie, and Dave and Valery—arrived on the Friday, the day before the official departure. Doug and Dave booked a campsite at Cannon Rocks, 20 or so kilometres from Bushman’s. We arranged a braai for that evening, and I admit that I was shocked to my little toes when I saw that Doug and Dave had brought a caravan and camping trailer along. This is definitely not a caravan or trailer-friendly route and I voiced my hesitation.</p>
|
||||||
|
|
||||||
|
<p>The long and the short was that Doug decided to continue despite my fears, and Dave decided to withdraw from the trip. This was not entirely due to my warnings but also to Valery not feeling up to scratch. We also heard that Roger would not be able to make it because of personal problems at home.</p>
|
||||||
|
|
||||||
|
<h5>Saturday: Bushman’s to Ocean View</h5>
|
||||||
|
|
||||||
|
<p>On Saturday morning, the remaining four vehicles met at Bushman’s River with our first destination set for Bathurst for breakfast. We drove via the "poor man’s game drive" (the old main road from Port Elizabeth to Port Alfred, now incorporated into the Sibuya Game Reserve) and the winding road through the spectacular Cowie River Valley.</p>
|
||||||
|
|
||||||
|
<p>After brunch (the trip took longer than expected due to the bad roads), we wandered along to the Fish River Lighthouse, a place worth a visit. This historic building was erected in the late 19th century with the light first shining on 1 July 1898. The warning light has a strength of 5,000,000 candelas and is 85 metres above the high water mark with a shine range of 32 sea miles. Wish I had that on the front of my Hilux!</p>
|
||||||
|
|
||||||
|
<p>The most unique feature about the light is that it has no bearings for the 2-ton light to spin on, but rather it floats in a bed of mercury—ingenious!</p>
|
||||||
|
|
||||||
|
<p>From there, we drove back past the Bushman’s River, towards Boknes, and onto the scenic gravel road going to Alexandria that services all the dairy farms in the area. We turned off the gravel onto a farm road and came out at a camping site, Ocean View, where we arranged to spend 2 nights amongst the dense Eastern Cape bush on the edge of the sand dunes. This made for a snug campsite sheltered from the wind.</p>
|
||||||
|
|
||||||
|
<p><strong>Interest:</strong> The location of this campsite is on the eastern edge of the area with the largest shifting dunes in the southern hemisphere—truly spectacular!</p>
|
||||||
|
|
||||||
|
<h5>Sunday: Beach Day</h5>
|
||||||
|
|
||||||
|
<p>The next day was spent exploring the beach—miles and miles of pristine beach where there is not another soul to be seen!</p>
|
||||||
|
|
||||||
|
<h5>Monday: To Brakkeduine</h5>
|
||||||
|
|
||||||
|
<p>Monday morning, bright and early, we set off towards Port Elizabeth where we planned to leave Max, our faithful hound, for the duration of the trip, then on to Humansdorp and finally to a resort called Brakkeduine. Doug and Santie, pulling their caravan, suffered a puncture and stopped in the little town of Alexandria to have the tyre repaired. We decided that the remainder would go on in convoy through Port Elizabeth and meet them there.</p>
|
||||||
|
|
||||||
|
<p>Once clear of Port Elizabeth, the three remaining vehicles followed the R102, down the old Van Staden’s Pass, across the single lane bridge spanning the Gamtoos River, and past Jefferey’s Bay. At Humansdorp, we hit the gravel roads and eventually reached Brakkeduine in the late afternoon. Doug and Santie were already there, with Doug trying his hand at fishing in the dam. The campsites are to die for—set along manicured grassy terraced ledges overlooking the dam, each site separated by neatly trimmed hedges.</p>
|
||||||
|
|
||||||
|
<h5>Tuesday: Dune Adventure</h5>
|
||||||
|
|
||||||
|
<p>The following morning we met Johan, our guide for the day. After airing down (0.6 bar!), we set off in convoy to attack the dunes. Before we reached the first dune, Doug pulled a tyre off the rim. We all got stuck in to repair the wheel and were on the road again fifteen minutes later. The airjack proved its usefulness!</p>
|
||||||
|
|
||||||
|
<p>We played in the sand for the next few hours, then Roy managed to pull one of his tyres off the rim—on a steep incline and in the boiling heat of the midday sun. This time the airjack did not do so well! We were eventually forced to use Mike’s trusty hi-lift jack. Eventually, we changed wheels and headed for camp, then back to Humansdorp to get the wheel repaired.</p>
|
||||||
|
|
||||||
|
<p>Doug had also picked up a problem with his Prado, and he and Santie decided to head to the Toyota garage in Joubertina, further along the R62, with the plan that we would all meet up again in Kareedouw.</p>
|
||||||
|
|
||||||
|
<h5>Wednesday: Rus en Vrede Trail</h5>
|
||||||
|
|
||||||
|
<p>From Kareedouw, we headed off north into the mountains. The road was rocky and full of loose stones. I was concerned about the tyres on Doug’s Prado and caravan, but we arrived at our camp as the sun was setting. Baviaans Lodge is situated in the Kouga Mountains at the start of the Rus en Vrede trail across the mountains to the Baviaanskloof. The campsite is cosy, set among the trees on the bank of a small stream. There is a hot water shower and toilets, all well maintained and clean.</p>
|
||||||
|
|
||||||
|
<p>We enjoyed an evening around the campfire, though I went to bed concerned about Doug pulling his caravan over the mountains.</p>
|
||||||
|
|
||||||
|
<p>On Wednesday morning, everyone was packed and ready to go by 08:00. The day was slow going but with no delays or problems. The only casualty was the awning from Mike’s Cruiser, which was shaken free and rescued by Roy.</p>
|
||||||
|
|
||||||
|
<p>The Rus en Vrede trail, originally cut by woodcutters in the 1800s, now crosses three farms. It consists of gravel, loose rocks, eroded farm tracks, and mountain terrain. There are 13 gates that had to be opened and closed—thank you Noelene and Naome!</p>
|
||||||
|
|
||||||
|
<p>The views are breathtaking, covering seven different mountain ranges. We were lucky with the weather—clear skies, no wind, and cool temperatures. The proteas were in bloom and the centuries-old cycads stood tall over the peaks.</p>
|
||||||
|
|
||||||
|
<h5>Thursday: Into the Kloof</h5>
|
||||||
|
|
||||||
|
<p>The trail ends at Rus en Vrede farm, where you pay the farmer per vehicle and person. We entered the Baviaanskloof Nature Reserve, crossing Holgat’s Pass, Kombrink’s Pass, and the Grootrivier Pass. The roads were rough and slow-going but scenic.</p>
|
||||||
|
|
||||||
|
<p>Our destination was Kudu Kaya, a working citrus farm. We camped on a hill overlooking the farm. Doug did some repairs to the caravan and Santie spent time cleaning up food shaken loose—custard and gunk everywhere!</p>
|
||||||
|
|
||||||
|
<h5>Friday: To Kaboega</h5>
|
||||||
|
|
||||||
|
<p>Thursday morning, we drove to Steytlerville via Antonie’s Pass—a rugged rock and gravel road. After lunch at the Royal Hotel in Steytlerville, we continued to Kaboega, a private farm near Addo Elephant Park. We camped at a big dam and were warmly welcomed by Ian Ritchie and his wife Sandy.</p>
|
||||||
|
|
||||||
|
<p>Friday morning, Ian and Sandy joined us for coffee. Sandy shared insights into Bushman’s paintings and local history. Ian then led us around the 6,000-hectare farm, sharing his deep knowledge of biodiversity, plants, and terrain. Apart from a locked gate we had to cut open, the day was smooth. We ended with a swim in a mountain pool instead of visiting more rock art sites due to the time.</p>
|
||||||
|
|
||||||
|
<h5>Saturday: Mountain Zebra Park</h5>
|
||||||
|
|
||||||
|
<p>On Saturday, we took scenic gravel roads to the Mountain Zebra Park via Somerset East and Cradock. After breakfast in Somerset East, we passed through Swarthoek and Maraiskloof Passes to Cradock for fuel, then entered the Park and set up camp.</p>
|
||||||
|
|
||||||
|
<p>Though we originally planned to stay one night, everyone decided to stay an extra day for game drives. The reserve is home to a wide range of plains animals, especially the rare mountain zebra, and other wildlife found in the gorges and valleys.</p>
|
||||||
|
<div style="width:100%; object-fit: cover;" class="image mt-40 mb-30" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<img src="assets/images/blog/1/widecrab.jpg" alt="Blog Details">
|
||||||
|
</div>
|
||||||
|
<h5>Services Offered by a Tour and Travel Agency</h5>
|
||||||
|
<p>Agency plays a pivotal role in crafting memorable experiences for travelers by offering wide range services tailored to individual preferences. Whether it's a family vacation, an adventure trip, or luxury getaway well-established travel agency can handle everything from flight bookings and accommodation to guided tours .</p>
|
||||||
|
<ul class="list-style-two mt-30 mb-45" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<li>Assisting customers in booking domestic and international flights.</li>
|
||||||
|
<li>Organizing adventure activities such as trekking, diving, safaris, or extreme sports.</li>
|
||||||
|
<li>Tailoring travel plans to meet the specific needs and preferences of the customer.</li>
|
||||||
|
<li>Providing professional guides for city tours, cultural experiences, adventure activities, etc.</li>
|
||||||
|
<li>Arranging local transportation such as car rentals, airport transfers, or bus tours.</li>
|
||||||
|
<li>Helping customers navigate the visa application process for international travel.</li>
|
||||||
|
</ul>
|
||||||
|
<div class="row mb-10">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="image mb-30" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<img src="assets/images/blog/blog-middle1.jpg" alt="Blog">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="image mb-30" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50" data-aos-delay="50">
|
||||||
|
<img src="assets/images/blog/blog-middle2.jpg" alt="Blog">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h5>How to Start a Tour and Travel Agency</h5>
|
||||||
|
<p>Agency plays a pivotal role in crafting memorable experiences for travelers by offering wide range services tailored to individual preferences. Whether it's a family vacation, an adventure trip, or luxury getaway well-established travel agency can handle everything from flight bookings and accommodation to guided tours .</p>
|
||||||
|
<blockquote class="mt-30 mb-35" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<i class="flaticon-quote"></i>
|
||||||
|
<div class="text">"In the world of tours and travel, every journey is an invitation to explore the unknown, connect with cultures, and create memories that last lifetime It's not just about the destination,extraordinary adventures."
|
||||||
|
</div>
|
||||||
|
<div class="blockquote-footer">
|
||||||
|
Kevin F. Glasscock
|
||||||
|
</div>
|
||||||
|
</blockquote>
|
||||||
|
<ul class="list-style-two mb-45" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<li>Understand the demand in your area, competition, and potential customers.</li>
|
||||||
|
<li>Register your business, obtain necessary licenses, and ensure compliance with local regulations.</li>
|
||||||
|
<li>Build relationships with hotels, airlines, transport companies, and other service providers.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="mb-45">
|
||||||
|
|
||||||
|
<div class="tag-share mb-50">
|
||||||
|
<div class="item" data-aos="fade-left" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<h6>Tags </h6>
|
||||||
|
<div class="tag-coulds">
|
||||||
|
<a href="blog.html">Travel</a>
|
||||||
|
<a href="blog.html">Hotel</a>
|
||||||
|
<a href="blog.html">Tour</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<h6>Share </h6>
|
||||||
|
<div class="social-style-one">
|
||||||
|
<a href="#"><i class="fab fa-facebook-f"></i></a>
|
||||||
|
<a href="#"><i class="fab fa-twitter"></i></a>
|
||||||
|
<a href="#"><i class="fab fa-linkedin-in"></i></a>
|
||||||
|
<a href="#"><i class="fab fa-instagram"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-comment bgc-lighter" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="comment-body">
|
||||||
|
<div class="author-thumb">
|
||||||
|
<img src="assets/images/blog/admin-comment.jpg" alt="Author">
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h4>Richard M. Fudge</h4>
|
||||||
|
<p>The world is a book, and those who do not travel read only one page. Every journey we undertake is a chapter filled with lessons, experiences, and stories.</p>
|
||||||
|
<div class="social-icons">
|
||||||
|
<a href="contact"><i class="fab fa-facebook-f"></i></a>
|
||||||
|
<a href="contact"><i class="fab fa-twitter"></i></a>
|
||||||
|
<a href="contact"><i class="fab fa-linkedin-in"></i></a>
|
||||||
|
<a href="contact"><i class="fab fa-instagram"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="next-prev-blog pt-70 pb-15">
|
||||||
|
<div class="item" data-aos="fade-left" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="image">
|
||||||
|
<img src="assets/images/blog/prev-post.jpg" alt="News">
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h6><a href="blog-details.html">Unique Destinations an tolded Stories ways</a></h6>
|
||||||
|
<span class="date"><i class="far fa-calendar-alt"></i> 25 Feb 2024</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="image">
|
||||||
|
<img src="assets/images/blog/next-post.jpg" alt="News">
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h6><a href="blog-details.html">Immersive Experiences from Around Globe</a></h6>
|
||||||
|
<span class="date"><i class="far fa-calendar-alt"></i> 25 Feb 2024</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="comment-form" class="comment-form bgc-lighter z-1 rel mt-25" name="review-form" action="#" method="post" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<h5>Leave A Comments</h5>
|
||||||
|
<p>Your email address will not be published. Required fields are marked *</p>
|
||||||
|
<div class="row gap-20 mt-30">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" id="full-name" name="full-name" class="form-control" placeholder="Name" value="" required="">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="email" id="email-address" name="email" class="form-control" placeholder="Email" value="" required="">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<textarea name="message" id="message" class="form-control" rows="5" placeholder="Message" required=""></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="form-group mb-0">
|
||||||
|
<ul class="radio-filter mb-25">
|
||||||
|
<li>
|
||||||
|
<input class="form-check-input" type="radio" name="terms-condition" id="terms-condition">
|
||||||
|
<label for="terms-condition">Save my name, email, and website in this browser for the next time I comment.</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<button type="submit" class="theme-btn style-two">
|
||||||
|
<span data-hover="Send Comments">Send Comments</span>
|
||||||
|
<i class="fal fa-arrow-right"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- <div class="col-lg-4 col-md-8 col-sm-10 rmt-75">
|
||||||
|
<div class="blog-sidebar">
|
||||||
|
|
||||||
|
<div class="widget widget-search" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<form action="#" class="default-search-form">
|
||||||
|
<input type="text" placeholder="Search" required="">
|
||||||
|
<button type="submit" class="searchbutton far fa-search"></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="widget widget-category" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<h5 class="widget-title">Category</h5>
|
||||||
|
<ul class="list-style-three">
|
||||||
|
<li><a href="blog.html">Adventure</a></li>
|
||||||
|
<li><a href="blog.html">Hiking & Trekking</a></li>
|
||||||
|
<li><a href="blog.html">Cycling Tours</a></li>
|
||||||
|
<li><a href="blog.html">Family Tours</a></li>
|
||||||
|
<li><a href="blog.html">Mountain Hiking</a></li>
|
||||||
|
<li><a href="blog.html">Rafting Excursion</a></li>
|
||||||
|
<li><a href="blog.html">Coastal Paragliding</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="widget widget-news" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<h5 class="widget-title">Recent News</h5>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<div class="image">
|
||||||
|
<img src="assets/images/widgets/news1.jpg" alt="News">
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h6><a href="blog-details.html">Unique Destinations an tolded Stories ways</a></h6>
|
||||||
|
<span class="date"><i class="far fa-calendar-alt"></i> 25 Feb 2024</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="image">
|
||||||
|
<img src="assets/images/widgets/news2.jpg" alt="News">
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h6><a href="blog-details.html">Immersive Experiences from Around Globe</a></h6>
|
||||||
|
<span class="date"><i class="far fa-calendar-alt"></i> 25 Feb 2024</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="image">
|
||||||
|
<img src="assets/images/widgets/news3.jpg" alt="News">
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h6><a href="blog-details.html">Journey to Inspire Your Next Adventure</a></h6>
|
||||||
|
<span class="date"><i class="far fa-calendar-alt"></i> 25 Feb 2024</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="widget widget-gallery" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<h5 class="widget-title">Gallery</h5>
|
||||||
|
<div class="gallery">
|
||||||
|
<a href="assets/images/widgets/gallery1.jpg">
|
||||||
|
<img src="assets/images/widgets/gallery1.jpg" alt="Gallery">
|
||||||
|
</a>
|
||||||
|
<a href="assets/images/widgets/gallery2.jpg">
|
||||||
|
<img src="assets/images/widgets/gallery2.jpg" alt="Gallery">
|
||||||
|
</a>
|
||||||
|
<a href="assets/images/widgets/gallery3.jpg">
|
||||||
|
<img src="assets/images/widgets/gallery3.jpg" alt="Gallery">
|
||||||
|
</a>
|
||||||
|
<a href="assets/images/widgets/gallery4.jpg">
|
||||||
|
<img src="assets/images/widgets/gallery4.jpg" alt="Gallery">
|
||||||
|
</a>
|
||||||
|
<a href="assets/images/widgets/gallery5.jpg">
|
||||||
|
<img src="assets/images/widgets/gallery5.jpg" alt="Gallery">
|
||||||
|
</a>
|
||||||
|
<a href="assets/images/widgets/gallery6.jpg">
|
||||||
|
<img src="assets/images/widgets/gallery6.jpg" alt="Gallery">
|
||||||
|
</a>
|
||||||
|
<a href="assets/images/widgets/gallery7.jpg">
|
||||||
|
<img src="assets/images/widgets/gallery7.jpg" alt="Gallery">
|
||||||
|
</a>
|
||||||
|
<a href="assets/images/widgets/gallery8.jpg">
|
||||||
|
<img src="assets/images/widgets/gallery8.jpg" alt="Gallery">
|
||||||
|
</a>
|
||||||
|
<a href="assets/images/widgets/gallery9.jpg">
|
||||||
|
<img src="assets/images/widgets/gallery9.jpg" alt="Gallery">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="widget widget-cta" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="content text-white">
|
||||||
|
<span class="h6">Explore The World</span>
|
||||||
|
<h3>Best Tourist Place</h3>
|
||||||
|
<a href="tour-grid.html" class="theme-btn style-two bgc-secondary">
|
||||||
|
<span data-hover="Explore Now">Explore Now</span>
|
||||||
|
<i class="fal fa-arrow-right"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="image">
|
||||||
|
<img src="assets/images/widgets/cta-widget.png" alt="CTA">
|
||||||
|
</div>
|
||||||
|
<div class="cta-shape"><img src="assets/images/widgets/cta-shape.png" alt="Shape"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Blog Detaisl Area end -->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- footer area start -->
|
||||||
|
<footer class="main-footer footer-two bgc-black rel z-15">
|
||||||
|
<div class="container">
|
||||||
|
<div class="footer-instagram pt-100">
|
||||||
|
<div class="row row-cols-xxl-6 row-cols-xl-5 row-cols-lg-4 row-cols-md-3 row-cols-2">
|
||||||
|
<div class="col" data-aos="zoom-in-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<a class="instagram-item" href="assets/images/instagram/instagram1.jpg">
|
||||||
|
<img src="assets/images/instagram/instagram1.jpg" alt="Instagram">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col" data-aos="zoom-in-down" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<a class="instagram-item" href="assets/images/instagram/instagram2.jpg">
|
||||||
|
<img src="assets/images/instagram/instagram2.jpg" alt="Instagram">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col" data-aos="zoom-in-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<a class="instagram-item" href="assets/images/instagram/instagram3.jpg">
|
||||||
|
<img src="assets/images/instagram/instagram3.jpg" alt="Instagram">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col" data-aos="zoom-in-down" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<a class="instagram-item" href="assets/images/instagram/instagram4.jpg">
|
||||||
|
<img src="assets/images/instagram/instagram4.jpg" alt="Instagram">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col" data-aos="zoom-in-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<a class="instagram-item" href="assets/images/instagram/instagram5.jpg">
|
||||||
|
<img src="assets/images/instagram/instagram5.jpg" alt="Instagram">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col" data-aos="zoom-in-down" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<a class="instagram-item" href="assets/images/instagram/instagram6.jpg">
|
||||||
|
<img src="assets/images/instagram/instagram6.jpg" alt="Instagram">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="widget-area bgp-bottom pt-70 pb-130 rpb-50" style="background-image: url(assets/images/backgrounds/footer-two.png);">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row row-cols-xxl-5 row-cols-xl-4 row-cols-md-3 row-cols-2">
|
||||||
|
<div class="col col-small" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="footer-widget footer-text">
|
||||||
|
<div class="footer-logo mb-40">
|
||||||
|
<a href="index.html"><img src="assets/images/logos/logo.png" alt="Logo"></a>
|
||||||
|
</div>
|
||||||
|
<div class="footer-map">
|
||||||
|
<iframe src="https://www.google.com/maps/embed?pb=!1m10!1m8!1m3!1d96777.16150026117!2d-74.00840582560909!3d40.71171357405996!3m2!1i1024!2i768!4f13.1!5e0!3m2!1sen!2sbd!4v1706508986625!5m2!1sen!2sbd" style="border:0; width: 100%;" allowfullscreen="" loading="lazy" referrerpolicy="no-referrer-when-downgrade"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col col-small" data-aos="fade-up" data-aos-delay="50" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="footer-widget footer-links ms-sm-5">
|
||||||
|
<div class="footer-title">
|
||||||
|
<h5>Services</h5>
|
||||||
|
</div>
|
||||||
|
<ul class="list-style-three">
|
||||||
|
<li><a href="destination-details.html">Best Tour Guide</a></li>
|
||||||
|
<li><a href="destination-details.html">Tour Booking</a></li>
|
||||||
|
<li><a href="destination-details.html">Hotel Booking</a></li>
|
||||||
|
<li><a href="destination-details.html">Ticket Booking</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col col-small" data-aos="fade-up" data-aos-delay="100" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="footer-widget footer-links ms-md-4">
|
||||||
|
<div class="footer-title">
|
||||||
|
<h5>Company</h5>
|
||||||
|
</div>
|
||||||
|
<ul class="list-style-three">
|
||||||
|
<li><a href="about.html">About Company</a></li>
|
||||||
|
<li><a href="blog.html">Community Blog</a></li>
|
||||||
|
<li><a href="contact">Jobs and Careers</a></li>
|
||||||
|
<li><a href="blog.html">latest News Blog</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col col-small" data-aos="fade-up" data-aos-delay="150" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="footer-widget footer-links ms-lg-4">
|
||||||
|
<div class="footer-title">
|
||||||
|
<h5>Destinations</h5>
|
||||||
|
</div>
|
||||||
|
<ul class="list-style-three">
|
||||||
|
<li><a href="destination-details.html">African Safaris</a></li>
|
||||||
|
<li><a href="destination-details.html">Alaska & Canada</a></li>
|
||||||
|
<li><a href="destination-details.html">South America</a></li>
|
||||||
|
<li><a href="destination-details.html">Middle East</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-6 col-10 col-small" data-aos="fade-up" data-aos-delay="200" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="footer-widget footer-contact">
|
||||||
|
<div class="footer-title">
|
||||||
|
<h5>Get In Touch</h5>
|
||||||
|
</div>
|
||||||
|
<ul class="list-style-one">
|
||||||
|
<li><i class="fal fa-map-marked-alt"></i> 578 Level, D-block 45 Street Melbourne, Australia</li>
|
||||||
|
<li><i class="fal fa-envelope"></i> <a href="mailto:supportrevelo@gmail.com">supportrevelo @gmail.com</a></li>
|
||||||
|
<li><i class="fal fa-phone-volume"></i> <a href="callto:+88012334588">+880 (123) 345 88</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer-bottom pt-20 pb-5">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-5">
|
||||||
|
<div class="copyright-text text-center text-lg-start">
|
||||||
|
<p>@Copy 2024 <a href="index.html">Ravelo</a>, All rights reserved</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-7 text-center text-lg-end">
|
||||||
|
<ul class="footer-bottom-nav">
|
||||||
|
<li><a href="about.html">Terms</a></li>
|
||||||
|
<li><a href="about.html">Privacy Policy</a></li>
|
||||||
|
<li><a href="about.html">Legal notice</a></li>
|
||||||
|
<li><a href="about.html">Accessibility</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Scroll Top Button -->
|
||||||
|
<button class="scroll-top scroll-to-target" data-target="html"><img src="assets/images/icons/scroll-up.png" alt="Scroll Up"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<!-- footer area end -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!--End pagewrapper-->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Jquery -->
|
||||||
|
<script src="assets/js/jquery-3.6.0.min.js"></script>
|
||||||
|
<!-- Bootstrap -->
|
||||||
|
<script src="assets/js/bootstrap.min.js"></script>
|
||||||
|
<!-- Appear Js -->
|
||||||
|
<script src="assets/js/appear.min.js"></script>
|
||||||
|
<!-- Slick -->
|
||||||
|
<script src="assets/js/slick.min.js"></script>
|
||||||
|
<!-- Magnific Popup -->
|
||||||
|
<script src="assets/js/jquery.magnific-popup.min.js"></script>
|
||||||
|
<!-- Nice Select -->
|
||||||
|
<script src="assets/js/jquery.nice-select.min.js"></script>
|
||||||
|
<!-- Image Loader -->
|
||||||
|
<script src="assets/js/imagesloaded.pkgd.min.js"></script>
|
||||||
|
<!-- Jquery UI -->
|
||||||
|
<script src="assets/js/jquery-ui.min.js"></script>
|
||||||
|
<!-- Isotope -->
|
||||||
|
<script src="assets/js/isotope.pkgd.min.js"></script>
|
||||||
|
<!-- AOS Animation -->
|
||||||
|
<script src="assets/js/aos.js"></script>
|
||||||
|
<!-- Custom script -->
|
||||||
|
<script src="assets/js/script.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
279
src/pages/blog/blog_edit.php
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
<?php
|
||||||
|
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||||
|
require_once($rootPath . "/src/config/env.php");
|
||||||
|
require_once($rootPath . "/src/config/connection.php");
|
||||||
|
require_once($rootPath . "/src/config/functions.php");
|
||||||
|
require_once($rootPath . "/header.php");
|
||||||
|
|
||||||
|
// Ensure the user is logged in
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
die("User not logged in.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageTitle = 'Edit Blog Post';
|
||||||
|
$breadcrumbs = [['Home' => 'index'], ['My Blog Posts' => 'user_blogs']];
|
||||||
|
require_once($rootPath . '/components/banner.php');
|
||||||
|
|
||||||
|
$token = $_GET['token'];
|
||||||
|
// Sanitize the trip_id to prevent SQL injection
|
||||||
|
$blog_id = intval(decryptData($token, $salt)); // Ensures $trip_id is treated as an integer
|
||||||
|
|
||||||
|
$user_id = $_SESSION['user_id'];
|
||||||
|
$role = getUserRole();
|
||||||
|
|
||||||
|
// Fetch article info
|
||||||
|
$stmt = $conn->prepare("SELECT * FROM blogs WHERE blog_id = ?");
|
||||||
|
$stmt->bind_param("i", $blog_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
if ($result->num_rows === 0) {
|
||||||
|
die("Blog post not found.");
|
||||||
|
}
|
||||||
|
$article = $result->fetch_assoc();
|
||||||
|
$stmt->close();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<script src="https://cdn.tiny.cloud/1/o6xuedbd9z22xk0p5zszinevn4bdbljxnfwn0tjjvv6r37pb/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
|
||||||
|
<script>
|
||||||
|
tinymce.init({
|
||||||
|
selector: '#content',
|
||||||
|
plugins: 'image code link',
|
||||||
|
toolbar: 'undo redo | blocks | bold italic | alignleft aligncenter alignright | code | image | link',
|
||||||
|
images_upload_url: 'upload_blog_image?blog_id=<?= $blog_id ?>',
|
||||||
|
image_class_list: [
|
||||||
|
{ title: 'None', value: '' },
|
||||||
|
{ title: 'Left Align', value: 'img-left' },
|
||||||
|
{ title: 'Right Align', value: 'img-right' },
|
||||||
|
{ title: 'Rounded', value: 'img-rounded' }
|
||||||
|
],
|
||||||
|
automatic_uploads: true,
|
||||||
|
images_upload_credentials: true, // include cookies if needed
|
||||||
|
content_style: "body { font-family:Helvetica,Arial,sans-serif; font-size:14px }",
|
||||||
|
|
||||||
|
setup: function (editor) {
|
||||||
|
editor.on('init', function () {
|
||||||
|
setTimeout(() => {
|
||||||
|
editor.setContent(`<?= str_replace("`", "\`", addslashes($article['content'])) ?>`);
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="account-settings-area py-70 rel z-1">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<div class="comment-form bgc-lighter z-1 rel mb-55">
|
||||||
|
<form action="submit_blog.php" method="POST" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="article_id" value="<?= htmlspecialchars($blog_id) ?>">
|
||||||
|
<div class="section-title py-20">
|
||||||
|
<h2>Edit Blog</h2>
|
||||||
|
<div id="autosave-status" style="font-style: italic; font-size: 0.9em;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-35">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="title">Blog Title</label>
|
||||||
|
<input type="text" id="title" class="form-control" name="title" placeholder="Title" required value="<?= htmlspecialchars($article['title']) ?>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="subtitle">Description</label>
|
||||||
|
<input type="text" id="subtitle" class="form-control" name="subtitle" placeholder="Description" required value="<?= htmlspecialchars($article['description']) ?>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cover_image">Cover Image</label>
|
||||||
|
<input type="file" class="form-control" name="cover_image" id="cover_image" accept="image/*">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 mb-10">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="category">Blog Category</label>
|
||||||
|
<select name="category" class="form-control" id="category" required>
|
||||||
|
<option value="Trip Report" <?= $article['category'] == 'Trip Report' ? 'selected' : '' ?>>Trip Report</option>
|
||||||
|
<option value="Gear Review" <?= $article['category'] == 'Gear Review' ? 'selected' : '' ?>>Gear Review</option>
|
||||||
|
<option value="Talking Dirty" <?= $article['category'] == 'Talking Dirty' ? 'selected' : '' ?>>Talking Dirty</option>
|
||||||
|
<option value="Report" <?= $article['category'] == 'Report' ? 'selected' : '' ?>>Report</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 mb-10">
|
||||||
|
<div class="form-group">
|
||||||
|
<?php if ($role === 'admin' || $role === 'superadmin'): ?>
|
||||||
|
<label for="author">Author:</label>
|
||||||
|
<select class="form-control" name="author" id="author">
|
||||||
|
<?php
|
||||||
|
$user_query = $conn->query("SELECT user_id, CONCAT(first_name, ' ', last_name) AS name FROM users ORDER BY first_name ASC");
|
||||||
|
while ($user = $user_query->fetch_assoc()):
|
||||||
|
?>
|
||||||
|
<option value="<?= $user['user_id'] ?>" <?= $user['user_id'] == $article['author'] ? 'selected' : '' ?>>
|
||||||
|
<?= htmlspecialchars($user['name']) ?>
|
||||||
|
</option>
|
||||||
|
<?php endwhile; ?>
|
||||||
|
</select>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<textarea id="content" name="content"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="button" class="theme-btn style-three" style="width:100%;" id="manualSaveBtn">Save Draft</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<a href="blog_read.php?token=<?php echo encryptData($blog_id, $salt); ?>" class="theme-btn style-three" style="width:100%;">Preview</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12">
|
||||||
|
<?php
|
||||||
|
if ($article['status'] == 'draft'){
|
||||||
|
echo '<div class="form-group">
|
||||||
|
<button type="button" class="theme-btn style-two" style="width:100%;" id="manualPostBtn">Publish</button>
|
||||||
|
|
||||||
|
</div> ';
|
||||||
|
} else {
|
||||||
|
echo '<div class="form-group">
|
||||||
|
<button type="button" class="theme-btn style-two" style="width:100%;" id="manualDraftBtn">Un-Publish</button>
|
||||||
|
|
||||||
|
</div> ';
|
||||||
|
}?>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
||||||
|
<script>
|
||||||
|
function autosavePost() {
|
||||||
|
const title = document.querySelector('[name="title"]').value;
|
||||||
|
const content = tinymce.get("content").getContent();
|
||||||
|
const subtitle = document.querySelector('[name="subtitle"]').value;
|
||||||
|
const category = document.querySelector('[name="category"]').value;
|
||||||
|
const author = document.querySelector('[name="author"]').value;
|
||||||
|
const articleId = document.querySelector('[name="article_id"]').value;
|
||||||
|
const coverImageInput = document.querySelector('[name="cover_image"]');
|
||||||
|
|
||||||
|
console.log("Saving: ", { title, subtitle, content, category, articleId, author });
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("id", articleId);
|
||||||
|
formData.append("title", title);
|
||||||
|
formData.append("content", content);
|
||||||
|
formData.append("subtitle", subtitle);
|
||||||
|
formData.append("category", category);
|
||||||
|
formData.append("author", author);
|
||||||
|
|
||||||
|
// Only append image if a new file is selected
|
||||||
|
if (coverImageInput.files.length > 0) {
|
||||||
|
formData.append("cover_image", coverImageInput.files[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch("autosave", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData
|
||||||
|
}).then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
document.getElementById("autosave-status").innerText = "Draft autosaved at " + new Date().toLocaleTimeString();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
document.getElementById("autosave-status").innerText = "Autosave failed";
|
||||||
|
console.error("Autosave failed", response.statusText);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.error("Autosave error:", err);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger autosave every 15s
|
||||||
|
setInterval(autosavePost, 15000);
|
||||||
|
|
||||||
|
// Manual autosave button
|
||||||
|
const manualSaveBtn = document.getElementById("manualSaveBtn");
|
||||||
|
if (manualSaveBtn) {
|
||||||
|
manualSaveBtn.addEventListener("click", autosavePost);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual publish button
|
||||||
|
const manualPostBtn = document.getElementById("manualPostBtn");
|
||||||
|
if (manualPostBtn) {
|
||||||
|
manualPostBtn.addEventListener("click", function () {
|
||||||
|
autosavePost().then(success => {
|
||||||
|
if (!success) return;
|
||||||
|
|
||||||
|
const articleId = document.querySelector('[name="article_id"]').value;
|
||||||
|
const publishData = new FormData();
|
||||||
|
publishData.append("id", articleId);
|
||||||
|
|
||||||
|
fetch("publish_blog", {
|
||||||
|
method: "POST",
|
||||||
|
body: publishData
|
||||||
|
}).then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
alert("Post published successfully!");
|
||||||
|
// Optional: redirect to the live post
|
||||||
|
window.location.href = "blog_read.php?token=<?php echo encryptData($blog_id, $salt);?>";
|
||||||
|
} else {
|
||||||
|
alert("Publish failed.");
|
||||||
|
console.error("Publish error:", response.statusText);
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.error("Publish error:", err);
|
||||||
|
alert("Publish failed due to network error.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual unpublish button
|
||||||
|
const manualDraftBtn = document.getElementById("manualDraftBtn");
|
||||||
|
if (manualDraftBtn) {
|
||||||
|
manualDraftBtn.addEventListener("click", function () {
|
||||||
|
autosavePost().then(success => {
|
||||||
|
if (!success) return;
|
||||||
|
|
||||||
|
const articleId = document.querySelector('[name="article_id"]').value;
|
||||||
|
const publishData = new FormData();
|
||||||
|
publishData.append("id", articleId);
|
||||||
|
|
||||||
|
fetch("blog_unpublish", {
|
||||||
|
method: "POST",
|
||||||
|
body: publishData
|
||||||
|
}).then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
alert("Post unpublished successfully!");
|
||||||
|
// Optional: redirect to the live post
|
||||||
|
window.location.href = "blog_read.php?token=<?php echo encryptData($blog_id, $salt);?>";
|
||||||
|
} else {
|
||||||
|
alert("unPublish failed.");
|
||||||
|
console.error("Publish error:", response.statusText);
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.error("Publish error:", err);
|
||||||
|
alert("Publish failed due to network error.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php include_once($rootPath . '/components/insta_footer.php'); ?>
|
||||||
181
src/pages/blog/blog_read.php
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
<?php
|
||||||
|
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||||
|
require_once($rootPath . "/src/config/env.php");
|
||||||
|
require_once($rootPath . "/src/config/connection.php");
|
||||||
|
require_once($rootPath . "/src/config/functions.php");
|
||||||
|
require_once($rootPath . "/header.php");
|
||||||
|
|
||||||
|
$token = $_GET['token'];
|
||||||
|
// Sanitize the trip_id to prevent SQL injection
|
||||||
|
$blog_id = intval(decryptData($token, $salt)); // Ensures $trip_id is treated as an integer
|
||||||
|
|
||||||
|
$pageTitle = 'Blog Post';
|
||||||
|
$breadcrumbs = [['Home' => 'index'], ['Blog' => 'blog']];
|
||||||
|
require_once($rootPath . '/components/banner.php');
|
||||||
|
|
||||||
|
$page_id = 'blog_'.$blog_id;
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("
|
||||||
|
SELECT a.blog_id, a.title, a.category, a.description, a.content, a.date, a.author,
|
||||||
|
u.first_name, u.last_name, u.user_id
|
||||||
|
FROM blogs a
|
||||||
|
JOIN users u ON a.author = u.user_id
|
||||||
|
WHERE a.blog_id = ?
|
||||||
|
");
|
||||||
|
$stmt->bind_param("i", $blog_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
|
||||||
|
if ($result->num_rows === 0) {
|
||||||
|
die("Article not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = $result->fetch_assoc();
|
||||||
|
$author = htmlspecialchars($row['first_name'] . ' ' . $row['last_name']);
|
||||||
|
$author_id = $row['author'];
|
||||||
|
$is_author = (isset($_SESSION['user_id']) && $_SESSION['user_id'] == $author_id);
|
||||||
|
?>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.image {
|
||||||
|
width: 400px;
|
||||||
|
/* Set your desired width */
|
||||||
|
height: 350px;
|
||||||
|
/* Set your desired height */
|
||||||
|
overflow: hidden;
|
||||||
|
/* Hide any overflow */
|
||||||
|
display: block;
|
||||||
|
/* Ensure proper block behavior */
|
||||||
|
}
|
||||||
|
|
||||||
|
.image img {
|
||||||
|
width: 100%;
|
||||||
|
/* Image scales to fill the container */
|
||||||
|
height: 100%;
|
||||||
|
/* Image scales to fill the container */
|
||||||
|
object-fit: cover;
|
||||||
|
/* Fills the container while maintaining aspect ratio */
|
||||||
|
object-position: top;
|
||||||
|
/* Aligns the top of the image with the top of the container */
|
||||||
|
display: block;
|
||||||
|
/* Prevents inline whitespace issues */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
/* font-family: Arial, sans-serif; */
|
||||||
|
line-height: 1.6;
|
||||||
|
/* max-width: 800px; */
|
||||||
|
margin: auto;
|
||||||
|
/* padding: 20px; */
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2 {
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin-bottom: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-left,
|
||||||
|
.img-right {
|
||||||
|
max-width: 30%;
|
||||||
|
margin: 20px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-left {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-right {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearfix {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// Dynamically set page title to blog title
|
||||||
|
if (isset($row) && !empty($row['title'])) {
|
||||||
|
$pageTitle = htmlspecialchars($row['title']);
|
||||||
|
} else {
|
||||||
|
$pageTitle = 'Blog Post';
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- Blog Detaisl Area start -->
|
||||||
|
<section class="blog-detaisl-page py-100 rel z-1">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="blog-details-content" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<a href="blog.html" class="category"><?= htmlspecialchars($row['category']) ?></a>
|
||||||
|
<ul class="blog-meta mb-30">
|
||||||
|
<li><img src="assets/images/pp/default.png" alt="Admin"> <a href="#"><?= $author?></a></li>
|
||||||
|
<li><i class="far fa-calendar-alt"></i> <a href="#"><?= htmlspecialchars($row['date']) ?></a></li>
|
||||||
|
<li><i class="far fa-comments"></i> <a href="#">Comments (<?= getCommentCount($page_id);?>)</a></li>
|
||||||
|
<?php if ($is_author): ?>
|
||||||
|
<li><a href="blog_edit.php?token=<?php echo encryptData($blog_id, $salt); ?>">Edit Post</a></li>
|
||||||
|
<?php endif; ?>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<?= $row['content'] ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4 col-md-8 col-sm-10 rmt-75">
|
||||||
|
<div class="blog-sidebar">
|
||||||
|
<div class="widget widget-gallery" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<h5 class="widget-title">Gallery</h5>
|
||||||
|
<div class="gallery">
|
||||||
|
<?php
|
||||||
|
$folder = $rootPath . '/uploads/blogs/' . $blog_id . '/';
|
||||||
|
$files = glob($folder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
||||||
|
|
||||||
|
if ($files && count($files) > 0) {
|
||||||
|
shuffle($files); // Randomize the order
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$relativePath = '/uploads/blogs/' . $blog_id . '/' . basename($file);
|
||||||
|
echo '<a href="' . $relativePath . '" style="width: 110px; height: 110px; overflow: hidden; display: inline-block; margin: 2px;">';
|
||||||
|
echo '<img src="' . $relativePath . '" alt="Gallery" style="width: 100%; height: 100%; object-fit: cover; display: block;">';
|
||||||
|
echo '</a>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo '<p style="font-size: 0.9em; color: #999;">No images available</p>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<hr class="mb-45">
|
||||||
|
<div class="tag-share">
|
||||||
|
<div class="item" data-aos="fade-left" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<h6>Tags </h6>
|
||||||
|
<div class="tag-coulds">
|
||||||
|
<a href="blog.php"><?= htmlspecialchars($row['category']) ?></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php include_once($rootPath . '/src/pages/other/comment_box.php'); ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php include_once($rootPath . '/components/insta_footer.php'); ?>
|
||||||
146
src/pages/blog/user_blogs.php
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
<?php
|
||||||
|
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||||
|
require_once($rootPath . "/src/config/env.php");
|
||||||
|
require_once($rootPath . "/src/config/connection.php");
|
||||||
|
require_once($rootPath . "/src/config/functions.php");
|
||||||
|
require_once($rootPath . "/header.php");
|
||||||
|
|
||||||
|
checkUserSession();
|
||||||
|
|
||||||
|
$pageTitle = 'My Blog Posts';
|
||||||
|
$breadcrumbs = [['Home' => 'index'], ['Blog' => 'blog']];
|
||||||
|
require_once($rootPath . '/components/banner.php');
|
||||||
|
|
||||||
|
$result = $conn->prepare("SELECT blog_id, title, description, status, date, image FROM blogs WHERE author = ? AND status != 'deleted' ORDER BY date DESC");
|
||||||
|
|
||||||
|
$result->bind_param("i", $user_id);
|
||||||
|
$result->execute();
|
||||||
|
$posts = $result->get_result();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.image {
|
||||||
|
width: 400px;
|
||||||
|
/* Set your desired width */
|
||||||
|
height: 350px;
|
||||||
|
/* Set your desired height */
|
||||||
|
overflow: hidden;
|
||||||
|
/* Hide any overflow */
|
||||||
|
display: block;
|
||||||
|
/* Ensure proper block behavior */
|
||||||
|
}
|
||||||
|
|
||||||
|
.image img {
|
||||||
|
width: 100%;
|
||||||
|
/* Image scales to fill the container */
|
||||||
|
height: 100%;
|
||||||
|
/* Image scales to fill the container */
|
||||||
|
object-fit: cover;
|
||||||
|
/* Fills the container while maintaining aspect ratio */
|
||||||
|
object-position: top;
|
||||||
|
/* Aligns the top of the image with the top of the container */
|
||||||
|
display: block;
|
||||||
|
/* Prevents inline whitespace issues */
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$bannerFolder = 'assets/images/banners/';
|
||||||
|
$bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
||||||
|
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- Blog List Area start -->
|
||||||
|
<section class="blog-list-page py-100 rel z-1">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
|
||||||
|
<h2>My Posts</h2>
|
||||||
|
<?php if (isset($_SESSION['message'])): ?>
|
||||||
|
<div class="alert alert-warning message-box">
|
||||||
|
<?php echo $_SESSION['message']; ?>
|
||||||
|
<span class="close-btn" onclick="this.parentElement.style.display='none'">×</span>
|
||||||
|
</div>
|
||||||
|
<?php unset($_SESSION['message']); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<a href="blog_create.php">+ New Post</a>
|
||||||
|
|
||||||
|
<?php while ($post = $posts->fetch_assoc()):
|
||||||
|
// Determine cover image - use provided image or fallback placeholder
|
||||||
|
$coverImage = $post["image"] ? $post["image"] : 'assets/images/placeholder.jpg';
|
||||||
|
// Output the HTML structure with dynamic data
|
||||||
|
echo '
|
||||||
|
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="image" style="width:200px;height:200px;"><img src="' . htmlspecialchars($coverImage) . '" alt="' . htmlspecialchars($post["title"]) . '"></div>
|
||||||
|
<div class="content" style="width:100%;">
|
||||||
|
|
||||||
|
<div class="destination-header">
|
||||||
|
<span class="badge bg-dark"> ' . strtoupper($post["status"]) . '</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5>' . $post["title"] . '</a></h5>
|
||||||
|
<p>' . $post["description"] . '</p>
|
||||||
|
<div class="destination-footer">
|
||||||
|
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;">
|
||||||
|
<a href="blog_edit.php?token='.encryptData($post["blog_id"], $salt).'" data-bs-toggle="tooltip" data-bs-placement="top" title="Edit"><span class="material-icons">edit</span></a>
|
||||||
|
<a href="blog_read.php?token='.encryptData($post["blog_id"], $salt).'" data-bs-toggle="tooltip" data-bs-placement="top" title="Preview"><span class="material-icons">visibility</span></a>
|
||||||
|
<button type="button" class="publish-btn" data-blog-id="' . $post["blog_id"] . '" data-status="' . $post["status"] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="' . ($post["status"] == "published" ? "Unpublish" : "Publish") . '" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">' . ($post["status"] == "published" ? "cloud_off" : "cloud_upload") . '</span></button>
|
||||||
|
<a href="blog_delete.php?token='.encryptData($post["blog_id"], $salt).'" data-bs-toggle="tooltip" data-bs-placement="top" title="Delete"><span class="material-icons">delete</span></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>';
|
||||||
|
endwhile; ?>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Blog List Area end -->
|
||||||
|
<script>
|
||||||
|
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||||
|
tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el));
|
||||||
|
|
||||||
|
// Handle publish/unpublish button clicks
|
||||||
|
document.querySelectorAll('.publish-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const blogId = this.dataset.blogId;
|
||||||
|
const status = this.dataset.status;
|
||||||
|
const action = status === 'published' ? 'unpublish' : 'publish';
|
||||||
|
const endpoint = status === 'published' ? 'blog_unpublish' : 'publish_blog';
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('id', blogId);
|
||||||
|
|
||||||
|
fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
alert(action.charAt(0).toUpperCase() + action.slice(1) + ' successful!');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert(action + ' failed.');
|
||||||
|
console.error('Error:', response.statusText);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Error:', err);
|
||||||
|
alert(action + ' failed due to network error.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<?php include_once($rootPath . '/components/insta_footer.php'); ?>
|
||||||
@@ -46,19 +46,13 @@ $result = $stmt->get_result();
|
|||||||
<div class="">
|
<div class="">
|
||||||
<h6><?= getFullName($row['user_id']); ?></h6>
|
<h6><?= getFullName($row['user_id']); ?></h6>
|
||||||
<?php
|
<?php
|
||||||
if (getUserMemberStatus($row['user_id'])){
|
if (getUserMemberStatus($row['user_id'])) {
|
||||||
echo '<div class="badge badge-primary badge-pill">MEMBER</div>';
|
echo '<div class="badge badge-primary badge-pill">MEMBER</div>';
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<em><?= $row['created_at'] ?></em>
|
<em><?= $row['created_at'] ?></em>
|
||||||
<!-- <div class="ratting">
|
|
||||||
<i class="fas fa-star"></i>
|
|
||||||
<i class="fas fa-star"></i>
|
|
||||||
<i class="fas fa-star"></i>
|
|
||||||
<i class="fas fa-star"></i>
|
|
||||||
<i class="fas fa-star-half-alt"></i>
|
|
||||||
</div> -->
|
|
||||||
<p><?= nl2br(htmlspecialchars($row['comment'])) ?></p>
|
<p><?= nl2br(htmlspecialchars($row['comment'])) ?></p>
|
||||||
<!-- <a class="read-more" href="#">Reply <i class="far fa-angle-right"></i></a> -->
|
<!-- <a class="read-more" href="#">Reply <i class="far fa-angle-right"></i></a> -->
|
||||||
</div>
|
</div>
|
||||||
@@ -85,12 +79,12 @@ $result = $stmt->get_result();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.comment-box {
|
.comment-box {
|
||||||
border: 1px solid #ccc;
|
/* border: 1px solid #ccc; */
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
}
|
}
|
||||||
@@ -156,4 +150,4 @@ $result = $stmt->get_result();
|
|||||||
.badge-pill {
|
.badge-pill {
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
91
src/processors/blog/autosave.php
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||||
|
require_once($rootPath . "/src/config/env.php");
|
||||||
|
require_once($rootPath . "/src/config/connection.php");
|
||||||
|
require_once($rootPath . "/src/config/functions.php");
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo "Not authorized";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$article_id = (int)($_POST['id'] ?? 0);
|
||||||
|
$title = $_POST['title'] ?? '';
|
||||||
|
$content = $_POST['content'] ?? '';
|
||||||
|
$description = $_POST['subtitle'] ?? '';
|
||||||
|
$category = $_POST['category'] ?? '';
|
||||||
|
$user_id = $_SESSION['user_id'];
|
||||||
|
|
||||||
|
|
||||||
|
// Default to current user
|
||||||
|
$author_id = $_SESSION['user_id'];
|
||||||
|
|
||||||
|
// Allow override if admin
|
||||||
|
$role = getUserRole();
|
||||||
|
if (($role === 'admin' || $role === 'superadmin') && isset($_POST['author'])) {
|
||||||
|
$author_id = (int)$_POST['author'];
|
||||||
|
}
|
||||||
|
echo $author_id;
|
||||||
|
|
||||||
|
$cover_image_path = null;
|
||||||
|
|
||||||
|
// Only attempt upload if a file was submitted
|
||||||
|
if (!empty($_FILES['cover_image']['name'])) {
|
||||||
|
$uploadDir = $rootPath . "/uploads/blogs/" . $article_id . "/";
|
||||||
|
if (!is_dir($uploadDir)) {
|
||||||
|
mkdir($uploadDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file using existing function
|
||||||
|
$file_result = validateFileUpload($_FILES['cover_image'], 'profile_picture');
|
||||||
|
if ($file_result === false) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo "Invalid file upload";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use fixed filename "cover" to avoid creating multiple copies on autosave
|
||||||
|
$extension = $file_result['extension'];
|
||||||
|
$filename = "cover." . $extension;
|
||||||
|
|
||||||
|
// Delete old cover if it exists with different extension
|
||||||
|
array_map('unlink', glob($uploadDir . "cover.*"));
|
||||||
|
|
||||||
|
$targetPath = $uploadDir . $filename;
|
||||||
|
$cover_image_path = "/uploads/blogs/" . $article_id . "/" . $filename;
|
||||||
|
|
||||||
|
// Move the uploaded file
|
||||||
|
if (move_uploaded_file($_FILES['cover_image']['tmp_name'], $targetPath)) {
|
||||||
|
// File moved successfully, $cover_image_path is set
|
||||||
|
} else {
|
||||||
|
http_response_code(500);
|
||||||
|
echo "Failed to move uploaded file.";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare SQL with/without image update
|
||||||
|
if ($cover_image_path) {
|
||||||
|
$stmt = $conn->prepare("
|
||||||
|
UPDATE blogs
|
||||||
|
SET title = ?, content = ?, description = ?, category = ?, image = ?, author = ?
|
||||||
|
WHERE blog_id = ?
|
||||||
|
");
|
||||||
|
$stmt->bind_param("ssssssi", $title, $content, $description, $category, $cover_image_path, $author_id, $article_id);
|
||||||
|
} else {
|
||||||
|
$stmt = $conn->prepare("
|
||||||
|
UPDATE blogs
|
||||||
|
SET title = ?, content = ?, description = ?, category = ?, author = ?
|
||||||
|
WHERE blog_id = ?
|
||||||
|
");
|
||||||
|
$stmt->bind_param("ssssii", $title, $content, $description, $category, $author_id, $article_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo "Saved";
|
||||||
|
} else {
|
||||||
|
http_response_code(500);
|
||||||
|
echo "Database update failed: " . $stmt->error;
|
||||||
|
}
|
||||||
33
src/processors/blog/blog_create.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||||
|
require_once($rootPath . "/src/config/env.php");
|
||||||
|
require_once($rootPath . "/src/config/connection.php");
|
||||||
|
require_once($rootPath . "/src/config/functions.php");
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
die("Not logged in");
|
||||||
|
}
|
||||||
|
$user_id = $_SESSION['user_id'];
|
||||||
|
$role = getUserRole();
|
||||||
|
|
||||||
|
if(!getUserMemberStatus($user_id)){
|
||||||
|
if ($role === 'user'){
|
||||||
|
$_SESSION['message'] = "Blogs only available to active members. Please contact info@4wdcsa.co.za for more information.";
|
||||||
|
header("Location: user_blogs.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$date = date('Y-m-d');
|
||||||
|
$status = 'draft';
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("INSERT INTO blogs (author, title, category, description, content, date, status)
|
||||||
|
VALUES (?, '', '', '', '', ?, ?)");
|
||||||
|
$stmt->bind_param("iss", $user_id, $date, $status);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
$blog_id = $stmt->insert_id;
|
||||||
|
header("Location: blog_edit.php?token=" . encryptData($blog_id, $salt));
|
||||||
|
exit;
|
||||||
37
src/processors/blog/blog_delete.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||||
|
require_once($rootPath . "/src/config/env.php");
|
||||||
|
require_once($rootPath . "/src/config/connection.php");
|
||||||
|
require_once($rootPath . "/src/config/functions.php");
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
$_SESSION['message'] = "Not authorized.";
|
||||||
|
header("Location: user_blogs.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $_GET['token'];
|
||||||
|
// Sanitize the trip_id to prevent SQL injection
|
||||||
|
$article_id = intval(decryptData($token, $salt)); // Ensures $trip_id is treated as an integer
|
||||||
|
|
||||||
|
$user_id = $_SESSION['user_id'];
|
||||||
|
|
||||||
|
if ($article_id <= 0) {
|
||||||
|
$_SESSION['message'] = "Invalid blog ID.";
|
||||||
|
header("Location: user_blogs.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("UPDATE blogs SET status = 'deleted' WHERE blog_id = ? AND author = ?");
|
||||||
|
$stmt->bind_param("ii", $article_id, $user_id);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
$_SESSION['message'] = "Blog deleted!";
|
||||||
|
} else {
|
||||||
|
$_SESSION['message'] = "Failed to delete blog: " . $stmt->error;
|
||||||
|
}
|
||||||
|
|
||||||
|
header("Location: user_blogs.php");
|
||||||
|
exit;
|
||||||
|
?>
|
||||||
54
src/processors/blog/blog_unpublish.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||||
|
require_once($rootPath . "/src/config/env.php");
|
||||||
|
require_once($rootPath . "/src/config/connection.php");
|
||||||
|
require_once($rootPath . "/src/config/functions.php");
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo "Not authorized";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$article_id = (int)($_POST['id'] ?? 0);
|
||||||
|
$user_id = $_SESSION['user_id'];
|
||||||
|
$role = getUserRole();
|
||||||
|
|
||||||
|
if ($article_id <= 0) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo "Invalid blog ID";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permissions: user must be author or admin
|
||||||
|
$stmt = $conn->prepare("SELECT author FROM blogs WHERE blog_id = ?");
|
||||||
|
$stmt->bind_param("i", $article_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
$blog = $result->fetch_assoc();
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
if (!$blog) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo "Blog not found";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow if user is author or admin
|
||||||
|
if ($blog['author'] != $user_id && !in_array($role, ['admin', 'superadmin'])) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo "Not authorized to unpublish this blog";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("UPDATE blogs SET status = 'draft' WHERE blog_id = ?");
|
||||||
|
$stmt->bind_param("i", $article_id);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo "Unpublished";
|
||||||
|
} else {
|
||||||
|
http_response_code(500);
|
||||||
|
echo "Failed to unpublish: " . $stmt->error;
|
||||||
|
}
|
||||||
|
?>
|
||||||
54
src/processors/blog/publish_blog.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||||
|
require_once($rootPath . "/src/config/env.php");
|
||||||
|
require_once($rootPath . "/src/config/connection.php");
|
||||||
|
require_once($rootPath . "/src/config/functions.php");
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo "Not authorized";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$article_id = (int)($_POST['id'] ?? 0);
|
||||||
|
$user_id = $_SESSION['user_id'];
|
||||||
|
$role = getUserRole();
|
||||||
|
|
||||||
|
if ($article_id <= 0) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo "Invalid blog ID";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permissions: user must be author or admin
|
||||||
|
$stmt = $conn->prepare("SELECT author FROM blogs WHERE blog_id = ?");
|
||||||
|
$stmt->bind_param("i", $article_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
$blog = $result->fetch_assoc();
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
if (!$blog) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo "Blog not found";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow if user is author or admin
|
||||||
|
if ($blog['author'] != $user_id && !in_array($role, ['admin', 'superadmin'])) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo "Not authorized to publish this blog";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("UPDATE blogs SET status = 'published' WHERE blog_id = ?");
|
||||||
|
$stmt->bind_param("i", $article_id);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo "Published";
|
||||||
|
} else {
|
||||||
|
http_response_code(500);
|
||||||
|
echo "Failed to publish: " . $stmt->error;
|
||||||
|
}
|
||||||
|
?>
|
||||||
83
src/processors/blog/submit_blog.php
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||||
|
require_once($rootPath . "/src/config/env.php");
|
||||||
|
require_once($rootPath . "/src/config/connection.php");
|
||||||
|
require_once($rootPath . "/src/config/functions.php");
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
die("Login required");
|
||||||
|
}
|
||||||
|
|
||||||
|
$title = $_POST['title'];
|
||||||
|
$category = $_POST['category'];
|
||||||
|
$description = $_POST['description'];
|
||||||
|
$content = $_POST['content'];
|
||||||
|
$user_id = $_SESSION['user_id'];
|
||||||
|
$date = date('Y-m-d');
|
||||||
|
$article_id = $_POST['article_id'] ?? null;
|
||||||
|
$image = null;
|
||||||
|
|
||||||
|
// Handle cover image upload if provided
|
||||||
|
if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) {
|
||||||
|
// For new blogs, we'll use the blog_id after insert, for now use temp folder
|
||||||
|
// Update: For editing, use article_id; for new blogs, we'll need to handle this after insert
|
||||||
|
$folder_id = $article_id ?? 'temp_' . uniqid();
|
||||||
|
$upload_dir = $rootPath . '/uploads/blogs/' . $folder_id . '/';
|
||||||
|
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
if (!is_dir($upload_dir)) {
|
||||||
|
mkdir($upload_dir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and process the file
|
||||||
|
$file_result = validateFileUpload($_FILES['cover_image'], 'profile_picture');
|
||||||
|
if ($file_result !== false) {
|
||||||
|
// Use fixed filename "cover" to avoid duplicates
|
||||||
|
$extension = $file_result['extension'];
|
||||||
|
$filename = "cover." . $extension;
|
||||||
|
|
||||||
|
// Delete old cover if it exists with different extension
|
||||||
|
array_map('unlink', glob($upload_dir . "cover.*"));
|
||||||
|
|
||||||
|
$upload_path = $upload_dir . $filename;
|
||||||
|
|
||||||
|
if (move_uploaded_file($_FILES['cover_image']['tmp_name'], $upload_path)) {
|
||||||
|
// Store relative path for database
|
||||||
|
$image = '/uploads/blogs/' . $folder_id . '/' . $filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If updating an existing blog, get the existing image if no new one was uploaded
|
||||||
|
if ($article_id && !$image) {
|
||||||
|
$check_stmt = $conn->prepare("SELECT image FROM blogs WHERE blog_id = ?");
|
||||||
|
$check_stmt->bind_param("i", $article_id);
|
||||||
|
$check_stmt->execute();
|
||||||
|
$result = $check_stmt->get_result();
|
||||||
|
if ($result->num_rows > 0) {
|
||||||
|
$row = $result->fetch_assoc();
|
||||||
|
$image = $row['image'];
|
||||||
|
}
|
||||||
|
$check_stmt->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is an update or insert
|
||||||
|
if ($article_id) {
|
||||||
|
// Update existing blog
|
||||||
|
$stmt = $conn->prepare("UPDATE blogs SET title = ?, content = ?, description = ?, category = ?" . ($image ? ", image = ?" : "") . " WHERE blog_id = ?");
|
||||||
|
if ($image) {
|
||||||
|
$stmt->bind_param("sssssi", $title, $content, $description, $category, $image, $article_id);
|
||||||
|
} else {
|
||||||
|
$stmt->bind_param("ssssi", $title, $content, $description, $category, $article_id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Insert new blog
|
||||||
|
$stmt = $conn->prepare("INSERT INTO blogs (author, title, content, description, category, date, image) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||||
|
$stmt->bind_param("issssss", $user_id, $title, $content, $description, $category, $date, $image);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt->execute();
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
header("Location: blog.php");
|
||||||
40
src/processors/blog/upload_blog_image.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||||
|
require_once($rootPath . "/src/config/env.php");
|
||||||
|
require_once($rootPath . "/src/config/connection.php");
|
||||||
|
require_once($rootPath . "/src/config/functions.php");
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if (!isset($_FILES['file'])) {
|
||||||
|
echo json_encode(['error' => 'No file uploaded']);
|
||||||
|
http_response_code(400);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get blog_id from query parameter
|
||||||
|
$blog_id = isset($_GET['blog_id']) ? intval($_GET['blog_id']) : null;
|
||||||
|
if (!$blog_id) {
|
||||||
|
echo json_encode(['error' => 'Blog ID required']);
|
||||||
|
http_response_code(400);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetDir = $rootPath . "/uploads/blogs/" . $blog_id . "/";
|
||||||
|
if (!file_exists($targetDir)) {
|
||||||
|
mkdir($targetDir, 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tmp = $_FILES['file']['tmp_name'];
|
||||||
|
$name = basename($_FILES['file']['name']);
|
||||||
|
$targetFile = $targetDir . uniqid() . "-" . $name;
|
||||||
|
|
||||||
|
if (move_uploaded_file($tmp, $targetFile)) {
|
||||||
|
// Return a relative path for the image
|
||||||
|
$relativePath = "/uploads/blogs/" . $blog_id . "/" . basename($targetFile);
|
||||||
|
echo json_encode(['location' => $relativePath]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['error' => 'Failed to move uploaded file']);
|
||||||
|
http_response_code(500);
|
||||||
|
}
|
||||||
BIN
uploads/blogs/1/blog_01.jpeg
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
uploads/blogs/1/blog_02.jpeg
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
uploads/blogs/1/blog_03.jpeg
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
uploads/blogs/1/blog_04.jpeg
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
uploads/blogs/1/blog_05.jpeg
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
uploads/blogs/1/blog_06.jpeg
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
uploads/blogs/1/blog_07.jpeg
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
uploads/blogs/1/blog_08.jpeg
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
uploads/blogs/1/blog_09.jpeg
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
uploads/blogs/1/blog_10.jpeg
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
uploads/blogs/1/blog_11.jpeg
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
uploads/blogs/1/blog_12.jpeg
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
uploads/blogs/1/blog_13.jpeg
Normal file
|
After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 472 KiB After Width: | Height: | Size: 472 KiB |
BIN
uploads/blogs/24/693674d37c228-base4.jpg
Normal file
|
After Width: | Height: | Size: 259 KiB |
BIN
uploads/blogs/24/b5ae9fda32440829b0c2cb4133bd0881.jpg
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
uploads/blogs/24/cd2e45f41cc38309f6de5ba9ed08b1a3.jpg
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
uploads/blogs/24/cover.jpg
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
uploads/blogs/25/cover.jpg
Normal file
|
After Width: | Height: | Size: 277 KiB |
|
Before Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |